Skip to content

Manifests and packaging

An app ships as one JSON file. This page documents the source manifest you edit, the package the CLI builds from it, the GitHub descriptor for distribution, and every limit the desktop enforces when it parses them.

Availability

The CLI ships with @techartdev/shellcanvas-app-sdk and needs Node.js 20 or newer. Parsers are importable from the package's /package and /repository entry points, so a build pipeline can validate without the CLI:

ts
import { parseAppPackage, parseAppManifest } from "@techartdev/shellcanvas-app-sdk/package";
import { parseAppRepository } from "@techartdev/shellcanvas-app-sdk/repository";

Interface

Source manifest — shellcanvas.json

json
{
  "$schema": "./node_modules/@techartdev/shellcanvas-app-sdk/schemas/app-manifest.schema.json",
  "format": 1,
  "kind": "app",
  "id": "org.example.notes",
  "version": "0.1.0",
  "title": "My Notes",
  "permissions": ["system.storage", "system.dialogs"]
}
FieldRequiredRules
formatyesExactly 1.
kindyesExactly "app".
idyesDotted lowercase, at least two segments: ^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$. Identity for storage, updates and grants — changing it makes a different app.
versionyesmajor.minor.patch, with an optional -prerelease suffix.
titleyesNon-blank, at most 100 UTF-16 units.
permissionsyesArray of unique, dotted permission identifiers. May be empty.
descriptionnoNon-blank, at most 160 UTF-16 units, no control characters or line separators.
iconnoRelative path to a .png, .jpg, .jpeg, .webp or .svg file beside the manifest.
clientPlatformsnoNon-empty unique subset of windows, macos, linux, android, ios, web. Omit it when your app is platform-neutral.

Unknown top-level keys are rejected. description and icon are newer fields: a desktop older than 0.1.2 refuses a package that uses them.

Built package — dist/app.shellcanvas.json

The package has the same fields, except that icon becomes an embedded data URI and two fields are added by the build:

FieldContent
scriptThe bundled, self-contained JavaScript.
styleThe contents of style.css, possibly empty.
icondata:image/png;base64,… (or jpeg, webp, svg+xml), at most 256 KiB decoded, checked against the file's real magic bytes.

Parser limits: the raw text of a manifest or package may not exceed 16 Mi UTF-16 units. A package that fails any rule is rejected as a whole — Invalid or unsupported app package. — rather than partially loaded.

Repository descriptor — shellcanvas.repo.json

json
{
  "format": 1,
  "kind": "app-repository",
  "id": "org.example.notes",
  "version": "0.1.0",
  "title": "My Notes",
  "description": "Quick notes for your hosts",
  "package": { "path": "dist/app.shellcanvas.json", "sha256": "…64 hex characters…" }
}

It lives at the repository root. description is required (it may be an empty string) and is capped at 1000 characters. package.path must stay inside the repository: 1–240 characters, no . or .. segments. The descriptor itself may not exceed 64 KiB, and the package it points at may not exceed 32 MiB.

CLI

shellcanvas-app init <new-directory> --id org.example.notes --title "Notes" [--sdk <sdk.tgz>]
shellcanvas-app build [directory] [--version 0.2.0]
shellcanvas-app validate <app.shellcanvas.json>
shellcanvas-app repository [directory] [--path dist/app.shellcanvas.json] [--description "App description"]
  • init creates a new directory only, never overwriting. --sdk points the generated dependency at a packed .tgz instead of the published package.
  • build writes atomically through a temporary file, so a failed build leaves the previous artifact intact. --version overrides the version without editing the manifest — useful in CI.
  • validate runs the same parser the desktop uses.
  • repository regenerates the descriptor, including a fresh hash. Run it after every build you intend to publish.

Examples

Release checklist for a GitHub-distributed app:

sh
npm run build                                   # dist/app.shellcanvas.json
npx shellcanvas-app validate dist/app.shellcanvas.json
npx shellcanvas-app repository . --description "Quick notes for your hosts"
git add dist/app.shellcanvas.json shellcanvas.repo.json
git commit -m "Release 0.2.0"
git push

Keep the package bytes stable by adding a .gitattributes rule, or the hash in the descriptor will not match after checkout:

dist/app.shellcanvas.json -text

Validating a package in CI without the CLI:

ts
import { readFile } from "node:fs/promises";
import { parseAppPackage } from "@techartdev/shellcanvas-app-sdk/package";

const app = parseAppPackage(await readFile("dist/app.shellcanvas.json", "utf8"));
console.log(app.id, app.version, app.permissions.join(", "));

Errors

MessageCause
Invalid app source manifest.shellcanvas.json breaks a rule above, or contains an unknown key.
Invalid or unsupported app package.The built package failed validation, including an unknown field on an older desktop.
App manifest is too large. / App package is too large.The raw text exceeded 16 Mi UTF-16 units.
<path> is larger than 256 KiB.The icon file exceeds the decoded size limit.
<path> is not a valid PNG image.The icon's bytes do not match the extension.
Apps must bundle their code and keep CSS in style.css; external assets/imports are unsupported.The bundle left an import unresolved or produced more than one file.
--sdk must name a packed SDK .tgz file.--sdk did not point at a .tgz.
Unknown, repeated or incomplete option: <arg>A flag is misspelled, repeated, or missing its value.
Package path must stay inside the repository.--path escaped the repository root.
Repository manifest is too large. / Invalid or unsupported ShellCanvas repository manifest.The descriptor exceeded 64 KiB or broke a rule.
Package integrity check failed. The repository may have changed; review it again.The downloaded package's hash did not match the descriptor.
The package identity does not match the repository manifest.id, version or title differ between descriptor and package.

Lifecycle and permissions

The manifest's permissions array is the complete set your app can ever use: the desktop intersects it with what the user approved, so an ungranted call fails with denied even if the manifest lists it. Adding a permission in an update leaves it unselected during the update review until the user approves it.

Identity drives lifecycle. The id determines which installed app an update replaces; the desktop keeps a per-installation principal behind it, which is what app storage and saved connections are keyed to. Updating preserves the principal; removing and reinstalling creates a new one, so the new installation starts with empty storage and no saved connections. Windows that are already open keep the exact package and grants they started with until they close.

ShellCanvas documentation