@synopackageland/cli 0.13.1 → 0.15.0

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 (119) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +44 -1
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -850
  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/app-launch.d.ts +11 -0
  14. package/dist/contract/app-launch.js +18 -0
  15. package/dist/contract/web-launch.d.ts +3 -13
  16. package/dist/contract/web-launch.js +14 -81
  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/error-registry.js +1020 -831
  46. package/dist/info.js +120 -28
  47. package/dist/init.js +188 -64
  48. package/dist/inspect.js +21 -6
  49. package/dist/runtime/executor-digests.json +1 -12
  50. package/dist/skill.js +51 -9
  51. package/dist/standalone/api-description-inspect.d.ts +4 -0
  52. package/dist/standalone/api-description-inspect.js +12 -0
  53. package/dist/standalone/api-description.d.ts +7 -0
  54. package/dist/standalone/api-description.js +73 -0
  55. package/dist/standalone/build.d.ts +3 -0
  56. package/dist/standalone/build.js +140 -33
  57. package/dist/standalone/compose-errors.d.ts +3 -0
  58. package/dist/standalone/compose-errors.js +243 -0
  59. package/dist/standalone/compose.d.ts +97 -10
  60. package/dist/standalone/compose.js +306 -75
  61. package/dist/standalone/daemon.js +111 -29
  62. package/dist/standalone/dsm-window.js +55 -9
  63. package/dist/standalone/identity-gateway.d.ts +9 -2
  64. package/dist/standalone/identity-gateway.js +282 -66
  65. package/dist/standalone/inspect-spk.d.ts +15 -0
  66. package/dist/standalone/inspect-spk.js +714 -128
  67. package/dist/standalone/lifecycle.js +124 -42
  68. package/dist/standalone/manifest.d.ts +10 -1
  69. package/dist/standalone/manifest.js +308 -87
  70. package/dist/standalone/resource.js +6 -2
  71. package/dist/standalone/spk.d.ts +4 -1
  72. package/dist/standalone/spk.js +139 -39
  73. package/dist/template-paths.d.ts +0 -2
  74. package/dist/template-paths.js +0 -5
  75. package/dist/test-command.d.ts +1 -1
  76. package/dist/test-command.js +1 -1
  77. package/dist/validate.d.ts +3 -3
  78. package/dist/validate.js +32 -140
  79. package/package.json +5 -3
  80. package/schema/synology-app.schema.json +1 -1
  81. package/schema/synopkgland-api-v1.schema.json +423 -0
  82. package/skills/SKILL.md +34 -1
  83. package/skills/reference/compose.md +349 -0
  84. package/skills/reference/create.md +26 -4
  85. package/skills/reference/ctl.md +41 -0
  86. package/skills/reference/deploy.md +1 -1
  87. package/skills/reference/files.md +4 -0
  88. package/skills/reference/identity-gateway.md +8 -138
  89. package/skills/reference/portal.md +31 -0
  90. package/skills/reference/ui-vue.md +31 -0
  91. package/skills/reference/wrap.md +10 -30
  92. package/assets/go/daemon/fixture_linux_test.go +0 -15
  93. package/assets/go/daemon/fixture_other_test.go +0 -7
  94. package/assets/go/daemon/go.mod +0 -5
  95. package/assets/go/daemon/main.go +0 -636
  96. package/assets/go/daemon/main_test.go +0 -315
  97. package/assets/go/daemon/network.go +0 -107
  98. package/assets/go/daemon/network_test.go +0 -86
  99. package/assets/go/daemon/process_linux.go +0 -55
  100. package/assets/go/daemon/process_linux_test.go +0 -161
  101. package/assets/go/daemon/process_other.go +0 -14
  102. package/assets/go/daemon/testdata/child.go +0 -44
  103. package/assets/go/gateway/auth.go +0 -101
  104. package/assets/go/gateway/auth_test.go +0 -216
  105. package/assets/go/gateway/cgi.go +0 -133
  106. package/assets/go/gateway/cgi_test.go +0 -156
  107. package/assets/go/gateway/csrf.go +0 -73
  108. package/assets/go/gateway/csrf_test.go +0 -155
  109. package/assets/go/gateway/directory.go +0 -108
  110. package/assets/go/gateway/directory_test.go +0 -202
  111. package/assets/go/gateway/gateway.go +0 -412
  112. package/assets/go/gateway/gateway_test.go +0 -676
  113. package/assets/go/gateway/go.mod +0 -5
  114. package/assets/go/gateway/groups.go +0 -66
  115. package/assets/go/gateway/main.go +0 -45
  116. package/dist/build-spk/gateway.d.ts +0 -31
  117. package/dist/build-spk/gateway.js +0 -60
  118. package/dist/compose/privilege.d.ts +0 -14
  119. package/dist/compose/privilege.js +0 -102
@@ -9,10 +9,14 @@ import { portalResource } from "../portal/package.js";
9
9
  export function renderStandaloneResource(m, portal) {
10
10
  const dataShare = m.sharedFolder && sharedFolderResource(m.package, m.sharedFolder);
11
11
  const resource = {
12
- ...(m.compose ? renderDockerProjectResource(m.package, "compose", false) : {}),
12
+ ...(m.compose && !m.compose.plan.mounts_blocked
13
+ ? renderDockerProjectResource(m.package, "compose", false, m.compose.plan.project_name)
14
+ : {}),
13
15
  ...(dataShare ? { "data-share": dataShare } : {}),
14
16
  ...(m.capabilities.jobs ? { "systemd-user-unit": {} } : {}),
15
17
  ...(portal ? portalResource() : {}),
16
18
  };
17
- return Object.keys(resource).length > 0 ? JSON.stringify(resource) : undefined;
19
+ return Object.keys(resource).length > 0
20
+ ? JSON.stringify(resource)
21
+ : undefined;
18
22
  }
@@ -11,10 +11,11 @@ export interface AssembleOptions {
11
11
  manifest: StandaloneManifest;
12
12
  arch: NativeArchFamily;
13
13
  /** The compiled or prebuilt CGI executable, already checked. */
14
- cgiPath: string;
14
+ cgiPath?: string;
15
15
  outputPath: string;
16
16
  executor?: ExecutorArtifact;
17
17
  daemonControl?: string;
18
+ composeControl?: string;
18
19
  /** The SDK identity gateway, required exactly when the manifest declares identity_gateway. */
19
20
  identityGateway?: string;
20
21
  /** The compiled background worker, required exactly when capabilities.jobs is enabled. */
@@ -23,6 +24,8 @@ export interface AssembleOptions {
23
24
  app?: string;
24
25
  /** The SDK's portal panel text, required exactly when the manifest declares a portal. */
25
26
  portalText?: PortalCatalogs;
27
+ apiRegistry?: Record<string, string>;
28
+ apiDescription?: import("../ctl/descriptor.js").Descriptor;
26
29
  }
27
30
  /**
28
31
  * Packs an unsigned Standalone Package SPK. DSM runs installation/lifecycle
@@ -1,39 +1,41 @@
1
+ import { sdkSource } from "../runtime/sdk-source.js";
2
+ import { resolvedSdkLaunch } from "../contract/app-launch.js";
1
3
  import { metadataInfoFields } from "../package-metadata.js";
2
4
  import { createHash } from "node:crypto";
3
5
  import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
4
6
  import { tmpdir } from "node:os";
5
7
  import { dirname, join } from "node:path";
6
8
  import { infoArchValue } from "../build-spk/dsm-arch.js";
7
- import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons } from "../build-spk/dsm-icons.js";
9
+ import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons, } from "../build-spk/dsm-icons.js";
8
10
  import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
9
11
  import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
10
- import { renderInstallWizardFiles, renderNativeInstallWizard } from "../build-spk/install-wizard.js";
11
- import { writeWizardUiFiles } from "../build-spk/wizard-i18n.js";
12
- import { renderDelegateScopeSteps } from '../build-spk/delegate-scope.js';
12
+ import { renderInstallWizardFiles, renderNativeInstallWizard, } from "../build-spk/install-wizard.js";
13
+ import { writeWizardUiFiles, } from "../build-spk/wizard-i18n.js";
14
+ import { renderDelegateScopeSteps } from "../build-spk/delegate-scope.js";
13
15
  import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
14
16
  import { renderStandaloneScript } from "./lifecycle.js";
15
17
  import { APP_SCRIPTS_DIR, APP_SCRIPTS_SPK_DIR } from "./app-scripts.js";
16
18
  import { codedMessage } from "../errors.js";
17
19
  import { DSM_LANGS, LOCALES } from "./i18n.js";
18
- import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
20
+ import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName, } from "./dsm-window.js";
19
21
  import { writeUiTexts } from "../build-spk/ui-texts.js";
20
22
  import { loginStyleAssets } from "../build-spk/login-style.js";
21
- import { copyStandaloneNotices } from "../build-spk/notices.js";
23
+ import { copyStandaloneNotices, copyNoticeTree } from "../build-spk/notices.js";
22
24
  import { copyAppTree } from "./bridge-app.js";
23
25
  import { isBridgeBackend } from "./bridge-backend.js";
24
26
  import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
25
- import { composeSource } from "./compose.js";
27
+ import { writeComposePayload } from "./compose.js";
26
28
  import { copyDaemonPayload } from "./daemon.js";
27
- import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig } from "./identity-gateway.js";
28
- import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS } from "./background.js";
29
+ import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
30
+ import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
29
31
  import { renderStandaloneResource } from "./resource.js";
30
32
  import { nativePortal } from "./portal.js";
31
33
  import { portalByExecutor } from "../build-spk/dsm-version.js";
32
- import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
33
- import { executorPlan } from "../runtime/executor-build.js";
34
+ import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalMustache, renderPortalWizardStep, } from "../portal/package.js";
35
+ import { executorPlan, } from "../runtime/executor-build.js";
34
36
  import { EXECUTOR_ANCHOR } from "../runtime/executor-inspect.js";
35
- import { renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps } from "./shared-folder.js";
36
- import { stateItemTexts, uninstallExecProblem, uninstallRecord } from "./uninstall.js";
37
+ import { renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, } from "./shared-folder.js";
38
+ import { stateItemTexts, uninstallExecProblem, uninstallRecord, } from "./uninstall.js";
37
39
  const FILE_MODE = 0o644;
38
40
  const EXEC_MODE = 0o755;
39
41
  const MAX_UI_BYTES = 64 * 1024 * 1024;
@@ -62,26 +64,39 @@ export async function assembleStandaloneSpk(options) {
62
64
  const { manifest } = options;
63
65
  const staging = mkdtempSync(join(tmpdir(), "syno-standalone-spk-"));
64
66
  try {
65
- if ((manifest.capabilities.rootOps || manifest.capabilities.delegate || manifest.capabilities.webapi) && !options.executor)
67
+ if ((manifest.capabilities.rootOps ||
68
+ manifest.capabilities.delegate ||
69
+ manifest.capabilities.webapi) &&
70
+ !options.executor)
66
71
  throw new StandaloneBuildError("EXECUTOR_PIN_REQUIRED", "Executor artifact required.");
67
72
  const icons = packageIcons(manifest.icon);
68
73
  const uiRoot = join(staging, "package", "ui");
69
74
  const appDir = join(uiRoot, "app");
70
- copyUiTree(manifest.ui.dir, appDir);
75
+ if (!manifest.compose)
76
+ copyUiTree(manifest.ui.dir, appDir);
77
+ else
78
+ mkdirSync(appDir, { recursive: true });
71
79
  const assetVersion = digestTree(appDir).slice(0, 12);
72
- const login = manifest.portal ? loginStyleAssets(manifest.loginStyle) : undefined;
80
+ const login = manifest.portal
81
+ ? loginStyleAssets(manifest.loginStyle)
82
+ : undefined;
73
83
  writeFile(join(uiRoot, "config"), renderUiConfig(manifest, icons, login?.fields));
74
84
  for (const [path, bytes] of login?.files ?? [])
75
85
  writeFile(join(uiRoot, path), bytes);
76
- writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
86
+ if (!manifest.compose)
87
+ writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
77
88
  writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
78
89
  writeDsmWindowIcons(uiRoot, icons);
79
90
  writeUiTexts(uiRoot, appTitle(manifest));
91
+ if (options.apiDescription)
92
+ writeFile(join(staging, "package", "synopkgland-api.json"), JSON.stringify(options.apiDescription) + "\n");
80
93
  const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
81
94
  mkdirSync(dirname(cgiTarget), { recursive: true });
82
- copyFileSync(options.cgiPath, cgiTarget);
83
- chmodSync(cgiTarget, EXEC_MODE);
84
- if (manifest.daemon || manifest.compose) {
95
+ if (options.cgiPath) {
96
+ copyFileSync(options.cgiPath, cgiTarget);
97
+ chmodSync(cgiTarget, EXEC_MODE);
98
+ }
99
+ if (manifest.daemon || (manifest.compose && manifest.identityGateway)) {
85
100
  if (!options.daemonControl)
86
101
  throw new StandaloneBuildError("STANDALONE_DAEMON_ARTIFACT_REQUIRED", "daemon controller artifact required.");
87
102
  copyDaemonPayload(manifest, options.arch, join(staging, "package", "daemon"), options.daemonControl);
@@ -89,7 +104,11 @@ export async function assembleStandaloneSpk(options) {
89
104
  if (manifest.identityGateway) {
90
105
  if (!options.identityGateway)
91
106
  throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_ARTIFACT_REQUIRED", "identity gateway artifact required.");
92
- for (const path of [GATEWAY_CGI_PATH, `daemon/payload/${GATEWAY_EXEC}`]) {
107
+ for (const path of [
108
+ GATEWAY_CGI_PATH,
109
+ "ui/sso/open.cgi",
110
+ `daemon/payload/${GATEWAY_EXEC}`,
111
+ ]) {
93
112
  const target = join(staging, "package", path);
94
113
  mkdirSync(dirname(target), { recursive: true });
95
114
  copyFileSync(options.identityGateway, target);
@@ -112,10 +131,33 @@ export async function assembleStandaloneSpk(options) {
112
131
  copyAppTree(options.app, join(staging, "package", "app"));
113
132
  }
114
133
  // A daemon is a Go program of the project's backend directory.
115
- const sdk = backend.kind !== 'binary' ? backend : manifest.daemon ? { kind: 'go', dir: join(manifest.root, 'backend') } : undefined;
116
- copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, 'package'), { go: backend.kind !== 'binary' || Boolean(manifest.daemon), rust: backend.kind === 'rust', sdk });
134
+ const sdk = backend.kind !== "binary"
135
+ ? backend
136
+ : manifest.daemon
137
+ ? { kind: "go", dir: join(manifest.root, "backend") }
138
+ : undefined;
139
+ copyStandaloneNotices(manifest.root, manifest.ui.dir, join(staging, "package"), {
140
+ go: backend.kind !== "binary" ||
141
+ Boolean(manifest.daemon || manifest.compose),
142
+ rust: backend.kind === "rust",
143
+ sdk,
144
+ });
145
+ if (manifest.compose) {
146
+ const source = sdkSource(manifest.compose.sdk);
147
+ copyNoticeTree(join(source.goDir, "LICENSE"), join(staging, "package/LICENSES/SDK.txt"));
148
+ copyNoticeTree(join(source.goDir, "compose/THIRD_PARTY_NOTICES.txt"), join(staging, "package/LICENSES/COMPOSE-THIRD_PARTY_NOTICES.txt"));
149
+ const nativeAdmin = join(source.goDir, "dsm/native");
150
+ for (const name of [
151
+ "admin_check.c",
152
+ "apppriv_check.c",
153
+ "build.sh",
154
+ "provenance.json",
155
+ "licenses",
156
+ ])
157
+ copyNoticeTree(join(nativeAdmin, name), join(staging, "package/LICENSES/native-admin", name));
158
+ }
117
159
  if (manifest.daemon)
118
- copyFileSync(new URL('../../assets/licenses/TEMPLATE.txt', import.meta.url), join(staging, 'package', 'LICENSES', 'DAEMON-LICENSE.txt'));
160
+ copyFileSync(new URL("../../assets/licenses/TEMPLATE.txt", import.meta.url), join(staging, "package", "LICENSES", "DAEMON-LICENSE.txt"));
119
161
  const portal = nativePortal(manifest);
120
162
  if (portal) {
121
163
  writeFile(join(staging, "package", PORTAL_JSON_RELPATH), renderPortalJson(portal));
@@ -137,16 +179,48 @@ export async function assembleStandaloneSpk(options) {
137
179
  copyExecutable(op.binary[options.arch].path, join(staging, "package", "root", op.name));
138
180
  }
139
181
  }
140
- const hook = manifest.uninstall && uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
182
+ const hook = manifest.uninstall &&
183
+ uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
141
184
  if (hook)
142
185
  throw new StandaloneBuildError(hook.code, hook.message);
143
- if (manifest.compose)
144
- writeFile(join(staging, "package/compose/docker-compose.yml"), composeSource(manifest.compose));
186
+ if (manifest.compose) {
187
+ const target = join(staging, "package/compose");
188
+ writeComposePayload(manifest.compose, manifest.root, target, identityGatewayDeclaration(manifest.identityGateway));
189
+ if (!options.composeControl)
190
+ throw new StandaloneBuildError("SDK_COMPOSE_BUILD_FAILED", "Compose runtime artifact required.");
191
+ copyFileSync(options.composeControl, join(target, "control"));
192
+ chmodSync(join(target, "control"), EXEC_MODE);
193
+ }
145
194
  const packageTgz = join(staging, "package.tgz");
146
- await createReproducibleTar(join(staging, "package"), packageTgz, { gzip: true });
195
+ await createReproducibleTar(join(staging, "package"), packageTgz, {
196
+ gzip: true,
197
+ });
147
198
  rmSync(join(staging, "package"), { recursive: true, force: true });
148
- const checksum = createHash("md5").update(readFileSync(packageTgz)).digest("hex");
149
- writeFile(join(staging, "sdk-package.json"), JSON.stringify({ schema: 2, backend: manifest.backend.kind, capabilities: Object.keys(manifest.capabilities).sort(), ...(manifest.compose ? { compose: { services: manifest.compose.services } } : {}), ...(manifest.entry ? { entry: manifest.entry } : {}), ...(manifest.uninstall ? { uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs) } : {}) }));
199
+ const checksum = createHash("md5")
200
+ .update(readFileSync(packageTgz))
201
+ .digest("hex");
202
+ writeFile(join(staging, "sdk-package.json"), JSON.stringify({
203
+ schema: 2,
204
+ ...(options.apiDescription ? { ctl: { hash: options.apiDescription.sha256, registry: options.apiRegistry } } : {}),
205
+ backend: manifest.backend.kind,
206
+ launch: resolvedSdkLaunch(manifest),
207
+ capabilities: Object.keys(manifest.capabilities).sort(),
208
+ ...(manifest.compose
209
+ ? {
210
+ compose: {
211
+ sdk: manifest.compose.sdk,
212
+ declaration: manifest.compose.declaration,
213
+ gateway: identityGatewayDeclaration(manifest.identityGateway),
214
+ },
215
+ }
216
+ : {}),
217
+ ...(manifest.entry ? { entry: manifest.entry } : {}),
218
+ ...(manifest.uninstall
219
+ ? {
220
+ uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs),
221
+ }
222
+ : {}),
223
+ }));
150
224
  writeFile(join(staging, "INFO"), renderInfo(manifest, options.arch, checksum, options.executor?.executorDigest));
151
225
  mkdirSync(join(staging, "conf"));
152
226
  writeFile(join(staging, "conf", "privilege"), renderNativePrivilege());
@@ -164,18 +238,37 @@ export async function assembleStandaloneSpk(options) {
164
238
  for (const name of manifest.appScripts ?? [])
165
239
  copyExecutable(join(manifest.root, APP_SCRIPTS_DIR, name), join(staging, APP_SCRIPTS_SPK_DIR, name));
166
240
  const folder = manifest.sharedFolder;
167
- const steps = manifest.capabilities.delegate?.scope === 'install-selected'
168
- ? (t) => [...renderDelegateScopeSteps(t), ...(folder ? sharedFolderInstallSteps(manifest.package, folder)(t) : [])]
169
- : folder ? sharedFolderInstallSteps(manifest.package, folder) : renderNativeInstallWizard;
170
- const install = renderInstallWizardFiles(manifest.package, portal ? (t) => [...steps(t), renderPortalWizardStep(t, portal.alias)] : steps);
241
+ const steps = manifest.capabilities.delegate?.scope === "install-selected"
242
+ ? (t) => [
243
+ ...renderDelegateScopeSteps(t),
244
+ ...(folder
245
+ ? sharedFolderInstallSteps(manifest.package, folder)(t)
246
+ : []),
247
+ ]
248
+ : folder
249
+ ? sharedFolderInstallSteps(manifest.package, folder)
250
+ : renderNativeInstallWizard;
251
+ const install = renderInstallWizardFiles(manifest.package, portal
252
+ ? (t) => [...steps(t), renderPortalWizardStep(t, portal.alias)]
253
+ : steps);
171
254
  writeWizardUiFiles(staging, {
172
255
  ...install.static,
173
256
  ...install.dynamic,
174
- ...(folder ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder) : {}),
257
+ ...(folder
258
+ ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder)
259
+ : {}),
175
260
  ...renderUninstallWizardFiles(manifest.package, "native", {
176
- executor: Boolean(options.executor), portal: Boolean(portal), portalEntry: Boolean(manifest.portal && portalByExecutor(manifest.os_min_ver)),
261
+ executor: Boolean(options.executor),
262
+ portal: Boolean(portal),
263
+ portalEntry: Boolean(manifest.portal && portalByExecutor(manifest.os_min_ver)),
177
264
  rootPurge: Boolean(manifest.uninstall?.purge.root),
178
- ...(manifest.uninstall?.state ? { state: { items: (locale) => stateItemTexts(manifest.uninstall, manifest.i18n?.catalogs, locale) } } : {}),
265
+ ...(manifest.uninstall?.state
266
+ ? {
267
+ state: {
268
+ items: (locale) => stateItemTexts(manifest.uninstall, manifest.i18n?.catalogs, locale),
269
+ },
270
+ }
271
+ : {}),
179
272
  }),
180
273
  });
181
274
  writePackageCenterIcons(staging, icons);
@@ -218,8 +311,15 @@ function renderInfo(manifest, arch, checksum, executorDigest) {
218
311
  // Package Center writes INFO as root; root tasks accept only this executor.
219
312
  if (executorDigest)
220
313
  fields.push([EXECUTOR_ANCHOR, executorDigest]);
221
- if (manifest.compose)
222
- fields.push(["install_dep_packages", "ContainerManager"]);
314
+ if (manifest.compose) {
315
+ // DSM dependency status can fail while the official docker-project worker is available.
316
+ // Let that worker check availability; never start or repair Container Manager here.
317
+ if (manifest.identityGateway)
318
+ fields.push([
319
+ "adminurl",
320
+ `/webman/3rdparty/${manifest.package}/sso/open.cgi`,
321
+ ]);
322
+ }
223
323
  const backend = manifest.backend;
224
324
  if (isBridgeBackend(backend) && backend.node)
225
325
  fields.push(["install_dep_packages", backend.node.package]);
@@ -3,5 +3,3 @@
3
3
  * package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
4
4
  */
5
5
  export declare function templatesRoot(env?: NodeJS.ProcessEnv, from?: string): string;
6
- /** Go sources the CLI builds into every SPK that needs them; they are not scaffolded. */
7
- export declare function goAssetDir(name: "daemon" | "gateway"): string;
@@ -1,6 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  /**
5
4
  * The scaffold templates come from the exactly pinned @synopackageland/templates
6
5
  * package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
@@ -15,7 +14,3 @@ export function templatesRoot(env = process.env, from = import.meta.url) {
15
14
  throw new Error("[TEMPLATES_MISSING] @synopackageland/templates is not installed; reinstall @synopackageland/cli, or set SYNOPKGLAND_TEMPLATES_DIR to a templates checkout.");
16
15
  }
17
16
  }
18
- /** Go sources the CLI builds into every SPK that needs them; they are not scaffolded. */
19
- export function goAssetDir(name) {
20
- return fileURLToPath(new URL(`../assets/go/${name}`, import.meta.url));
21
- }
@@ -10,4 +10,4 @@ export interface TestProjectResult {
10
10
  errors: string[];
11
11
  }
12
12
  export declare function testProject(options: TestProjectOptions): Promise<TestProjectResult>;
13
- export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment } from "./compose/env.js";
13
+ export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment, } from "./compose/env.js";
@@ -23,4 +23,4 @@ export async function testProject(options) {
23
23
  }
24
24
  return { ok: true, composeInvoked: true, errors: [] };
25
25
  }
26
- export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment } from "./compose/env.js";
26
+ export { materializeComposePaths, composeEnvDirsExist, prepareComposeEnvironment, } from "./compose/env.js";
@@ -1,8 +1,8 @@
1
+ import type { ProjectRoot } from "./discovery.js";
1
2
  import type { ValidationResult } from "./types.js";
2
- import { type ProjectRoot } from "./discovery.js";
3
3
  export type AnalyzerProfile = "personal" | "listing";
4
- export declare function validateProject(project: ProjectRoot, options?: {
4
+ /** All shipped Compose and native projects use the schema-2 build path. */
5
+ export declare function validateProject(project: ProjectRoot, _options?: {
5
6
  profile?: AnalyzerProfile;
6
7
  }): ValidationResult;
7
8
  export { findProjectRoot } from "./discovery.js";
8
- export type { ProjectRoot } from "./discovery.js";
package/dist/validate.js CHANGED
@@ -1,149 +1,41 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { contractSpan, makeError } from "./errors.js";
4
3
  import { getErrorCode } from "./error-registry.js";
5
- import { collectInstallParameters, getValuePosition, loadContract } from "./contract/load.js";
6
- import { validateContract } from "./contract/validate.js";
7
- import { findListingUnsupportedInstallTypes } from "./contract/unsupported-types.js";
8
- import { classifyComposeRoute, collectComposeInterpolations, collectVolumeSourceInterpolations, loadCompose, validateAdminPortMapping, validateComposeHostBinds, validateComposeNumericCommandArgs, validateComposeRunAs, validateComposeSecurity, validateGatewayPortLoopback, } from "./compose/analyze.js";
9
- import { usesAuthGateway } from "./contract/web-launch.js";
10
- import { isNativeLayout, loadNativeLayout } from "./standalone/native-layout.js";
11
4
  import { loadStandaloneManifest } from "./standalone/manifest.js";
12
- import { APP_SCRIPTS_DIR, readAppScripts } from "./standalone/app-scripts.js";
5
+ import { isNativeLayout, loadNativeLayout, } from "./standalone/native-layout.js";
13
6
  import { manifestPolicyProblem } from "./runtime/executor-policy-check.js";
14
- import { COMPOSE_FILENAME, CONTRACT_FILENAME, DSM_PACKAGE_FILENAME, } from "./discovery.js";
15
- import { declaredPrivilegeFlags } from "./compose/privilege.js";
16
- export function validateProject(project, options = {}) {
17
- const profile = options.profile ?? "personal";
18
- const errors = [];
19
- const contractFile = CONTRACT_FILENAME;
20
- const composeFile = COMPOSE_FILENAME;
21
- const standalonePath = join(project.root, DSM_PACKAGE_FILENAME);
22
- const hasCompose = existsSync(project.composePath);
23
- const hasStandalone = existsSync(standalonePath);
24
- if (hasCompose && hasStandalone && isNativeLayout(project.root)) {
25
- return { ok: false, errors: [{
26
- code: "PROJECT_RUNTIME_EXCLUSIVE",
27
- span: { file: DSM_PACKAGE_FILENAME, line: 1, column: 1 },
28
- message: "The project has both compose.yaml and dsm-package.yaml, which are mutually exclusive.",
29
- suggestion: "Choose compose.yaml (container App) or dsm-package.yaml (native App) and delete the other file.",
30
- }] };
31
- }
32
- if (hasStandalone) {
33
- const loaded = isNativeLayout(project.root) ? loadNativeLayout(project.root) : loadStandaloneManifest(project.root);
34
- const policy = loaded.ok && "capabilities" in loaded.manifest ? manifestPolicyProblem(loaded.manifest) : undefined;
35
- const problems = loaded.ok ? (policy ? [policy] : []) : loaded.errors;
36
- return !problems.length ? { ok: true, errors: [] } : { ok: false, errors: problems.map(error => ({
37
- ...error, span: { file: DSM_PACKAGE_FILENAME, line: 1, column: 1 },
38
- suggestion: getErrorCode(error.code)?.suggestion ?? "Fix the schema 2 fields and capability declarations in dsm-package.yaml.",
39
- })) };
40
- }
41
- const parsed = loadContract(project.contractPath);
42
- errors.push(...validateContract(parsed, contractFile));
43
- const capabilities = parsed.contract.capabilities;
44
- if (capabilities !== undefined) {
45
- errors.push(makeError("CAPABILITY_UNSUPPORTED", contractSpan(contractFile, 1, 1, "capabilities"), "A compose project does not support capabilities.", "Remove capabilities, or use a dsm-package.yaml native-go project."));
46
- }
47
- readAppScripts(project.root, e => errors.push({ ...e, span: { file: APP_SCRIPTS_DIR, line: 1, column: 1 }, suggestion: getErrorCode(e.code).suggestion }));
48
- if (profile === "listing") {
49
- for (const { name, type, code } of findListingUnsupportedInstallTypes(parsed.contract)) {
50
- const pos = getValuePosition(parsed.doc, ["install", name, "type"]) ??
51
- getValuePosition(parsed.doc, ["install", name]) ??
52
- { line: 1, column: 1 };
53
- errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `install.${name}.type`), `The Install Parameter "${name}" with type: ${type} can only be snapshotted by synopkgland deploy with an administrator session at install time; Package Center and Package Source listing do not support it.`, "Use synopkgland deploy --host <name> so an administrator session takes the Share list snapshot at install time; this type cannot ship through Package Center or Package Source listing."));
54
- }
55
- }
56
- if (!hasCompose) {
57
- errors.push({
58
- code: "COMPOSE_MISSING",
59
- span: { file: composeFile, line: 1, column: 1 },
60
- message: "compose.yaml or dsm-package.yaml was not found.",
61
- suggestion: "Create compose.yaml or a schema 2 dsm-package.yaml in the project root.",
62
- });
63
- return { ok: false, errors };
64
- }
65
- const parsedCompose = loadCompose(project.composePath);
66
- if (!parsedCompose) {
67
- return { ok: false, errors };
68
- }
69
- errors.push(...validateComposeSecurity(parsedCompose.compose, composeFile, {
70
- profile,
71
- declaredPrivilege: declaredPrivilegeFlags(parsed.contract),
72
- }));
73
- if (classifyComposeRoute(parsed.contract, parsedCompose, composeFile).route !== "reject") {
74
- errors.push(...validateComposeNumericCommandArgs(parsedCompose.compose, composeFile));
75
- }
76
- const installParams = collectInstallParameters(parsed.contract);
77
- const sharedFolderParams = new Set(installParams.filter(({ param }) => param.type === "shared_folder").map(({ name }) => name));
78
- const homesParams = new Set(installParams.filter(({ param }) => param.type === "homes").map(({ name }) => name));
79
- const posixAllSharesParams = new Set(installParams
80
- .filter(({ param }) => param.type === "posix_all_shares")
81
- .map(({ name }) => name));
82
- const installParamNames = new Set(installParams.map(({ name }) => name));
83
- errors.push(...validateComposeHostBinds(parsedCompose.compose, composeFile, {
84
- sharedFolderParams,
85
- homesParams,
86
- posixAllSharesParams,
87
- installParamNames,
88
- }));
89
- if (typeof parsed.contract.adminport === "number") {
90
- errors.push(...validateAdminPortMapping(parsed.contract.adminport, parsedCompose.compose, composeFile));
91
- }
92
- if (typeof parsed.contract.adminport === "number" && usesAuthGateway(parsed.contract)) {
93
- errors.push(...validateGatewayPortLoopback(parsed.contract.adminport, parsedCompose.compose, composeFile));
94
- }
95
- const interpolations = collectComposeInterpolations(parsedCompose.compose);
96
- const volumeSourceVars = collectVolumeSourceInterpolations(parsedCompose.compose);
97
- const dsmUserParams = installParams.filter(({ param }) => param.type === "dsm_user");
98
- const hasSharedFolder = sharedFolderParams.size > 0;
99
- if (dsmUserParams.length > 1) {
100
- errors.push({
101
- code: "CONTRACT_DSM_USER_DUPLICATE",
102
- span: { file: contractFile, line: 1, column: 1, path: "install" },
103
- message: "Each App has at most one dsm_user.",
104
- suggestion: "Keep only one Container Run-as parameter.",
105
- });
106
- }
107
- if (hasSharedFolder && dsmUserParams.length === 0) {
108
- errors.push({
109
- code: "DSM_USER_REQUIRED",
110
- span: { file: contractFile, line: 1, column: 1, path: "install" },
111
- message: "With a Shared Folder, declare exactly one dsm_user as the Container Run-as.",
112
- suggestion: "Add install.<NAME>.type: dsm_user and reference ${NAME_UID}:${NAME_GID} in Compose.",
113
- severity: profile === "listing" ? "error" : "warning",
114
- });
115
- }
116
- errors.push(...validateComposeRunAs(parsedCompose.compose, composeFile, {
117
- hasSharedFolder,
118
- hasDsmUser: dsmUserParams.length > 0,
119
- }).map((item) => item.code === "COMPOSE_USER_ROOT_FORBIDDEN"
120
- ? { ...item, severity: (profile === "listing" ? "error" : "warning") }
121
- : item));
122
- for (const { name, param } of installParams) {
123
- const referenced = param.type === "shared_folder" || param.type === "homes" || param.type === "posix_all_shares"
124
- ? volumeSourceVars.has(name)
125
- : interpolations.has(name);
126
- if ((param.type === "string" ||
127
- param.type === "boolean" ||
128
- param.type === "shared_folder" ||
129
- param.type === "homes" ||
130
- param.type === "posix_all_shares") &&
131
- !referenced) {
132
- errors.push({
133
- code: "INSTALL_PARAMETER_UNREFERENCED",
134
- span: {
135
- file: contractFile,
136
- line: 1,
137
- column: 1,
138
- path: `install.${name}`,
7
+ /** All shipped Compose and native projects use the schema-2 build path. */
8
+ export function validateProject(project, _options = {}) {
9
+ const file = "dsm-package.yaml";
10
+ if (!existsSync(join(project.root, file)))
11
+ return {
12
+ ok: false,
13
+ errors: [
14
+ {
15
+ code: "COMPOSE_LEGACY_MANIFEST_REMOVED",
16
+ span: { file: "synology-app.yaml", line: 1, column: 1 },
17
+ message: "The legacy synology-app.yaml Compose route has been removed.",
18
+ suggestion: "Use init -t compose and declare DSM differences in schema-2 dsm-package.yaml; keep upstream Compose unchanged.",
139
19
  },
140
- message: `The Install Parameter "${name}" is not referenced in Compose.`,
141
- suggestion: `Reference this parameter in compose.yaml as \${${name}}.`,
142
- });
20
+ ],
21
+ };
22
+ const loaded = isNativeLayout(project.root)
23
+ ? loadNativeLayout(project.root)
24
+ : loadStandaloneManifest(project.root);
25
+ const policy = loaded.ok && "capabilities" in loaded.manifest
26
+ ? manifestPolicyProblem(loaded.manifest)
27
+ : undefined;
28
+ const problems = loaded.ok ? (policy ? [policy] : []) : loaded.errors;
29
+ return problems.length
30
+ ? {
31
+ ok: false,
32
+ errors: problems.map((error) => ({
33
+ ...error,
34
+ span: { file, line: 1, column: 1 },
35
+ suggestion: getErrorCode(error.code)?.suggestion ??
36
+ "Correct the schema-2 declaration; see the SDK Compose support matrix.",
37
+ })),
143
38
  }
144
- }
145
- const warnings = errors.filter((item) => item.severity === "warning");
146
- const hardErrors = errors.filter((item) => item.severity !== "warning");
147
- return { ok: hardErrors.length === 0, errors: hardErrors, warnings };
39
+ : { ok: true, errors: [] };
148
40
  }
149
41
  export { findProjectRoot } from "./discovery.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,16 +18,18 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "build": "rm -rf dist && tsc",
21
- "test": "npm run build && node --import tsx --test test/**/*.test.ts",
21
+ "test": "npm run build && node --import tsx --test test/*.test.ts test/ctl/*.test.ts",
22
22
  "test:scripts": "node --test scripts/test/gate.test.mjs scripts/test/release.test.mjs scripts/test/pack-docker.test.mjs scripts/test/npm-pack.test.mjs scripts/test/executor-bump.test.mjs scripts/test/smoke-exports.test.mjs scripts/test/executor-digests.test.mjs scripts/test/parallel.test.mjs scripts/test/scaffold-matrix.test.mjs scripts/test/frontend-deps.test.mjs scripts/test/docker-gate.test.mjs scripts/test/gate-plan.test.mjs scripts/test/gates.test.mjs",
23
23
  "gates": "bash scripts/ci/gates.sh",
24
24
  "gates:fast": "bash scripts/ci/gates.sh --fast",
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@synopackageland/templates": "0.13.1",
28
+ "@napi-rs/keyring": "^2.1.1",
29
+ "@synopackageland/templates": "0.15.0",
29
30
  "ajv": "^8.17.1",
30
31
  "commander": "^13.1.0",
32
+ "json-canonicalize": "^3.0.1",
31
33
  "tar": "^7.4.3",
32
34
  "yaml": "^2.7.0"
33
35
  },
@@ -122,7 +122,7 @@
122
122
  "dsm-window",
123
123
  "new-page"
124
124
  ],
125
- "description": "Web App Launch Mode. dsm-window opens a DSM Application Window at the Trusted App Entry; new-page uses Package Center Open. Omit for dsm-window. Headless apps omit this field."
125
+ "description": "Web App Launch Mode. dsm-window opens a DSM Application Window at the Trusted App Entry; new-page opens a new tab from the main menu and Package Center. Omit for new-page. Headless apps omit this field."
126
126
  },
127
127
  "cookies": {
128
128
  "type": "array",