@synopackageland/cli 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. package/dist/compose/privilege.js +0 -102
@@ -1,20 +1,8 @@
1
- import type { AppContract, ComposeDocument } from "../types.js";
2
- import { type ResourceWorkerEligibility } from "../compose/analyze.js";
3
- export { classifyComposeRoute, resourceWorkerEligibility, type ComposeRoute, type ComposeRouteClassification, type ResourceWorkerEligibility, type ResourceWorkerIneligibleReason, } from "../compose/analyze.js";
1
+ /** Static metadata for DSM's official Container Manager worker. No runtime code. */
4
2
  export declare function composeProjectName(packageId: string): string;
5
3
  /**
6
4
  * `forcePull` re-resolves every image tag on install, which can move an image
7
5
  * shared with other containers. Digest-pinned images cannot move, so it is only
8
6
  * needed (and kept, to preserve upgrade behaviour) while a tag is unpinned.
9
7
  */
10
- export declare function renderDockerProjectResource(packageId: string, projectPath?: string, forcePull?: boolean): Record<string, unknown>;
11
- export declare function renderWebserviceResource(contract: AppContract, adminport: number): Record<string, unknown>;
12
- export declare function renderResourceConfig(contract: AppContract, eligibility: ResourceWorkerEligibility, compose: ComposeDocument): string;
13
- /**
14
- * Renders the lifecycle control script for the Docker Project resource-worker
15
- * route. ContainerManager owns the actual project start/stop operation through
16
- * the worker timing hooks; the package script must wait for that operation to
17
- * leave a reachable service and must not claim running unconditionally.
18
- */
19
- export declare function renderResourceWorkerStartStopStatus(packageId: string, adminport?: number, headless?: boolean): string;
20
- export declare function renderHeadlessResourceMarker(): string;
8
+ export declare function renderDockerProjectResource(packageId: string, projectPath?: string, forcePull?: boolean, projectNameOverride?: string): Record<string, unknown>;
@@ -1,6 +1,4 @@
1
- import { collectUnpinnedImages } from "../compose/analyze.js";
2
- import { cookieStripHeaders, resolvedWebLaunch, usesAuthGateway, webStationAlias, webStationServiceName, } from "../contract/web-launch.js";
3
- export { classifyComposeRoute, resourceWorkerEligibility, } from "../compose/analyze.js";
1
+ /** Static metadata for DSM's official Container Manager worker. No runtime code. */
4
2
  export function composeProjectName(packageId) {
5
3
  const slug = packageId
6
4
  .toLowerCase()
@@ -13,8 +11,8 @@ export function composeProjectName(packageId) {
13
11
  * shared with other containers. Digest-pinned images cannot move, so it is only
14
12
  * needed (and kept, to preserve upgrade behaviour) while a tag is unpinned.
15
13
  */
16
- export function renderDockerProjectResource(packageId, projectPath = "compose", forcePull = true) {
17
- const projectName = composeProjectName(packageId);
14
+ export function renderDockerProjectResource(packageId, projectPath = "compose", forcePull = true, projectNameOverride) {
15
+ const projectName = projectNameOverride ?? composeProjectName(packageId);
18
16
  return {
19
17
  "docker-project": {
20
18
  projects: [
@@ -31,164 +29,3 @@ export function renderDockerProjectResource(packageId, projectPath = "compose",
31
29
  },
32
30
  };
33
31
  }
34
- export function renderWebserviceResource(contract, adminport) {
35
- const alias = webStationAlias(String(contract.package));
36
- const launch = resolvedWebLaunch(contract);
37
- const cookieHeaders = cookieStripHeaders(contract);
38
- const proxyHeaderEntries = cookieHeaders.map((header) => ({
39
- name: header.name,
40
- value: header.value,
41
- }));
42
- const service = {
43
- service: webStationServiceName(alias),
44
- display_name: String(contract.displayname ?? contract.package),
45
- type: "reverse_proxy",
46
- support_alias: true,
47
- support_server: false,
48
- proxy_target: `http://127.0.0.1:${adminport}`,
49
- proxy_headers: proxyHeaderEntries,
50
- };
51
- const portal = {
52
- service: webStationServiceName(alias),
53
- name: alias,
54
- type: "alias",
55
- alias,
56
- };
57
- if (launch === "dsm-window") {
58
- portal.app = String(contract.package);
59
- }
60
- return {
61
- webservice: {
62
- services: [service],
63
- portals: [portal],
64
- },
65
- };
66
- }
67
- export function renderResourceConfig(contract, eligibility, compose) {
68
- const packageId = String(contract.package);
69
- const forcePull = collectUnpinnedImages(compose).length > 0;
70
- const dockerResource = renderDockerProjectResource(packageId, "compose", forcePull);
71
- let combined = { ...dockerResource };
72
- // A gateway package is reached through its DSM-authenticated CGI only; a Web
73
- // Station portal in front of the container would be an anonymous second door.
74
- if (eligibility.adminport !== undefined && !usesAuthGateway(contract)) {
75
- const webResource = renderWebserviceResource(contract, eligibility.adminport);
76
- combined = {
77
- ...combined,
78
- ...webResource,
79
- };
80
- }
81
- return `${JSON.stringify(combined, null, 2)}\n`;
82
- }
83
- /**
84
- * Renders the lifecycle control script for the Docker Project resource-worker
85
- * route. ContainerManager owns the actual project start/stop operation through
86
- * the worker timing hooks; the package script must wait for that operation to
87
- * leave a reachable service and must not claim running unconditionally.
88
- */
89
- export function renderResourceWorkerStartStopStatus(packageId, adminport, headless = false) {
90
- if (headless) {
91
- return `#!/bin/sh
92
- SYNOPKG_PKGNAME="\${SYNOPKG_PKGNAME:-${packageId}}"
93
- SYNOPKGLAND_PROC_ROOT="\${SYNOPKGLAND_PROC_ROOT:-/proc}"
94
-
95
- probe_project_process() {
96
- for p in "$SYNOPKGLAND_PROC_ROOT"/[0-9]*; do
97
- [ -d "$p" ] || continue
98
- if grep -q docker "$p/cgroup" 2>/dev/null &&
99
- grep -q \\
100
- "@appstore/\${SYNOPKG_PKGNAME}/compose/" \\
101
- "$p/mountinfo" 2>/dev/null; then
102
- return 0
103
- fi
104
- done
105
- return 1
106
- }
107
-
108
- case "$1" in
109
- start|stop) exit 0 ;;
110
- status)
111
- probe_project_process
112
- exit $?
113
- ;;
114
- *) exit 1 ;;
115
- esac
116
- `;
117
- }
118
- if (adminport === undefined) {
119
- return `#!/bin/sh
120
- case "$1" in
121
- start|status)
122
- echo "resource-worker package has no published port to probe" >&2
123
- exit 3
124
- ;;
125
- stop) exit 0 ;;
126
- *) exit 1 ;;
127
- esac
128
- `;
129
- }
130
- const url = `http://127.0.0.1:${adminport}/`;
131
- return `#!/bin/sh
132
- probe_service() {
133
- if command -v curl >/dev/null 2>&1; then
134
- curl --silent --output /dev/null --connect-timeout 2 --max-time 5 "${url}"
135
- return $?
136
- fi
137
- if command -v wget >/dev/null 2>&1; then
138
- wget --quiet --output-document=/dev/null --timeout=5 --tries=1 "${url}"
139
- return $?
140
- fi
141
- return 127
142
- }
143
-
144
- wait_for_service() {
145
- attempts=0
146
- while [ "$attempts" -lt 30 ]; do
147
- if probe_service; then
148
- return 0
149
- fi
150
- attempts=$((attempts + 1))
151
- sleep 1
152
- done
153
- return 1
154
- }
155
-
156
- case "$1" in
157
- start)
158
- if wait_for_service; then
159
- exit 0
160
- fi
161
- exit 3
162
- ;;
163
- stop)
164
- exit 0
165
- ;;
166
- status)
167
- if probe_service; then
168
- exit 0
169
- fi
170
- exit 3
171
- ;;
172
- *)
173
- exit 1
174
- ;;
175
- esac
176
- `;
177
- }
178
- export function renderHeadlessResourceMarker() {
179
- return `#!/bin/sh
180
- set -eu
181
- PKGVAR="\${SYNOPKG_PKGVAR:?}"
182
- [ -d "$PKGVAR" ] || mkdir -p "$PKGVAR"
183
- [ ! -L "$PKGVAR" ] || { echo "package var is a symlink" >&2; exit 1; }
184
- MARKER="$PKGVAR/.docker-project-managed"
185
- [ ! -L "$MARKER" ] || { echo "docker-project marker is a symlink" >&2; exit 1; }
186
- tmp="$(mktemp "$PKGVAR/.docker-project-managed.tmp.XXXXXX")"
187
- trap 'rm -f "$tmp"' EXIT HUP INT TERM
188
- printf '%s\n' managed > "$tmp"
189
- chmod 600 "$tmp"
190
- mv "$tmp" "$MARKER"
191
- trap - EXIT HUP INT TERM
192
- exit 0
193
- `;
194
- }
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { codedMessage, formatErrors } from "./errors.js";
9
9
  import { getErrorCode } from "./error-registry.js";
10
10
  import { formatInspectReport, inspectProject } from "./inspect.js";
11
11
  import { formatPinReport, pinProjectImages } from "./images/pin.js";
12
- import { initFromTemplate, initNextSteps, isTemplateName, listTemplates } from "./init.js";
12
+ import { initFromTemplate, initNextSteps, isTemplateName, listTemplates, } from "./init.js";
13
13
  import { templatesRoot } from "./template-paths.js";
14
14
  import { addCapabilities } from "./add.js";
15
15
  import { CAPABILITY_NAMES } from "./capabilities.js";
@@ -29,9 +29,9 @@ import { followProjectLogs, followStandaloneLogs } from "./log.js";
29
29
  import { formatSkillList, readSkill, resolveSkillsRoot } from "./skill.js";
30
30
  import { resolveInstallParameterExtraValues } from "./deploy/param-values.js";
31
31
  import { findStandaloneRoot, loadStandaloneManifest, } from "./standalone/manifest.js";
32
- import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES } from "./standalone/native-layout.js";
32
+ import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES, } from "./standalone/native-layout.js";
33
33
  import { buildStandalone, runnerFor } from "./standalone/build.js";
34
- import { checkJobs, commitOutputs, forEachArch } from "./standalone/arch-jobs.js";
34
+ import { checkJobs, commitOutputs, forEachArch, } from "./standalone/arch-jobs.js";
35
35
  import { checkReproducible } from "./reproducible.js";
36
36
  import { inspectAnySpk } from "./spk/inspect.js";
37
37
  function failure(code, message, hint = "Fix the configuration and retry; use --help to see command usage.") {
@@ -61,9 +61,19 @@ function formatIconReport(report, manifestFile) {
61
61
  lines: report.source === "custom"
62
62
  ? [`Icon: custom ${report.path}`]
63
63
  : report.source === "layout"
64
- ? ["Icon: provided by the layout metadata's PACKAGE_ICON*.PNG (if present)", `info: set icon: in ${manifestFile} to have the CLI generate the Package Center icon.`]
65
- : ["Icon: SDK default icon", `info: set icon: in ${manifestFile} to <a square PNG (at least 256x256) or an icon_<size>.png directory> to use your own icon.`],
66
- ...(report.solid ? { warning: "warning: the icon is a single colour, so Package Center and the DSM main menu show a flat block; use an icon with a graphic." } : {}),
64
+ ? [
65
+ "Icon: provided by the layout metadata's PACKAGE_ICON*.PNG (if present)",
66
+ `info: set icon: in ${manifestFile} to have the CLI generate the Package Center icon.`,
67
+ ]
68
+ : [
69
+ "Icon: SDK default icon",
70
+ `info: set icon: in ${manifestFile} to <a square PNG (at least 256x256) or an icon_<size>.png directory> to use your own icon.`,
71
+ ],
72
+ ...(report.solid
73
+ ? {
74
+ warning: "warning: the icon is a single colour, so Package Center and the DSM main menu show a flat block; use an icon with a graphic.",
75
+ }
76
+ : {}),
67
77
  };
68
78
  }
69
79
  const LABEL = { error: "error", warning: "warning" };
@@ -95,8 +105,12 @@ function formatSpkChecks(report) {
95
105
  function formatRootSteps(report) {
96
106
  const policy = report.executor?.policy;
97
107
  const steps = [
98
- ...(policy?.start ? [`start ${policy.start} (once after each install or upgrade)`] : []),
99
- ...(policy?.stop ? [`stop ${policy.stop} (at package stop and upgrade)`] : []),
108
+ ...(policy?.start
109
+ ? [`start ${policy.start} (once after each install or upgrade)`]
110
+ : []),
111
+ ...(policy?.stop
112
+ ? [`stop ${policy.stop} (at package stop and upgrade)`]
113
+ : []),
100
114
  ...(policy?.purge ? [`purge ${policy.purge} (at uninstall)`] : []),
101
115
  ];
102
116
  return steps.length ? [`Root steps: ${steps.join(", ")}`] : [];
@@ -105,19 +119,47 @@ function formatSpkReport(spk, report) {
105
119
  return [
106
120
  `SPK: ${spk}`,
107
121
  `package: ${report.package ?? "?"} version: ${report.version ?? "?"}`,
108
- report.synopkgland ? `arch: ${report.arch ?? "?"} runtime: ${report.kind ?? "?"}` : `arch: ${report.arch ?? "?"} build: not synopkgland (DSM format only)`,
122
+ report.synopkgland
123
+ ? `arch: ${report.arch ?? "?"} runtime: ${report.kind ?? "?"}`
124
+ : `arch: ${report.arch ?? "?"} build: not synopkgland (DSM format only)`,
125
+ ...(report.compose
126
+ ? [
127
+ `Compose readiness: ${report.compose.readiness.profile}`,
128
+ report.compose.readiness.limitation,
129
+ ]
130
+ : []),
109
131
  `Signature: ${report.signed ? "Synology-signed" : "unsigned (Package Center must allow any publisher)"}`,
110
132
  ...(report.synopkgland ? [`CGI: ${report.cgi ?? "none"}`] : []),
111
- ...(report.icon ? [`Icon: ${report.icon.source === "default" ? "SDK default icon" : report.icon.source === "custom" ? `custom (PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "no PACKAGE_ICON"}`] : []),
112
- ...(report.entry === "admin-only" ? ["Entry: entry: admin-only; only a DSM administrator can open it and call any route (including SDK routes)"] : []),
113
- ...(report.entry === "app-privilege" ? ["Entry: entry: app-privilege; DSM Control Panel > Application Privileges decides who can use it"] : []),
133
+ ...(report.icon
134
+ ? [
135
+ `Icon: ${report.icon.source === "default" ? "SDK default icon" : report.icon.source === "custom" ? `custom (PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "no PACKAGE_ICON"}`,
136
+ ]
137
+ : []),
138
+ ...(report.entry === "admin-only"
139
+ ? [
140
+ "Entry: entry: admin-only; only a DSM administrator can open it and call any route (including SDK routes)",
141
+ ]
142
+ : []),
143
+ ...(report.entry === "app-privilege"
144
+ ? [
145
+ "Entry: entry: app-privilege; DSM Control Panel > Application Privileges decides who can use it",
146
+ ]
147
+ : []),
114
148
  ...formatRootSteps(report),
115
- ...(report.appScripts ? [`App scripts: ${report.appScripts.join(", ")} (scripts/app, run by the generated scripts of the same name as the package account)`] : []),
149
+ ...(report.appScripts
150
+ ? [
151
+ `App scripts: ${report.appScripts.join(", ")} (scripts/app, run by the generated scripts of the same name as the package account)`,
152
+ ]
153
+ : []),
116
154
  ...(report.loginStyle ? [formatLoginStyle(report.loginStyle)] : []),
117
- ...(report.portal ? [`Application Portal: default alias /${report.portal.alias}/ (${report.portal.appId}), active after an administrator applies it`] : []),
155
+ ...(report.portal
156
+ ? [
157
+ `Application Portal: default alias /${report.portal.alias}/ (${report.portal.appId}), active after an administrator applies it`,
158
+ ]
159
+ : []),
118
160
  ...report.capabilities.map((cap) => `Capability ${cap.name}: ${cap.requiresAdminApproval ? "needs DSM administrator approval" : "ready after install"}`),
119
161
  "payload:",
120
- ...(report.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
162
+ ...(report.uninstallCleanup ?? []).map((item) => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
121
163
  ...report.payload.map((e) => ` ${e.mode} ${e.path}`),
122
164
  ...formatSpkChecks(report),
123
165
  ].join("\n");
@@ -131,8 +173,20 @@ async function standaloneBuild(root, arches, options) {
131
173
  ? resolve(options.outDir)
132
174
  : undefined;
133
175
  const result = isNativeLayout(root)
134
- ? await buildNativeLayout({ root, arches, outDir, minDsm: options.minDsm, jobs: options.jobs })
135
- : await buildStandalone({ root, arches: arches, outDir, minDsm: options.minDsm, jobs: options.jobs });
176
+ ? await buildNativeLayout({
177
+ root,
178
+ arches,
179
+ outDir,
180
+ minDsm: options.minDsm,
181
+ jobs: options.jobs,
182
+ })
183
+ : await buildStandalone({
184
+ root,
185
+ arches: arches,
186
+ outDir,
187
+ minDsm: options.minDsm,
188
+ jobs: options.jobs,
189
+ });
136
190
  result.warnings?.forEach((warning) => console.error(warning));
137
191
  return result.outputs.map((output) => {
138
192
  if (!options.output)
@@ -145,26 +199,46 @@ async function standaloneBuild(root, arches, options) {
145
199
  /** What the reproducible check needs to know about the project being built. */
146
200
  function reproducibleTarget(standalone, project, arches) {
147
201
  if (!standalone)
148
- return { root: project.root, manifestPath: project.contractPath, arches: arches ?? [] };
202
+ return {
203
+ root: project.root,
204
+ manifestPath: project.contractPath,
205
+ arches: arches ?? [],
206
+ };
149
207
  const manifestPath = join(standalone, "dsm-package.yaml");
150
208
  if (!isNativeLayout(standalone)) {
151
209
  const loaded = loadStandaloneManifest(standalone);
152
- return { root: standalone, manifestPath, arches: arches ?? (loaded.ok ? loaded.manifest.arches : []) };
210
+ return {
211
+ root: standalone,
212
+ manifestPath,
213
+ arches: arches ?? (loaded.ok ? loaded.manifest.arches : []),
214
+ };
153
215
  }
154
216
  const loaded = loadNativeLayout(standalone);
155
217
  if (!loaded.ok)
156
218
  return { root: standalone, manifestPath, arches: arches ?? [] };
157
219
  // Toolkit pins decide what the native hooks compile against.
158
- const toolkits = createHash("sha256").update(JSON.stringify(loaded.manifest.native.toolkits)).digest("hex");
159
- return { root: standalone, manifestPath, arches: arches ?? loaded.manifest.arches, extraInputs: { "native.toolkits": toolkits } };
220
+ const toolkits = createHash("sha256")
221
+ .update(JSON.stringify(loaded.manifest.native.toolkits))
222
+ .digest("hex");
223
+ return {
224
+ root: standalone,
225
+ manifestPath,
226
+ arches: arches ?? loaded.manifest.arches,
227
+ extraInputs: { "native.toolkits": toolkits },
228
+ };
160
229
  }
161
230
  function reproducibleLines(report) {
162
231
  if (report.skipped)
163
- return [`Reproducible check: the dependency fingerprint ${report.fingerprint.slice(0, 12)} matches the last passing run, skipping.`];
232
+ return [
233
+ `Reproducible check: the dependency fingerprint ${report.fingerprint.slice(0, 12)} matches the last passing run, skipping.`,
234
+ ];
164
235
  return report.results.map((result) => result.identical
165
236
  ? `Reproducible check: ${basename(result.output)} is identical (sha256 ${result.sha256[0].slice(0, 12)})`
166
237
  : `Reproducible check: ${basename(result.output)} differs (sha256 ${result.sha256[0].slice(0, 12)} ≠ ${result.sha256[1].slice(0, 12)})\n` +
167
- [...result.payload.map((file) => ` payload ${file}`), ...result.metadata.map((file) => ` metadata ${file}`)].join("\n"));
238
+ [
239
+ ...result.payload.map((file) => ` payload ${file}`),
240
+ ...result.metadata.map((file) => ` metadata ${file}`),
241
+ ].join("\n"));
168
242
  }
169
243
  const ARCH_REQUIRED = [
170
244
  "NATIVE_ARCH_FAMILY_REQUIRED",
@@ -177,8 +251,16 @@ async function containerBuild(project, arches, options) {
177
251
  throw failure("CONTAINER_ARCH_UNSUPPORTED", `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${unsupported}.`);
178
252
  const output = options.output ? resolve(options.output) : undefined;
179
253
  const build = async (archFamily, outputPath = output) => {
180
- const result = await buildSpk({ project, archFamily, outputPath, minDsm: options.minDsm, gatewayRun: archFamily && runnerFor(undefined, jobs, archFamily) });
181
- if (!result.ok && !(archFamily === undefined && result.errors.every((code) => ARCH_REQUIRED.includes(code))))
254
+ const result = await buildSpk({
255
+ project,
256
+ archFamily,
257
+ outputPath,
258
+ minDsm: options.minDsm,
259
+ gatewayRun: archFamily && runnerFor(undefined, jobs, archFamily),
260
+ });
261
+ if (!result.ok &&
262
+ !(archFamily === undefined &&
263
+ result.errors.every((code) => ARCH_REQUIRED.includes(code))))
182
264
  throw failure(result.errors[0] ?? "BUILD_FAILED", `Build failed: ${result.errors.join(", ")}`);
183
265
  return result;
184
266
  };
@@ -190,9 +272,15 @@ async function containerBuild(project, arches, options) {
190
272
  if (output)
191
273
  throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o requires a single --arch.");
192
274
  const packageId = String(loadContract(project.contractPath).contract.package);
193
- return commitOutputs(options.outDir ? resolve(options.outDir) : project.root, (stage) => forEachArch(arches ?? CONTAINER_ARCH_FAMILIES, jobs, async (arch) => (await build(arch, join(stage, `${packageId}-${arch}.spk`))).outputPath));
275
+ return commitOutputs(options.outDir ? resolve(options.outDir) : project.root, (stage) => forEachArch(arches ?? CONTAINER_ARCH_FAMILIES, jobs, async (arch) => (await build(arch, join(stage, `${packageId}-${arch}.spk`)))
276
+ .outputPath));
194
277
  }
195
278
  export async function runCli(argv) {
279
+ if (argv[2] === "ctl") {
280
+ const { runCtl } = await import("./ctl/cli.js");
281
+ await runCtl(argv.slice(3));
282
+ return;
283
+ }
196
284
  const json = argv.includes("--json");
197
285
  const stdout = console.log.bind(console);
198
286
  const progress = console.log;
@@ -211,6 +299,7 @@ export async function runCli(argv) {
211
299
  throw failure("NAS_CREDENTIALS_MISSING", "NAS credentials were not found.", "Set a synoagentcli profile or DSM_URL/DSM_PASSWORD.");
212
300
  return nas;
213
301
  };
302
+ program.command("ctl").description("Control registered NAS Apps using described AI commands (ctl --help)");
214
303
  program
215
304
  .command("check")
216
305
  .description("Validate the manifest, capability combination and Compose")
@@ -223,26 +312,61 @@ export async function runCli(argv) {
223
312
  throw failure(validation.errors[0].code, formatErrors(validation.errors), validation.errors[0].suggestion);
224
313
  const standalone = standaloneRoot(rootOf(command));
225
314
  if (standalone) {
226
- const loaded = isNativeLayout(standalone) ? loadNativeLayout(standalone) : loadStandaloneManifest(standalone);
315
+ const loaded = isNativeLayout(standalone)
316
+ ? loadNativeLayout(standalone)
317
+ : loadStandaloneManifest(standalone);
227
318
  if (!loaded.ok)
228
319
  throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "Update dsm-package.yaml to schema: 2, fix the declaration and retry.");
229
- const declared = "icon" in loaded.manifest && loaded.manifest.icon ? resolve(standalone, loaded.manifest.icon) : undefined;
230
- const icon = isNativeLayout(standalone) && !declared ? { source: "layout", solid: false } : iconReport(standalone, declared);
320
+ const declared = "icon" in loaded.manifest && loaded.manifest.icon
321
+ ? resolve(standalone, loaded.manifest.icon)
322
+ : undefined;
323
+ const icon = isNativeLayout(standalone) && !declared
324
+ ? { source: "layout", solid: false }
325
+ : iconReport(standalone, declared);
231
326
  const iconText = formatIconReport(icon, "dsm-package.yaml");
232
327
  if (iconText.warning)
233
328
  console.error(iconText.warning);
234
329
  // A portal build carries the login style; a native layout keeps its own ui/config.
235
- const loginStyle = !isNativeLayout(standalone) && "portal" in loaded.manifest && loaded.manifest.portal
236
- ? loginStyleReport(standalone, "loginStyle" in loaded.manifest ? loaded.manifest.loginStyle : undefined) : undefined;
330
+ const loginStyle = !isNativeLayout(standalone) &&
331
+ "portal" in loaded.manifest &&
332
+ loaded.manifest.portal
333
+ ? loginStyleReport(standalone, "loginStyle" in loaded.manifest
334
+ ? loaded.manifest.loginStyle
335
+ : undefined)
336
+ : undefined;
237
337
  emit("check", {
238
338
  package: loaded.manifest.package,
239
- capabilities: "capabilities" in loaded.manifest ? loaded.manifest.capabilities : {},
339
+ capabilities: "capabilities" in loaded.manifest
340
+ ? loaded.manifest.capabilities
341
+ : {},
342
+ ...("compose" in loaded.manifest && loaded.manifest.compose
343
+ ? {
344
+ compose: {
345
+ images: loaded.manifest.compose.plan.images,
346
+ services: loaded.manifest.compose.plan.services,
347
+ mountsBlocked: loaded.manifest.compose.plan.mounts_blocked,
348
+ acceptanceBlockers: loaded.manifest.compose.plan.acceptance_blockers,
349
+ },
350
+ }
351
+ : {}),
240
352
  icon,
241
353
  ...(loginStyle ? { loginStyle } : {}),
242
- }, ["Validation passed.", ...iconText.lines, ...(loginStyle ? [
243
- formatLoginStyle(loginStyle),
244
- "info: set login_style: in dsm-package.yaml (color, wallpaper with wallpaper_thumbnail, title) to brand the Login Portal page.",
245
- ] : [])].join("\n"));
354
+ }, [
355
+ "Validation passed.",
356
+ ...("compose" in loaded.manifest && loaded.manifest.compose
357
+ ? [
358
+ "Containers use the fixed package identity; signed-in File Station ACLs are not inherited.",
359
+ ...loaded.manifest.compose.plan.acceptance_blockers.map((b) => `Acceptance blocker: ${b}`),
360
+ ]
361
+ : []),
362
+ ...iconText.lines,
363
+ ...(loginStyle
364
+ ? [
365
+ formatLoginStyle(loginStyle),
366
+ "info: set login_style: in dsm-package.yaml (color, wallpaper with wallpaper_thumbnail, title) to brand the Login Portal page.",
367
+ ]
368
+ : []),
369
+ ].join("\n"));
246
370
  return;
247
371
  }
248
372
  const project = requireProject(rootOf(command));
@@ -256,7 +380,11 @@ export async function runCli(argv) {
256
380
  const iconText = formatIconReport(icon, "synology-app.yaml");
257
381
  if (iconText.warning)
258
382
  console.error(iconText.warning);
259
- emit("check", { report, warnings: result.warnings ?? [], icon }, [`Validation passed.`, formatInspectReport(report), ...iconText.lines].join("\n"));
383
+ emit("check", { report, warnings: result.warnings ?? [], icon }, [
384
+ `Validation passed.`,
385
+ formatInspectReport(report),
386
+ ...iconText.lines,
387
+ ].join("\n"));
260
388
  });
261
389
  program
262
390
  .command("inspect <spk>")
@@ -390,8 +518,7 @@ export async function runCli(argv) {
390
518
  arch: options.arch,
391
519
  dev: options.dev,
392
520
  });
393
- if (plan.kind !== "container" &&
394
- (options.share || options.runAs))
521
+ if (plan.kind !== "container" && (options.share || options.runAs))
395
522
  throw failure("CLI_OPTION_INVALID", "native-go/binary do not accept --share/--run-as; pass install wizard values with --param.");
396
523
  if (options.dryRun) {
397
524
  emit("deploy", plan, JSON.stringify(plan, null, 2));
@@ -399,7 +526,9 @@ export async function runCli(argv) {
399
526
  }
400
527
  if (plan.kind !== "container") {
401
528
  const host = hostOf(options.host);
402
- const output = await deployStandalone(plan, host, { params: options.param });
529
+ const output = await deployStandalone(plan, host, {
530
+ params: options.param,
531
+ });
403
532
  const status = await standaloneInfo({ root: plan.root, host });
404
533
  emit("deploy", { ...plan, output, status }, `Deployed ${plan.packageId}: ${output}\n${formatProjectInfo(status)}${plan.notes.length ? "\n" + plan.notes.join("\n") : ""}`);
405
534
  return;
@@ -472,7 +601,9 @@ export async function runCli(argv) {
472
601
  if (result.diagnostics)
473
602
  stdout(JSON.stringify(result.diagnostics, null, 2));
474
603
  });
475
- const skill = program.command("skill").description("Bundled Agent Skill Pack");
604
+ const skill = program
605
+ .command("skill")
606
+ .description("Bundled Agent Skill Pack");
476
607
  skill.command("path").action(() => stdout(resolveSkillsRoot()));
477
608
  skill.command("list").action(() => stdout(formatSkillList()));
478
609
  skill.command("show <id>").action((id) => stdout(readSkill(id)));
@@ -482,14 +613,19 @@ export async function runCli(argv) {
482
613
  .option("--json", "Print a single JSON object")
483
614
  .option("-o, --output <path>", "Output SPK; use with a single --arch")
484
615
  .option("--out-dir <dir>", "Output directory; native projects default to dist/")
485
- .option("--arch <families>", `Architectures, comma-separated or repeated: ${NATIVE_ARCH_FAMILIES.join(",")} (armv7 is for native-go and binary, not for Container Manager Apps); native layout also supports armv8`, (value, previous) => [...(previous ?? []), value])
616
+ .option("--arch <families>", `Architectures, comma-separated or repeated: ${NATIVE_ARCH_FAMILIES.join(",")} (armv7 is for native-go and binary, not for Container Manager Apps); native layout also supports armv8`, (value, previous) => [
617
+ ...(previous ?? []),
618
+ value,
619
+ ])
486
620
  .option("--min-dsm <version>", "Minimum DSM version; uses the manifest value when omitted")
487
621
  .option("--jobs <n>", "Number of architectures to build in parallel, default 1 (sequential)", Number)
488
622
  .option("--verify-reproducible [when]", "Build again from a clean state and compare; when: changed (default, skips when the dependency fingerprint is unchanged) or always")
489
623
  .action(async (options, command) => {
490
624
  const root = rootOf(command);
491
625
  const standalone = standaloneRoot(root);
492
- const verify = options.verifyReproducible === true ? "changed" : options.verifyReproducible;
626
+ const verify = options.verifyReproducible === true
627
+ ? "changed"
628
+ : options.verifyReproducible;
493
629
  if (verify !== undefined && verify !== "changed" && verify !== "always")
494
630
  throw failure("CLI_OPTION_INVALID", `--verify-reproducible accepts only changed or always, got ${verify}.`);
495
631
  let arches;
@@ -498,7 +634,10 @@ export async function runCli(argv) {
498
634
  options.arch === undefined
499
635
  ? undefined
500
636
  : standalone && isNativeLayout(standalone)
501
- ? options.arch.join(",").split(",").map((arch) => {
637
+ ? options.arch
638
+ .join(",")
639
+ .split(",")
640
+ .map((arch) => {
502
641
  if (!Object.hasOwn(LAYOUT_ARCHES, arch))
503
642
  throw new Error(`Native layout arch must be one of ${Object.keys(LAYOUT_ARCHES).join(", ")}`);
504
643
  return arch;
@@ -511,18 +650,29 @@ export async function runCli(argv) {
511
650
  const project = standalone ? undefined : requireProject(root);
512
651
  const build = (overrides = {}) => standalone
513
652
  ? standaloneBuild(standalone, arches, { ...options, ...overrides })
514
- : containerBuild(project, arches, { ...options, ...overrides });
653
+ : containerBuild(project, arches, {
654
+ ...options,
655
+ ...overrides,
656
+ });
515
657
  const outputs = await build();
516
- const reproducible = verify === undefined ? undefined : await checkReproducible({
517
- ...reproducibleTarget(standalone, project, arches),
518
- outputs,
519
- mode: verify,
520
- // The second build writes only to its own folder; a single Compose SPK keeps its name there.
521
- rebuild: (outDir) => standalone || outputs.length > 1
522
- ? build({ output: undefined, outDir })
523
- : build({ output: join(outDir, basename(outputs[0])), outDir: undefined }),
524
- });
525
- const lines = [...outputs.map((output) => `Built ${output}`), ...(reproducible ? reproducibleLines(reproducible) : [])];
658
+ const reproducible = verify === undefined
659
+ ? undefined
660
+ : await checkReproducible({
661
+ ...reproducibleTarget(standalone, project, arches),
662
+ outputs,
663
+ mode: verify,
664
+ // The second build writes only to its own folder; a single Compose SPK keeps its name there.
665
+ rebuild: (outDir) => standalone || outputs.length > 1
666
+ ? build({ output: undefined, outDir })
667
+ : build({
668
+ output: join(outDir, basename(outputs[0])),
669
+ outDir: undefined,
670
+ }),
671
+ });
672
+ const lines = [
673
+ ...outputs.map((output) => `Built ${output}`),
674
+ ...(reproducible ? reproducibleLines(reproducible) : []),
675
+ ];
526
676
  if (reproducible && !reproducible.identical)
527
677
  throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "Find the payload files that differ between builds (timestamps, random paths, unpinned dependencies) and retry after fixing them."), { report: { outputs, reproducible } });
528
678
  emit("build", { outputs, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
@@ -553,7 +703,9 @@ export async function runCli(argv) {
553
703
  ok: false,
554
704
  code,
555
705
  message,
556
- hint: detail.hint ?? getErrorCode(code)?.suggestion ?? "Fix the configuration and retry; use --help to see command usage.",
706
+ hint: detail.hint ??
707
+ getErrorCode(code)?.suggestion ??
708
+ "Fix the configuration and retry; use --help to see command usage.",
557
709
  ...(detail.report ? { data: detail.report } : {}),
558
710
  }));
559
711
  }