# Package Format Armature packages are distributed as `.pkg` files -- ZIP archives with a standard internal structure. ## ZIP Structure ``` my-package.pkg ├── manifest.json # required -- package metadata ├── js/ # browser-side JavaScript │ └── index.js ├── css/ # stylesheets │ └── styles.css ├── script.star # Starlark entry point ├── star/ # additional Starlark modules ├── assets/ # static assets (images, etc.) └── migrations/ # schema migration files ``` Only `manifest.json` is required. All other directories are optional and included only if present in the source. ## manifest.json Reference ```json { "id": "my-package", "title": "My Package", "type": "surface", "tier": "browser", "version": "1.0.0", "description": "What this package does.", "icon": "📦", "author": "your-name", "route": "/s/my-package", "auth": "authenticated", "permissions": [], "api_routes": [], "db_tables": {}, "settings": {}, "hooks": {}, "exports": [], "schema_version": 1 } ``` ### Required Fields | Field | Description | |-------|-------------| | `id` | Unique kebab-case identifier | | `title` | Human-readable display name | | `type` | `surface`, `extension`, `full`, `library`, `workflow` | | `tier` | `browser`, `starlark`, `sidecar` | | `version` | Semver version string | ### Optional Fields | Field | Description | |-------|-------------| | `description` | Short description | | `icon` | Emoji for sidebar/menu display | | `author` | Package author | | `surfaces` | Array of surface entries with per-path access, title, layout (see below) | | `route` | *(deprecated — use `surfaces`)* URL path for surfaces | | `auth` | Default access level for all surfaces: `authenticated`, `public`, `admin` | | `layout` | Default layout for all surfaces: `single`, `editor` | | `permissions` | Array of required capabilities (`db.write`, `http`, `notifications`, `secrets`, `realtime.publish`) | | `api_routes` | Array of `{"method": "GET", "path": "/items"}` | | `db_tables` | Table definitions with columns and indexes | | `settings` | User-configurable settings with type, label, description, default | | `hooks` | Event bus subscription patterns | | `exports` | Functions exported for cross-package calls via `lib.require()` | | `depends` | Array of package IDs this package depends on | | `slots` | Named UI injection points (host surfaces declare these) | | `contributes` | Slot contributions this package injects into other surfaces | | `capabilities` | Environment requirements: `{"required": [...], "optional": [...]}` | | `schema_version` | Integer for additive schema migrations | | `form_template` | Typed form template (v0.9.5). Fields array or fieldsets for progressive forms. Validated at install. | ## Multi-Surface Packages A package can serve multiple pages, each with its own path, access level, title, and layout. Declare a `surfaces` array in the manifest: ```json { "id": "bug-tracker", "title": "Bug Tracker", "type": "full", "auth": "authenticated", "layout": "single", "surfaces": [ { "path": "/", "title": "Dashboard" }, { "path": "/submit", "access": "public", "title": "Report a Bug" }, { "path": "/:id", "title": "Bug Detail", "nav": false }, { "path": "/admin", "access": "admin", "title": "Settings", "nav": false } ] } ``` ### Surface Entry Fields | Field | Type | Default | Description | |----------|--------|----------------|-------------| | `path` | string | **required** | Relative to `/s/{pkg-id}`. Supports `:param` segments. | | `access` | string | package `auth` | `public`, `authenticated`, `admin`, `group:{name}`. | | `title` | string | package `title`| Human label. Used in nav if `nav: true`. | | `layout` | string | package `layout`| `single`, `editor`, or future layouts. | | `nav` | bool | see rules | Show in sidebar navigation. | ### Nav Visibility Rules - `path: "/"` defaults to `nav: true` (primary entry point) - All others default to `nav: false` (sub-pages) - Explicit `"nav": true` overrides — a package can put multiple entries in nav - The sidebar links to the first surface with `nav: true` ### Backward Compatibility If `surfaces` is absent, the kernel synthesizes one entry from the legacy `auth` and `layout` fields. No existing packages break. ### Client-Side Navigation Within a multi-surface package, use `sw.navigate()` for SPA-style routing: ```js const path = window.__SURFACE_PATH__ || '/'; const params = window.__SURFACE_PARAMS__ || {}; if (path === '/') renderDashboard(); if (path === '/submit') renderSubmitForm(); if (path === '/:id') renderDetail(params.id); // Navigate to another surface within the package sw.navigate('/submit'); sw.navigate('/:id', { id: 'bug-42' }); // Listen for navigation events (including back/forward) sw.on('surface.navigate', ({ path, params }) => { renderView(path, params); }); ``` ## Composability: Slots and Contributions Packages compose with each other through named UI injection points (**slots**) and **contributions**. ### Declaring Slots (Host Surfaces) Surfaces declare slots where other extensions can inject UI: ```json { "slots": { "toolbar-actions": { "description": "Toolbar action buttons", "context": { "noteId": "string — current note ID", "getContent": "function — returns note body text" } } } } ``` Slot names are namespaced at runtime as `{package-id}:{slot-name}` (e.g., `notes:toolbar-actions`). The `context` field documents what data the slot provides — it is not enforced at runtime. ### Contributing to Slots Extensions declare which slots they inject into: ```json { "contributes": { "notes:toolbar-actions": { "label": "Dictate", "icon": "🎤", "description": "Voice-to-text dictation" } } } ``` Contributing extensions can be installed before or after their host surface — the coupling is soft. The admin can view all slots and their contributors at `GET /api/v1/admin/slots`. ### Cross-Package Function Calls Any package that declares `exports` can be called via `lib.require()`, not just library-type packages. The caller must declare the target in its `depends` array: ```json { "depends": ["image-gen"], "permissions": ["api.http"] } ``` ## Package Lifecycle 1. **Install**: Upload a `.pkg` file via Admin > Packages or `POST /api/v1/admin/packages/install`. The kernel extracts the archive, creates database tables, and registers routes. 2. **Enable**: Activate the package so its routes, hooks, and UI become available. `PUT /api/v1/admin/packages/:id/enable`. 3. **Disable**: Deactivate without removing data. `PUT /api/v1/admin/packages/:id/disable`. 4. **Update**: Upload a new `.pkg` with a higher semver version. Schema changes must be additive (new columns/tables only). `POST /api/v1/admin/packages/:id/update`. 5. **Export**: Download the installed package as a `.pkg` archive. `GET /api/v1/admin/packages/:id/export`. 6. **Delete**: Remove the package and its data. `DELETE /api/v1/admin/packages/:id`. ## Bundled vs User-Installed **Bundled packages** ship inside the Docker image at `/app/bundled-packages`. On first boot, a curated default set is auto-installed. Behavior: - First boot: curated defaults are installed and enabled. - Subsequent boots: already-installed packages are skipped. - Admin uninstalls: the package stays uninstalled (never force-reinstalled). - Control via `BUNDLED_PACKAGES` env var: empty = defaults, `"*"` = all, or comma-separated IDs. **User-installed packages** are uploaded through the Admin UI or API. They follow the same lifecycle but are not tied to the Docker image. ## Building Packages Packages are built by zipping the standard directories alongside `manifest.json`: ```bash # Build all packages in the packages/ directory cd packages && bash build.sh # Build a single package cd packages && bash build.sh my-package ``` Output goes to `dist/my-package.pkg`. ### Manual Build ```bash cd packages/my-package zip -r ../../dist/my-package.pkg manifest.json js/ css/ script.star ``` The build script automatically includes whichever standard directories exist: `js/`, `css/`, `assets/`, `script.star`, `star/`, `migrations/`. ### Custom Docker Image To bundle custom packages into a Docker image: 1. Place your package in `packages/your-package/` with a `manifest.json`. 2. Run `cd packages && bash build.sh all`. 3. Build the image: `docker build -t my-armature .` The Dockerfile builds all packages and copies them into the bundled packages directory.