@synopackageland/cli 0.14.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 (114) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +26 -16
  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/web-launch.d.ts +1 -12
  14. package/dist/contract/web-launch.js +10 -74
  15. package/dist/ctl/cli.d.ts +36 -0
  16. package/dist/ctl/cli.js +218 -0
  17. package/dist/ctl/descriptor.d.ts +81 -0
  18. package/dist/ctl/descriptor.js +93 -0
  19. package/dist/ctl/dsm-auth.d.ts +10 -0
  20. package/dist/ctl/dsm-auth.js +30 -0
  21. package/dist/ctl/errors.d.ts +17 -0
  22. package/dist/ctl/errors.js +32 -0
  23. package/dist/ctl/flows.d.ts +11 -0
  24. package/dist/ctl/flows.js +52 -0
  25. package/dist/ctl/input.d.ts +20 -0
  26. package/dist/ctl/input.js +128 -0
  27. package/dist/ctl/json.d.ts +2 -0
  28. package/dist/ctl/json.js +38 -0
  29. package/dist/ctl/redaction.d.ts +15 -0
  30. package/dist/ctl/redaction.js +198 -0
  31. package/dist/ctl/service.d.ts +38 -0
  32. package/dist/ctl/service.js +156 -0
  33. package/dist/ctl/store.d.ts +51 -0
  34. package/dist/ctl/store.js +93 -0
  35. package/dist/ctl/transfers.d.ts +14 -0
  36. package/dist/ctl/transfers.js +74 -0
  37. package/dist/ctl/transport.d.ts +11 -0
  38. package/dist/ctl/transport.js +79 -0
  39. package/dist/deploy/deploy.d.ts +4 -3
  40. package/dist/deploy/deploy.js +5 -210
  41. package/dist/deploy/plan.js +26 -7
  42. package/dist/dev.js +5 -4
  43. package/dist/error-registry.js +1020 -834
  44. package/dist/info.js +120 -28
  45. package/dist/init.js +188 -64
  46. package/dist/inspect.js +21 -6
  47. package/dist/runtime/executor-digests.json +1 -12
  48. package/dist/skill.js +51 -9
  49. package/dist/standalone/api-description-inspect.d.ts +4 -0
  50. package/dist/standalone/api-description-inspect.js +12 -0
  51. package/dist/standalone/api-description.d.ts +7 -0
  52. package/dist/standalone/api-description.js +73 -0
  53. package/dist/standalone/build.d.ts +3 -0
  54. package/dist/standalone/build.js +140 -33
  55. package/dist/standalone/compose-errors.d.ts +3 -0
  56. package/dist/standalone/compose-errors.js +243 -0
  57. package/dist/standalone/compose.d.ts +97 -10
  58. package/dist/standalone/compose.js +306 -75
  59. package/dist/standalone/daemon.js +111 -29
  60. package/dist/standalone/dsm-window.js +39 -8
  61. package/dist/standalone/identity-gateway.d.ts +9 -2
  62. package/dist/standalone/identity-gateway.js +282 -66
  63. package/dist/standalone/inspect-spk.d.ts +12 -0
  64. package/dist/standalone/inspect-spk.js +666 -136
  65. package/dist/standalone/lifecycle.js +124 -42
  66. package/dist/standalone/manifest.d.ts +8 -1
  67. package/dist/standalone/manifest.js +303 -86
  68. package/dist/standalone/resource.js +6 -2
  69. package/dist/standalone/spk.d.ts +4 -1
  70. package/dist/standalone/spk.js +138 -39
  71. package/dist/template-paths.d.ts +0 -2
  72. package/dist/template-paths.js +0 -5
  73. package/dist/test-command.d.ts +1 -1
  74. package/dist/test-command.js +1 -1
  75. package/dist/validate.d.ts +3 -3
  76. package/dist/validate.js +32 -140
  77. package/package.json +5 -3
  78. package/schema/synopkgland-api-v1.schema.json +423 -0
  79. package/skills/SKILL.md +3 -1
  80. package/skills/reference/compose.md +349 -0
  81. package/skills/reference/create.md +8 -17
  82. package/skills/reference/ctl.md +41 -0
  83. package/skills/reference/deploy.md +1 -1
  84. package/skills/reference/files.md +4 -0
  85. package/skills/reference/identity-gateway.md +8 -144
  86. package/skills/reference/wrap.md +10 -39
  87. package/assets/go/daemon/fixture_linux_test.go +0 -15
  88. package/assets/go/daemon/fixture_other_test.go +0 -7
  89. package/assets/go/daemon/go.mod +0 -5
  90. package/assets/go/daemon/main.go +0 -636
  91. package/assets/go/daemon/main_test.go +0 -315
  92. package/assets/go/daemon/network.go +0 -107
  93. package/assets/go/daemon/network_test.go +0 -86
  94. package/assets/go/daemon/process_linux.go +0 -55
  95. package/assets/go/daemon/process_linux_test.go +0 -161
  96. package/assets/go/daemon/process_other.go +0 -14
  97. package/assets/go/daemon/testdata/child.go +0 -44
  98. package/assets/go/gateway/auth.go +0 -101
  99. package/assets/go/gateway/auth_test.go +0 -216
  100. package/assets/go/gateway/cgi.go +0 -133
  101. package/assets/go/gateway/cgi_test.go +0 -156
  102. package/assets/go/gateway/csrf.go +0 -73
  103. package/assets/go/gateway/csrf_test.go +0 -155
  104. package/assets/go/gateway/directory.go +0 -108
  105. package/assets/go/gateway/directory_test.go +0 -202
  106. package/assets/go/gateway/gateway.go +0 -412
  107. package/assets/go/gateway/gateway_test.go +0 -676
  108. package/assets/go/gateway/go.mod +0 -5
  109. package/assets/go/gateway/groups.go +0 -66
  110. package/assets/go/gateway/main.go +0 -45
  111. package/dist/build-spk/gateway.d.ts +0 -31
  112. package/dist/build-spk/gateway.js +0 -60
  113. package/dist/compose/privilege.d.ts +0 -14
  114. 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,3 +1,4 @@
1
+ import { sdkSource } from "../runtime/sdk-source.js";
1
2
  import { resolvedSdkLaunch } from "../contract/app-launch.js";
2
3
  import { metadataInfoFields } from "../package-metadata.js";
3
4
  import { createHash } from "node:crypto";
@@ -5,36 +6,36 @@ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync,
5
6
  import { tmpdir } from "node:os";
6
7
  import { dirname, join } from "node:path";
7
8
  import { infoArchValue } from "../build-spk/dsm-arch.js";
8
- import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons } from "../build-spk/dsm-icons.js";
9
+ import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons, } from "../build-spk/dsm-icons.js";
9
10
  import { renderNativePrivilege } from "../build-spk/native-lifecycle.js";
10
11
  import { renderUninstallWizardFiles } from "../build-spk/uninstall-wizard.js";
11
- import { renderInstallWizardFiles, renderNativeInstallWizard } from "../build-spk/install-wizard.js";
12
- import { writeWizardUiFiles } from "../build-spk/wizard-i18n.js";
13
- 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";
14
15
  import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
15
16
  import { renderStandaloneScript } from "./lifecycle.js";
16
17
  import { APP_SCRIPTS_DIR, APP_SCRIPTS_SPK_DIR } from "./app-scripts.js";
17
18
  import { codedMessage } from "../errors.js";
18
19
  import { DSM_LANGS, LOCALES } from "./i18n.js";
19
- 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";
20
21
  import { writeUiTexts } from "../build-spk/ui-texts.js";
21
22
  import { loginStyleAssets } from "../build-spk/login-style.js";
22
- import { copyStandaloneNotices } from "../build-spk/notices.js";
23
+ import { copyStandaloneNotices, copyNoticeTree } from "../build-spk/notices.js";
23
24
  import { copyAppTree } from "./bridge-app.js";
24
25
  import { isBridgeBackend } from "./bridge-backend.js";
25
26
  import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
26
- import { composeSource } from "./compose.js";
27
+ import { writeComposePayload } from "./compose.js";
27
28
  import { copyDaemonPayload } from "./daemon.js";
28
- import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig } from "./identity-gateway.js";
29
- 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";
30
31
  import { renderStandaloneResource } from "./resource.js";
31
32
  import { nativePortal } from "./portal.js";
32
33
  import { portalByExecutor } from "../build-spk/dsm-version.js";
33
- import { PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
34
- 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";
35
36
  import { EXECUTOR_ANCHOR } from "../runtime/executor-inspect.js";
36
- import { renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps } from "./shared-folder.js";
37
- import { stateItemTexts, uninstallExecProblem, uninstallRecord } from "./uninstall.js";
37
+ import { renderSharedFolderUpgradeWizardFiles, sharedFolderInstallSteps, } from "./shared-folder.js";
38
+ import { stateItemTexts, uninstallExecProblem, uninstallRecord, } from "./uninstall.js";
38
39
  const FILE_MODE = 0o644;
39
40
  const EXEC_MODE = 0o755;
40
41
  const MAX_UI_BYTES = 64 * 1024 * 1024;
@@ -63,26 +64,39 @@ export async function assembleStandaloneSpk(options) {
63
64
  const { manifest } = options;
64
65
  const staging = mkdtempSync(join(tmpdir(), "syno-standalone-spk-"));
65
66
  try {
66
- 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)
67
71
  throw new StandaloneBuildError("EXECUTOR_PIN_REQUIRED", "Executor artifact required.");
68
72
  const icons = packageIcons(manifest.icon);
69
73
  const uiRoot = join(staging, "package", "ui");
70
74
  const appDir = join(uiRoot, "app");
71
- copyUiTree(manifest.ui.dir, appDir);
75
+ if (!manifest.compose)
76
+ copyUiTree(manifest.ui.dir, appDir);
77
+ else
78
+ mkdirSync(appDir, { recursive: true });
72
79
  const assetVersion = digestTree(appDir).slice(0, 12);
73
- const login = manifest.portal ? loginStyleAssets(manifest.loginStyle) : undefined;
80
+ const login = manifest.portal
81
+ ? loginStyleAssets(manifest.loginStyle)
82
+ : undefined;
74
83
  writeFile(join(uiRoot, "config"), renderUiConfig(manifest, icons, login?.fields));
75
84
  for (const [path, bytes] of login?.files ?? [])
76
85
  writeFile(join(uiRoot, path), bytes);
77
- 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));
78
88
  writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
79
89
  writeDsmWindowIcons(uiRoot, icons);
80
90
  writeUiTexts(uiRoot, appTitle(manifest));
91
+ if (options.apiDescription)
92
+ writeFile(join(staging, "package", "synopkgland-api.json"), JSON.stringify(options.apiDescription) + "\n");
81
93
  const cgiTarget = join(staging, "package", CGI_PAYLOAD_PATH);
82
94
  mkdirSync(dirname(cgiTarget), { recursive: true });
83
- copyFileSync(options.cgiPath, cgiTarget);
84
- chmodSync(cgiTarget, EXEC_MODE);
85
- 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)) {
86
100
  if (!options.daemonControl)
87
101
  throw new StandaloneBuildError("STANDALONE_DAEMON_ARTIFACT_REQUIRED", "daemon controller artifact required.");
88
102
  copyDaemonPayload(manifest, options.arch, join(staging, "package", "daemon"), options.daemonControl);
@@ -90,7 +104,11 @@ export async function assembleStandaloneSpk(options) {
90
104
  if (manifest.identityGateway) {
91
105
  if (!options.identityGateway)
92
106
  throw new StandaloneBuildError("STANDALONE_IDENTITY_GATEWAY_ARTIFACT_REQUIRED", "identity gateway artifact required.");
93
- 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
+ ]) {
94
112
  const target = join(staging, "package", path);
95
113
  mkdirSync(dirname(target), { recursive: true });
96
114
  copyFileSync(options.identityGateway, target);
@@ -113,10 +131,33 @@ export async function assembleStandaloneSpk(options) {
113
131
  copyAppTree(options.app, join(staging, "package", "app"));
114
132
  }
115
133
  // A daemon is a Go program of the project's backend directory.
116
- const sdk = backend.kind !== 'binary' ? backend : manifest.daemon ? { kind: 'go', dir: join(manifest.root, 'backend') } : undefined;
117
- 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
+ }
118
159
  if (manifest.daemon)
119
- 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"));
120
161
  const portal = nativePortal(manifest);
121
162
  if (portal) {
122
163
  writeFile(join(staging, "package", PORTAL_JSON_RELPATH), renderPortalJson(portal));
@@ -138,16 +179,48 @@ export async function assembleStandaloneSpk(options) {
138
179
  copyExecutable(op.binary[options.arch].path, join(staging, "package", "root", op.name));
139
180
  }
140
181
  }
141
- const hook = manifest.uninstall && uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
182
+ const hook = manifest.uninstall &&
183
+ uninstallExecProblem(join(staging, "package"), manifest.uninstall.purge);
142
184
  if (hook)
143
185
  throw new StandaloneBuildError(hook.code, hook.message);
144
- if (manifest.compose)
145
- 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
+ }
146
194
  const packageTgz = join(staging, "package.tgz");
147
- await createReproducibleTar(join(staging, "package"), packageTgz, { gzip: true });
195
+ await createReproducibleTar(join(staging, "package"), packageTgz, {
196
+ gzip: true,
197
+ });
148
198
  rmSync(join(staging, "package"), { recursive: true, force: true });
149
- const checksum = createHash("md5").update(readFileSync(packageTgz)).digest("hex");
150
- writeFile(join(staging, "sdk-package.json"), JSON.stringify({ schema: 2, backend: manifest.backend.kind, launch: resolvedSdkLaunch(manifest), 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
+ }));
151
224
  writeFile(join(staging, "INFO"), renderInfo(manifest, options.arch, checksum, options.executor?.executorDigest));
152
225
  mkdirSync(join(staging, "conf"));
153
226
  writeFile(join(staging, "conf", "privilege"), renderNativePrivilege());
@@ -165,18 +238,37 @@ export async function assembleStandaloneSpk(options) {
165
238
  for (const name of manifest.appScripts ?? [])
166
239
  copyExecutable(join(manifest.root, APP_SCRIPTS_DIR, name), join(staging, APP_SCRIPTS_SPK_DIR, name));
167
240
  const folder = manifest.sharedFolder;
168
- const steps = manifest.capabilities.delegate?.scope === 'install-selected'
169
- ? (t) => [...renderDelegateScopeSteps(t), ...(folder ? sharedFolderInstallSteps(manifest.package, folder)(t) : [])]
170
- : folder ? sharedFolderInstallSteps(manifest.package, folder) : renderNativeInstallWizard;
171
- 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);
172
254
  writeWizardUiFiles(staging, {
173
255
  ...install.static,
174
256
  ...install.dynamic,
175
- ...(folder ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder) : {}),
257
+ ...(folder
258
+ ? renderSharedFolderUpgradeWizardFiles(manifest.package, folder)
259
+ : {}),
176
260
  ...renderUninstallWizardFiles(manifest.package, "native", {
177
- 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)),
178
264
  rootPurge: Boolean(manifest.uninstall?.purge.root),
179
- ...(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
+ : {}),
180
272
  }),
181
273
  });
182
274
  writePackageCenterIcons(staging, icons);
@@ -219,8 +311,15 @@ function renderInfo(manifest, arch, checksum, executorDigest) {
219
311
  // Package Center writes INFO as root; root tasks accept only this executor.
220
312
  if (executorDigest)
221
313
  fields.push([EXECUTOR_ANCHOR, executorDigest]);
222
- if (manifest.compose)
223
- 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
+ }
224
323
  const backend = manifest.backend;
225
324
  if (isBridgeBackend(backend) && backend.node)
226
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.14.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.14.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
  },