@synopackageland/cli
v0.17.4
Published
Syno Package Land Developer Kit CLI
Readme
Syno Package Land CLI
synopkgland builds Synology DSM packages (SPK). Pick a template, declare what your app needs,
and the CLI checks, builds and installs the package.
Full documentation: https://www.varvis.me/docs/ (繁體中文).
Install
npm install -g @synopackageland/cli --registry=https://registry.npmjs.orgNative Go apps need Go 1.25+ and npm. The CLI is tested on Node.js 22.19.
Quick start
synopkgland init -t native-go --with files,jobs,login --package MyApp my-app
cd my-app
(cd frontend && npm install)
synopkgland check --json
synopkgland build --arch x86_64 --json
synopkgland inspect dist/MyApp-0.1.0-0001-x86_64.spk --json
synopkgland deploy --host my-nas --arch x86_64 --json
synopkgland status --host my-nas --jsondeploy --dry-run prints the deployment plan without connecting to a NAS. You can also install
the SPK with Package Center's Manual Install.
Templates
| Template | Use it for | Manifest |
| --- | --- | --- |
| native-go | A Go backend with a Vue UI (or --ui plain) | dsm-package.yaml |
| native-python, native-node, native-rust | The native-go App with a Python, Node.js (TypeScript) or Rust backend | dsm-package.yaml |
| binary | A static Linux program you build yourself | dsm-package.yaml |
| compose | Unchanged upstream Compose, generated override and SDK planner | dsm-package.yaml |
Existing native products with their own payload layout use backend: {kind: native} in
dsm-package.yaml.
A backend in Python, Node or Rust uses backend: {runtime: python|node|rust, ...}: the CGI is
the SDK's Go runtime, which checks the DSM session and permissions.json and starts the App
from target/app as the package account for each request (see skills/reference/create.md).
SDK and templates
The CLI packages, deploys and checks. The app-side code is the SDK
(synopackageland/syno-pkg-land-sdk): the Go module
github.com/synopackageland/syno-pkg-land-sdk and the npm package @synopackageland/sdk (Vue components at
@synopackageland/sdk/vue). The require github.com/synopackageland/syno-pkg-land-sdk line in
backend/go.mod is the app's only SDK version; build compiles the executor of that version
and checks it against the digests shipped in this CLI. A CLI builds the SDK releases of its own minor version
(CLI X.Y.z builds SDK X.Y.*) that its digest table records; EXECUTOR_DIGEST_MISMATCH names the ones it knows.
The SDK repositories are private. The CLI adds github.com/synopackageland/* to its Go
children's GOPRIVATE, GONOPROXY and GONOSUMDB, preserving existing environment and
go env -w settings. Give git a credential that can read
https://github.com/synopackageland/syno-pkg-land-sdk (check with git ls-remote);
a failed fetch reports SDK_DOWNLOAD_FAILED.
CLI-only patch releases can differ from the SDK patch. Compose import and SDK defaults read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
init and add read their templates from @synopackageland/templates, an exact-pinned
dependency. For development against local checkouts, set SYNOPKGLAND_TEMPLATES_DIR to a
templates checkout and SYNOPKGLAND_SDK_DIR to an SDK checkout; SPKs built with
SYNOPKGLAND_SDK_DIR warn EXECUTOR_DIGEST_UNVERIFIED and must not be published. A Vue project
packs the local SDK into its frontend (npm install --install-links) without building it: run
npm ci && npm run build in $SYNOPKGLAND_SDK_DIR/ts first, and again after SDK changes.
To check the CLI's DSM terminology rules against templates without a new templates release:
SYNOPKGLAND_TEMPLATES_DIR=/path/to/templates npm run test:glossaryThis compares OFF_GLOSSARY and the Italian sentence rule, including regex sources and
flags. It supports declaration-only rules with read-only use in glossaryProblems;
post-declaration mutations, aliases and other unsupported rule uses fail the check instead of
comparing a partial snapshot. The command also fails on drift or missing source. It needs a checkout because templates
0.15.1 does not publish scripts/test/i18n.test.mjs. The npm gate runs this check when
SYNOPKGLAND_TEMPLATES_DIR is set; otherwise it explicitly reports that the drift check was skipped.
Capabilities
Add them with init --with … or later with synopkgland add <capability…>.
| Capability | What the app can do | Who approves |
| --- | --- | --- |
| files | Browse, read and write NAS files as the signed-in DSM user | Nobody; File Station applies that user's permissions |
| jobs | Run background jobs and schedules as the package | Nobody |
| login | Sign users in with their DSM account, without the DSM desktop | Nobody; requires HTTPS for password sign-in |
| delegate | Move files into a user's chosen folder while the user is offline | An administrator, once |
| root-ops | Run fixed root/*.sh scripts for administrators | An administrator, once |
| portal | Open the app from its own Application Portal alias, with a login page style (login_style changes the colour, wallpaper and title) | An administrator applies the alias; DSM 7.2-64570+ needs no executor (INFO asks DSM to reload nginx on uninstall). DSM 7.1-42661+ needs an executor to register the entry |
Delegate is implemented; its default operation is files.moveFromStaging.
Device lifecycle acceptance on real hardware is still pending for delegate and root operations.
Lifecycle scripts
Every template has a dsm-scripts/ folder with DSM's own script names: preinst, postinst,
preupgrade, postupgrade, preuninst, postuninst and start-stop-status. Each does nothing
until you edit it. build packs them and calls each one from the generated script of the same
name, after the SDK's steps (before them for preupgrade, preuninst and stop); they run as
the package account, with DSM's arguments, environment and exit codes. Projects without
dsm-scripts/ build as before. See synopkgland skill show lifecycle.
Root work after an install or upgrade, such as removing what an older version left, is not a
lifecycle script: name a root/ program as root.start in dsm-package.yaml. The executor runs
it as root once at the first start of each new version, while the administrator's approval is
live, for at most 60 seconds; a failure is logged and does not block the start, and the executor
status reports the result as start_step. While it runs, the executor answers only status
(state: "starting") and refuses other requests with the retryable EXECUTOR_STARTING; the DSM
window's approval waits for it, while a status read answers executor_starting at once and the
SDK's panels check again on their own. The program cannot declare run or job, and
declaring it changes the policy digest, so an administrator approves the App again.
Commands
| Command | Purpose |
| --- | --- |
| init | Create a project from a template |
| add | Add capabilities or portal to a project |
| check | Validate the manifest, capability combinations and Compose file |
| build | Build the SPK for one or more architectures |
| inspect | Check any SPK against the DSM 7 package format without installing it; SPKs built by synopkgland also get the build contract |
| deploy | Build and install on a NAS |
| status | Show package state, Open URL and capability readiness |
| logs | Show package status diagnostics |
| dev, test, images | Run, test and pin images for Compose projects |
| skill | Read the bundled guidance for AI assistants |
--json prints one JSON object on stdout: {ok: true, command, data} on success and
{ok: false, code, message, hint} on failure. See the
CLI reference and
error codes.
E2E helpers
@synopackageland/cli/e2e holds the DSM helpers for a package's real-NAS
suite on e2e and @e2e-dev/web
(optional peers, with ai and @ai-sdk/openai). The same file,
src/e2e/dsm.ts, can be copied verbatim to tests/e2e/support/dsm.ts.
// e2e.config.ts
import {dsmTarget, magpieAgent} from '@synopackageland/cli/e2e';
export default {targets: [dsmTarget()], output: '.e2e/output', agents: {default: magpieAgent()}};
// tests/e2e/dsm.setup.e2e.ts: signs in once (a private saved profile when it still works)
import {dsmLoginSetup} from '@synopackageland/cli/e2e';
dsmLoginSetup({session: 'dsm', user: 'admin'});
// a test that declares {sessions: ['dsm']}
import {dismissNotifications, openApp} from '@synopackageland/cli/e2e';
const window = await openApp(app, 'SYNO.SDS.MyApp.Instance'); // closes DSM's notification cards first
await dismissNotifications(); // when a card shows later"scripts": {"e2e:nas": "node --env-file-if-exists=.env node_modules/@synopackageland/cli/dist/e2e/dsm.js tunnel -- e2e run"}runWithTunnel (the tunnel -- command) opens an SSH tunnel to the NAS
(DSM_E2E_SSH, required, e.g. [email protected]) on a free loopback port and runs the
command with DSM_E2E_URL set. It masks DSM's session cookies (id, did,
_SSID), X-SYNO-TOKEN, SynoToken and _sid as *** in the command's
output and in the files under DSM_E2E_OUTPUT_DIR (default .e2e), and makes
that directory private (700, files 600); saved sessions keep their cookies.
Set DSM_E2E_SSH to your NAS SSH destination in the environment or a local
untracked .env; an unset or blank value fails before SSH or the command starts.
nas.example is a fixture hostname; replace it with your own host. Accounts come
from mode 600 files named by DSM_E2E_ACCOUNT_<USER>. Saved profiles use
DSM_E2E_TARGET (default dsm) and the user as their names; set the target to
an existing profile's target name to keep reusing it.
signIn and openDesktop also register those values as e2e secrets in the
test's worker, so e2e masks them in its own report. A test that forwards
requests with context.route and route.fetch should end with
await context.unrouteAll({behavior: 'ignoreErrors'}): a request still in
flight otherwise rejects into a run error that carries its headers.
AI assistants
Every project includes AGENTS.md and a CLAUDE.md pointer. Start with
synopkgland skill list and synopkgland skill show orchestrator. For UI work, read
synopkgland skill show ui-vue (Vue components) or synopkgland skill show ui-json
(JSON specs). From an installed frontend, npx --no syno-pkg-land-ui catalog --json lists the
UI components and npx --no syno-pkg-land-ui describe Button --json describes one.
Licensing
The CLI is MIT licensed, Copyright (c) 2026 synopkgland. Third-party components keep their own terms.
- Generated projects keep the template terms in
LICENSES/TEMPLATE.txt.initdoes not choose a license for your app: add your ownLICENSE. - An SPK carries its legal notices under
LICENSES/in the installed payload:TEMPLATE.txt, an optionalAPP.txt,SDK.txt(the SDK's MIT license, whenbackend/go.modrequires the SDK), Go's license, patents and third-party notices, and the Vue UI notices. - Vue builds generate the UI notices from the installed dependencies; run
npm installinfrontend/first. A missing notice or version mismatch fails the build. - Keep
backend/THIRD_PARTY_NOTICES.txtcurrent when you change Go modules. Your own binaries, added modules, container images and upstream apps need their own notices.
Delegated JSON WebAPI profiles need SDK 0.11.0 or later. Declare:
capabilities:
webapi:
calendar:
purpose: Create calendar reminders after workflow approval
apis:
SYNO.Cal.Event:
list: [6]
create: [6]
delete: [6]Use matching backend/frontend SDK pins. The profile participates in the normal executor approval digest; a profile change needs administrator approval. Users grant the named profile separately. The permanent denylist, current-user permission checks, bounded JSON and uncertain-outcome rules are in the SDK's executor/README.md. Binary downloads require later SDK support.
Raw WebAPI profiles
SDK 0.12.1 add opt-in raw responses. In capabilities.webapi.<profile>,
set raw_max_bytes: 26214400 for a 25 MiB MIME limit and explicitly allow
SYNO.MailClient.Message.get_original v10. Omitting the limit preserves JSON-only
profiles. Changing it changes the approved policy and requires fresh user consent.
Call webapi.call with response_type: "raw"; Go Apps use
JobContext.WebAPI().CallRaw. Small bodies use base64; large bodies use the
package's private executor-staging folder with size/SHA-256 and immediate cleanup.
Do not use Attachment.download or follow X-Accel-Redirect. See the SDK raw response
contract for the complete transport and CGI validation rules.
App launch mode
Native Apps declare ui.launch: dsm-window | new-page in dsm-package.yaml.
Use dsm-window for frontends built with @synopackageland/ui; use new-page
for every other frontend. Plain/binary scaffolds explicitly choose new-page; Vue uses the native default. An omitted
native ui.launch resolves to dsm-window; the CLI does not guess a frontend's
framework from its build command. new-page registers DSM type: "standalone",
so the main menu and Package Center open a full page on the DSM origin.
ctx.fullPage, ctx.appIcon, session/SynoToken, launch parameters and entry
permissions use the same wrapper in both modes. The wrapper sets the standalone
page title, favicon and device-width viewport. It retains ExtJS; pureEnv is
not appropriate, and alternate Portal ports are not needed (allowAltPort is omitted).
A full page does not need Application Portal. Omit portal unless an alias is
explicitly wanted; only that opt-in adds defaultAlias, login styling, Portal
panels and capabilities. No Portal is a healthy default for inspect/info/doctor.
Compose uses schema-2 dsm-package.yaml and ui.launch; the default is new-page.
Classless gateways use type url via the shared contract/app-launch resolver;
AppWindow wrappers use standalone. Headless projects have no launcher.
Rebuilt container Apps include informational Registry check diagnostics in preinst, postinst, postupgrade and prestart. Messages use the existing nine-language catalog and end with the native recovery step: fix the cause, then use Package Center Repair. Repair re-runs the install worker and downloads missing images; it is not an SDK Retry action. Checks use only immutable image references from the packaged lock, never the Docker socket or registry credentials. They do not block cached/offline startup or establish daemon disk space, image existence or pull success. Native wrapper codes alone never identify a cause. SDK and CLI changes must ship together and Apps must be rebuilt to receive these hooks and catalogs.
See Compose for multiple upstream files, generated override/worker artifacts, image proofs and the support matrix. Containers use a fixed package identity and do not inherit a signed-in user's File Station ACL. Shared Folder intent requires admin confirmation after installation; official protected ACL/project control and bridge readback remain blocked. No SSO Server or wrapper container is added. Linux tests do not prove DSM readiness; inspect lists explicit acceptance blockers.
Control installed Apps
Use synopkgland ctl host add nas --url https://nas.example:5001 --username alice
to register and authenticate a NAS in OS secure storage. ctl host list --json
checks sessions; ctl list --host nas --json discovers all approved task-level
commands and their schemas. Run the listed alias/command with typed flags,
--help and --dry-run; writes require --yes. There is no mutable current
context. Use --host a,b or --all-hosts for bounded parallel operations.
ctl preserves these process exit codes, including with --json:
| Exit code | Meaning and examples |
| --- | --- |
| 0 | Command completed successfully (including help and --dry-run). |
| 1 | Unexpected internal error (CTL_INTERNAL), or an uncaught CLI exception. |
| 2 | Authentication/session failure: CTL_NOT_AUTHENTICATED, CTL_AUTH_FAILED, CTL_CREDENTIAL_MISMATCH, or unavailable OS credential storage. |
| 3 | Invalid parameters, schema validation or unknown names: CTL_PARAMS_INVALID, CTL_ARGUMENT_INVALID, CTL_APP_UNKNOWN, CTL_COMMAND_UNKNOWN. |
| 4 | Permission refusal that fails the command: CTL_PERMISSION_DENIED, CTL_INVENTORY_DENIED. |
| 5 | App/contract failure that fails the command: CTL_APP_ERROR, CTL_RESPONSE_INVALID, CTL_JOB_FAILED. |
| 6 | Network/HTTP failure or uncertain completion: CTL_NETWORK, CTL_HTTP_ERROR, CTL_JOB_TIMEOUT, CTL_LOGOUT_FAILED. |
| 7 | Confirmation required (CTL_CONFIRM_REQUIRED): a write/destructive command or host removal without confirmation; no business operation runs. |
| 8 | A multi-host command has at least one failed host (including when all hosts fail); inspect each host's error. |
For example, synopkgland ctl studio drafts create --host nas --json without
--yes returns ok:false with CTL_CONFIRM_REQUIRED and exits 7 once discovery
and parameter validation succeed. Missing sessions fail earlier with exit 2.
Discovery catches per-App description errors and excludes those Apps from its
results. For example, a changed pinned description (CTL_DESCRIPTION_CHANGED)
or an invalid descriptor (CTL_DESCRIPTOR_INVALID) appears in
ctl list --host nas --refresh --json under data.diagnostics; the listing still
returns ok:true and exits 0, even if no Apps remain. These diagnostic codes do
not become process exit codes 4 or 5. Attempting to execute an excluded App with
--refresh instead returns ok:false, CTL_APP_UNKNOWN and exit 3 because the
App is absent from discovery results; no business operation runs.
ctl host list/show can also succeed while reporting authenticated:false and
exit 0. Check discovery apps/diagnostics and session fields as well as the exit
code when deciding whether an App is ready.
After a network or job timeout, query the App's state before retrying a write.
Read synopkgland skill show ctl for the AI workflow, batch discovery approval,
cache refresh, host removal/revocation and explicit ctl.commands authoring.
API descriptions are private SPK files named synopkgland-api.json, served
only by the authenticated fixed synopkgland.describe CGI operation.
Generated SDK windows expose ctx.batch([{method, params}, ...]) when the
selected SDK supplies its batch factory. One to eight independent JSON reads
share a request; results keep input order and retain per-method errors. A batch
uses the same token recovery, request deadline and cancellation as ctx.call,
and is never replayed automatically. Older SDKs and binary backends retain the
single-call client. Rebuild the window with the current CLI/SDK to adopt it.
