@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
package/dist/init.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { goChildEnv } from "./runtime/go-env.js";
2
2
  import { spawnSync } from "node:child_process";
3
- import { isMap, isScalar, parse, parseDocument } from "yaml";
3
+ import { parse, parseDocument } from "yaml";
4
4
  import { setTopLevel } from "./yaml-edit.js";
5
- import { capabilityNames, capabilitySettings } from "./capabilities.js";
6
- import { MOVE_FROM_STAGING } from './runtime/delegate-contract.js';
7
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { capabilityNames, capabilitySettings, } from "./capabilities.js";
6
+ import { MOVE_FROM_STAGING } from "./runtime/delegate-contract.js";
7
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
8
8
  import { basename, dirname, join, relative, resolve } from "node:path";
9
9
  import { APP_SCRIPTS_DIR } from "./standalone/app-scripts.js";
10
10
  import { SDK_MODULE, SDK_PACKAGE, sdkSource } from "./runtime/sdk-source.js";
11
11
  import { templatesRoot } from "./template-paths.js";
12
- import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
13
- import { copyNodeSdk, copyPythonSdk, NODE_SDK_PACKAGE } from "./standalone/bridge-app.js";
12
+ import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS, } from "./capability-overlays.js";
13
+ import { copyNodeSdk, copyPythonSdk, NODE_SDK_PACKAGE, } from "./standalone/bridge-app.js";
14
14
  import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
15
- import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
15
+ import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, } from "./build-spk/dsm-version.js";
16
+ import { composeInputs } from "./standalone/compose.js";
16
17
  import { readSharedFolder } from "./standalone/shared-folder.js";
17
18
  const TEMPLATE_NAMES = [
18
19
  "compose",
@@ -26,13 +27,13 @@ const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
26
27
  const isBridgeTemplate = (template) => BRIDGE_TEMPLATES.includes(template);
27
28
  /** Where each native template keeps its API permissions. */
28
29
  const PERMISSIONS_FILE = {
29
- "native-go": `backend/${API_PERMISSIONS_FILE.go}`, "native-python": `backend/app/${API_PERMISSIONS_FILE.python}`,
30
- "native-node": `backend/app/${API_PERMISSIONS_FILE.node}`, "native-rust": `backend/${API_PERMISSIONS_FILE.rust}`,
30
+ "native-go": `backend/${API_PERMISSIONS_FILE.go}`,
31
+ "native-python": `backend/app/${API_PERMISSIONS_FILE.python}`,
32
+ "native-node": `backend/app/${API_PERMISSIONS_FILE.node}`,
33
+ "native-rust": `backend/${API_PERMISSIONS_FILE.rust}`,
31
34
  };
32
35
  /** Templates whose App runs through the DSM docker-project resource worker. */
33
- const CONTAINER_TEMPLATES = [
34
- "compose",
35
- ];
36
+ const CONTAINER_TEMPLATES = ["compose"];
36
37
  /** The container templates need DSM 7.2.1 or later; native-go and binary need 7.1. */
37
38
  export function isContainerTemplate(template) {
38
39
  return CONTAINER_TEMPLATES.includes(template);
@@ -46,26 +47,35 @@ export function listTemplates() {
46
47
  /** New native scaffolds opt in explicitly; existing manifests are never migrated. */
47
48
  export function nativeCapabilitySettings(names) {
48
49
  const settings = capabilitySettings(names);
49
- if (names.includes('delegate'))
50
+ if (names.includes("delegate"))
50
51
  settings.delegate = {
51
- scope: 'install-selected', operations: [MOVE_FROM_STAGING], destinations: ['save'], lifetime: 'until-revoked',
52
+ scope: "install-selected",
53
+ operations: [MOVE_FROM_STAGING],
54
+ destinations: ["save"],
55
+ lifetime: "until-revoked",
52
56
  };
53
57
  return settings;
54
58
  }
55
59
  /** Templates whose identity is filled in at init: every generated file may carry a placeholder. */
56
- const IDENTIFIED = [...NATIVE_TEMPLATES, "binary"];
60
+ const IDENTIFIED = [...NATIVE_TEMPLATES, "binary", "compose"];
57
61
  const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
58
62
  /** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
59
63
  const RENAMED = { gitignore: ".gitignore" };
60
64
  function words(dir) {
61
- return basename(dir).split(/[^A-Za-z0-9]+/).filter(Boolean);
65
+ return basename(dir)
66
+ .split(/[^A-Za-z0-9]+/)
67
+ .filter(Boolean);
62
68
  }
63
69
  function defaultPackageId(dir) {
64
- const id = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
70
+ const id = words(dir)
71
+ .map((w) => w[0].toUpperCase() + w.slice(1))
72
+ .join("");
65
73
  return PACKAGE_ID.test(id) ? id : "MyPackage";
66
74
  }
67
75
  function defaultDisplayName(dir) {
68
- const name = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
76
+ const name = words(dir)
77
+ .map((w) => w[0].toUpperCase() + w.slice(1))
78
+ .join(" ");
69
79
  return name || "My Package";
70
80
  }
71
81
  function templateFiles(root, dir = root) {
@@ -84,7 +94,7 @@ function targetName(file) {
84
94
  }
85
95
  /** Native templates default to the Vue shell; the CLI and the library entry point agree. */
86
96
  function resolvedUi(template, options) {
87
- return isNativeTemplate(template) ? options.ui ?? "vue" : options.ui;
97
+ return isNativeTemplate(template) ? (options.ui ?? "vue") : options.ui;
88
98
  }
89
99
  /**
90
100
  * Copy one template tree into a project. A template directory may carry a local
@@ -92,18 +102,32 @@ function resolvedUi(template, options) {
92
102
  * to a generated project (and would otherwise shadow the project's own install).
93
103
  */
94
104
  export function copyTemplateTree(source, target) {
95
- cpSync(source, target, { recursive: true, filter: (path) => !isTemplateBuildArtifact(path) });
105
+ cpSync(source, target, {
106
+ recursive: true,
107
+ filter: (path) => !isTemplateBuildArtifact(path),
108
+ });
96
109
  }
97
110
  function isTemplateBuildArtifact(path) {
98
111
  const name = basename(path);
99
- return name === "node_modules" || name === "dist" || name === ".go-results.json";
112
+ return (name === "node_modules" || name === "dist" || name === ".go-results.json");
100
113
  }
101
114
  export function initFromTemplate(template, targetDir, options = {}) {
102
115
  if (!isTemplateName(template))
103
116
  throw new Error(`[INIT_TEMPLATE_UNKNOWN] Unknown template: ${template}`);
104
117
  const requested = capabilityNames(options.with ?? []);
105
- const capability = isNativeTemplate(template) ? capabilityOverlays(template, requested) : { overlays: [], borrowed: [], removed: [], permissions: {}, implied: [], methods: [] };
106
- const caps = capabilityNames([...new Set([...capability.implied ?? [], ...requested])]);
118
+ const capability = isNativeTemplate(template)
119
+ ? capabilityOverlays(template, requested)
120
+ : {
121
+ overlays: [],
122
+ borrowed: [],
123
+ removed: [],
124
+ permissions: {},
125
+ implied: [],
126
+ methods: [],
127
+ };
128
+ const caps = capabilityNames([
129
+ ...new Set([...(capability.implied ?? []), ...requested]),
130
+ ]);
107
131
  if (caps.includes("login") && resolvedUi(template, options) === "plain")
108
132
  throw new Error("[CAPABILITY_COMBINATION_INVALID] login needs a standalone sign-in page; plain UI does not support it, use --ui vue.");
109
133
  if (caps.includes("delegate") && resolvedUi(template, options) === "plain")
@@ -112,7 +136,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
112
136
  throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from applies only to compose.");
113
137
  if (caps.length && !isNativeTemplate(template))
114
138
  throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose does not support --with capabilities.");
115
- const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR ? resolve(process.env.SYNOPKGLAND_SDK_DIR) : undefined;
139
+ const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR
140
+ ? resolve(process.env.SYNOPKGLAND_SDK_DIR)
141
+ : undefined;
116
142
  if (localSdk)
117
143
  assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
118
144
  const source = join(templatesRoot(), template);
@@ -126,23 +152,47 @@ export function initFromTemplate(template, targetDir, options = {}) {
126
152
  const substitutions = substitutionsFor(template, targetDir, options);
127
153
  const ui = resolvedUi(template, options);
128
154
  const rootExample = caps.includes("root-ops");
129
- if (rootExample && minDsm !== undefined && !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
155
+ if (rootExample &&
156
+ minDsm !== undefined &&
157
+ !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
130
158
  throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
131
159
  }
132
- const vue = ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : { overlays: [], borrowed: [] };
160
+ const vue = ui === "vue" && isNativeTemplate(template)
161
+ ? VUE_OVERLAYS[template]
162
+ : { overlays: [], borrowed: [] };
133
163
  const base = isNativeTemplate(template) ? BASE_BORROWED[template] : [];
134
164
  // Later borrowed files replace earlier ones, as later overlays do.
135
- const borrowed = borrowedFiles(templatesRoot(), [...base, ...vue.borrowed, ...capability.borrowed]);
136
- const overlays = [...vue.overlays, ...capability.overlays].map(name => join(templatesRoot(), name));
165
+ const borrowed = borrowedFiles(templatesRoot(), [
166
+ ...base,
167
+ ...vue.borrowed,
168
+ ...capability.borrowed,
169
+ ]);
170
+ const overlays = [...vue.overlays, ...capability.overlays].map((name) => join(templatesRoot(), name));
137
171
  // Shared legal resources also live in the CLI package; only the template
138
172
  // terms are an unconditional part of a generated project.
139
173
  const shared = join(templatesRoot(), "_shared");
140
174
  // dsm-scripts/: the DSM lifecycle scripts every project starts with (doing nothing); older templates have none.
141
- const sharedScripts = existsSync(join(shared, APP_SCRIPTS_DIR)) ? readdirSync(join(shared, APP_SCRIPTS_DIR)).sort().map(name => `${APP_SCRIPTS_DIR}/${name}`) : [];
142
- const sharedFiles = ["LICENSES/TEMPLATE.txt", "AGENTS.md", "CLAUDE.md", ...sharedScripts];
143
- const files = [...templateFiles(source), ...sharedFiles, ...overlays.flatMap(root => templateFiles(root)),
144
- ...borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
145
- const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
175
+ const sharedScripts = existsSync(join(shared, APP_SCRIPTS_DIR))
176
+ ? readdirSync(join(shared, APP_SCRIPTS_DIR))
177
+ .sort()
178
+ .map((name) => `${APP_SCRIPTS_DIR}/${name}`)
179
+ : [];
180
+ const sharedFiles = [
181
+ "LICENSES/TEMPLATE.txt",
182
+ "AGENTS.md",
183
+ "CLAUDE.md",
184
+ ...sharedScripts,
185
+ ];
186
+ const files = [
187
+ ...templateFiles(source),
188
+ ...sharedFiles,
189
+ ...overlays.flatMap((root) => templateFiles(root)),
190
+ ...borrowed.map(borrowedTarget),
191
+ ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : []),
192
+ ];
193
+ const collisions = files
194
+ .map(targetName)
195
+ .filter((file) => existsSync(join(targetDir, file)));
146
196
  if (collisions.length > 0) {
147
197
  throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} already has ${collisions.join(", ")}; init does not overwrite existing files. Use an empty directory.`);
148
198
  }
@@ -154,16 +204,19 @@ export function initFromTemplate(template, targetDir, options = {}) {
154
204
  }
155
205
  for (const overlay of overlays)
156
206
  copyTemplateTree(overlay, targetDir);
157
- for (const dir of (ui === "vue" ? ["frontend"] : [])) {
207
+ for (const dir of ui === "vue" ? ["frontend"] : []) {
158
208
  cpSync(join(shared, "third-party-notices.mjs"), join(targetDir, dir, "third-party-notices.mjs"));
159
209
  }
160
210
  for (const path of borrowed) {
161
- mkdirSync(dirname(join(targetDir, borrowedTarget(path))), { recursive: true });
211
+ mkdirSync(dirname(join(targetDir, borrowedTarget(path))), {
212
+ recursive: true,
213
+ });
162
214
  cpSync(join(templatesRoot(), path), join(targetDir, borrowedTarget(path)));
163
215
  }
164
216
  for (const file of capability.removed)
165
217
  rmSync(join(targetDir, file));
166
- if (Object.keys(capability.permissions).length && isNativeTemplate(template)) {
218
+ if (Object.keys(capability.permissions).length &&
219
+ isNativeTemplate(template)) {
167
220
  const path = join(targetDir, PERMISSIONS_FILE[template]);
168
221
  const policy = JSON.parse(readFileSync(path, "utf8"));
169
222
  Object.assign(policy.methods, capability.permissions);
@@ -183,15 +236,18 @@ export function initFromTemplate(template, targetDir, options = {}) {
183
236
  if (capability.methods?.length && template === "native-rust") {
184
237
  // A cross-compiled Rust App cannot describe itself: its manifest lists the methods.
185
238
  const manifest = join(targetDir, "dsm-package.yaml");
186
- writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map(name => `, ${name}`).join("")}]`));
239
+ writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map((name) => `, ${name}`).join("")}]`));
187
240
  }
188
241
  if (minDsm) {
189
- const manifest = join(targetDir, template !== "compose" ? "dsm-package.yaml" : "synology-app.yaml");
242
+ const manifest = join(targetDir, "dsm-package.yaml");
190
243
  writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
191
244
  }
192
245
  if (ui === "vue") {
193
246
  const manifest = join(targetDir, "dsm-package.yaml");
194
- writeFileSync(manifest, readFileSync(manifest, "utf8").replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}").replace(" launch: new-page\n", "").replace("Non-UI-SDK frontends open a full page", "UI SDK frontends open a DSM window"));
247
+ writeFileSync(manifest, readFileSync(manifest, "utf8")
248
+ .replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}")
249
+ .replace(" launch: new-page\n", "")
250
+ .replace("Non-UI-SDK frontends open a full page", "UI SDK frontends open a DSM window"));
195
251
  rmSync(join(targetDir, "ui"), { recursive: true });
196
252
  }
197
253
  if (ui !== "vue")
@@ -199,14 +255,37 @@ export function initFromTemplate(template, targetDir, options = {}) {
199
255
  if (template !== "compose") {
200
256
  // Only the capabilities change: the template's comments document the optional fields.
201
257
  const path = join(targetDir, "dsm-package.yaml");
202
- writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), { capabilities: nativeCapabilitySettings(caps) }));
258
+ writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), {
259
+ capabilities: nativeCapabilitySettings(caps),
260
+ }));
203
261
  }
204
262
  if (template === "compose" && options.from)
205
- importCompose(options.from, targetDir, options);
263
+ importCompose(options.from, targetDir);
206
264
  if (template === "binary") {
207
265
  mkdirSync(join(targetDir, "prebuilt"), { recursive: true });
208
- for (const [arch, goarch] of [["x86_64", "amd64"], ["aarch64", "arm64"]]) {
209
- const result = spawnSync("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", "-s -w -buildid=", "-o", join(targetDir, "prebuilt", `api-${arch}`), "main.go"], { cwd: join(targetDir, "example"), env: goChildEnv({ ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: goarch }), encoding: "utf8" });
266
+ for (const [arch, goarch] of [
267
+ ["x86_64", "amd64"],
268
+ ["aarch64", "arm64"],
269
+ ]) {
270
+ const result = spawnSync("go", [
271
+ "build",
272
+ "-trimpath",
273
+ "-buildvcs=false",
274
+ "-ldflags",
275
+ "-s -w -buildid=",
276
+ "-o",
277
+ join(targetDir, "prebuilt", `api-${arch}`),
278
+ "main.go",
279
+ ], {
280
+ cwd: join(targetDir, "example"),
281
+ env: goChildEnv({
282
+ ...process.env,
283
+ CGO_ENABLED: "0",
284
+ GOOS: "linux",
285
+ GOARCH: goarch,
286
+ }),
287
+ encoding: "utf8",
288
+ });
210
289
  if (result.error || result.status !== 0)
211
290
  throw new Error(`[INIT_BINARY_BUILD_FAILED] Install Go and run init again, or provide your own static CGI binary. ${result.stderr ?? result.error}`);
212
291
  }
@@ -218,15 +297,19 @@ export function initFromTemplate(template, targetDir, options = {}) {
218
297
  }
219
298
  /** Each native template's method tests, as its README runs them; Node.js also compiles the backend/app the SPK ships. */
220
299
  const BACKEND_TESTS = {
221
- "native-go": "go test ./...", "native-python": "python3 -m unittest discover -s tests",
222
- "native-node": "npm ci && npm test && npm run build", "native-rust": "cargo test --locked",
300
+ "native-go": "go test ./...",
301
+ "native-python": "python3 -m unittest discover -s tests",
302
+ "native-node": "npm ci && npm test && npm run build",
303
+ "native-rust": "cargo test --locked",
223
304
  };
224
305
  /** What to run after init in `dir`, for a template built from source; undefined for the others. */
225
306
  export function initNextSteps(template, dir, frontend, localSdk) {
226
307
  if (!isNativeTemplate(template))
227
308
  return undefined;
228
309
  // --install-links packs a local SDK like the released package; a symlink would load the checkout's own node_modules.
229
- const install = frontend ? ` (cd frontend && npm install${localSdk ? " --install-links" : ""})\n` : "";
310
+ const install = frontend
311
+ ? ` (cd frontend && npm install${localSdk ? " --install-links" : ""})\n`
312
+ : "";
230
313
  return `Next steps:\n cd ${dir}\n (cd backend && ${BACKEND_TESTS[template]})\n${install} synopkgland build\nSee README.md.`;
231
314
  }
232
315
  /**
@@ -234,38 +317,74 @@ export function initNextSteps(template, dir, frontend, localSdk) {
234
317
  * directory-derived) identity and keeps only the example install entries the
235
318
  * imported Compose references as `${KEY}` (or `${KEY_UID}`/`${KEY_GID}`).
236
319
  */
237
- function importCompose(from, targetDir, options) {
320
+ function importCompose(from, targetDir) {
238
321
  const imported = readFileSync(from, "utf8");
239
322
  const compose = parse(imported);
240
323
  if (!compose || typeof compose !== "object" || !compose.services)
241
324
  throw new Error("[INIT_COMPOSE_INVALID] --from needs a Compose YAML with services.");
242
- writeFileSync(join(targetDir, "compose.yaml"), imported);
243
- const path = join(targetDir, "synology-app.yaml");
325
+ const sourceRoot = dirname(resolve(from)), file = basename(from);
326
+ const envFile = existsSync(join(sourceRoot, ".env")) ? ".env" : undefined;
327
+ const inputs = composeInputs(sourceRoot, file, envFile);
328
+ for (const input of inputs) {
329
+ const dest = join(targetDir, input.path);
330
+ if (existsSync(dest) && input.path !== "compose.yaml")
331
+ throw new Error(`[INIT_TARGET_NOT_EMPTY] Imported input collides with scaffold: ${input.path}`);
332
+ }
333
+ if (file !== "compose.yaml")
334
+ rmSync(join(targetDir, "compose.yaml"));
335
+ for (const input of inputs) {
336
+ const dest = join(targetDir, input.path);
337
+ mkdirSync(dirname(dest), { recursive: true });
338
+ writeFileSync(dest, Buffer.from(input.content, "base64"), { mode: 0o600 });
339
+ }
340
+ // Import local sidecar input files without changing the upstream YAML. The
341
+ // SDK planner validates the complete include/env closure before any build.
342
+ const path = join(targetDir, "dsm-package.yaml");
244
343
  const doc = parseDocument(readFileSync(path, "utf8"));
245
- doc.set("package", options.packageId ?? defaultPackageId(targetDir));
246
- doc.set("displayname", validDisplayName(options.displayName ?? defaultDisplayName(targetDir)));
247
- const install = doc.get("install");
248
- const referenced = new Set([...imported.matchAll(/\$\{([A-Z][A-Z0-9_]*?)(?:_UID|_GID)?\}/g)].map(match => match[1]));
249
- if (isMap(install))
250
- install.items = install.items.filter(item => referenced.has(String(isScalar(item.key) ? item.key.value : item.key)));
251
- if (!isMap(install) || install.items.length === 0)
252
- doc.delete("install");
344
+ doc.setIn(["compose", "files"], [file]);
345
+ if (envFile)
346
+ doc.setIn(["compose", "env_files"], [envFile]);
347
+ const services = Object.entries(compose.services);
348
+ const candidates = services
349
+ .flatMap(([service, spec]) => (spec.ports ?? []).map((port) => {
350
+ const target = typeof port === "object"
351
+ ? Number(port.target)
352
+ : Number(String(port).split("/")[0].split(":").pop());
353
+ return { service, port: target };
354
+ }))
355
+ .filter((p) => Number.isInteger(p.port) && p.port > 0 && p.port < 65536);
356
+ if (candidates.length === 1)
357
+ doc.setIn(["compose", "web"], {
358
+ ...candidates[0],
359
+ health: { path: "/", status: 200 },
360
+ });
361
+ else {
362
+ doc.deleteIn(["compose", "web"]);
363
+ doc.delete("identity_gateway");
364
+ } // Ambiguous ingress requires an explicit developer decision.
253
365
  writeFileSync(path, String(doc));
254
366
  }
255
367
  function validDisplayName(displayName) {
256
368
  if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
257
- throw new Error("[INIT_DISPLAY_NAME_INVALID] The display name must be single-line and without \" or \\.");
369
+ throw new Error('[INIT_DISPLAY_NAME_INVALID] The display name must be single-line and without " or \\.');
258
370
  }
259
371
  return displayName;
260
372
  }
261
373
  /** The `install.sharedFolder` block, validated by the same reader build-spk uses. */
262
374
  function sharedFolderYaml(options) {
263
- const folder = { mode: options.mode, key: options.key ?? "shared", ...(options.name === undefined ? {} : { name: options.name }), permission: options.permission ?? "rw" };
375
+ const folder = {
376
+ mode: options.mode,
377
+ key: options.key ?? "shared",
378
+ ...(options.name === undefined ? {} : { name: options.name }),
379
+ permission: options.permission ?? "rw",
380
+ };
264
381
  const errors = [];
265
- if (!readSharedFolder({ sharedFolder: folder }, e => errors.push(e.message))) {
382
+ if (!readSharedFolder({ sharedFolder: folder }, (e) => errors.push(e.message))) {
266
383
  throw new Error(`[INIT_SHARED_FOLDER_INVALID] ${errors.join(" ")}`);
267
384
  }
268
- return `\ninstall:\n sharedFolder:\n${Object.entries(folder).map(([k, v]) => ` ${k}: ${v}\n`).join("")}`;
385
+ return `\ninstall:\n sharedFolder:\n${Object.entries(folder)
386
+ .map(([k, v]) => ` ${k}: ${v}\n`)
387
+ .join("")}`;
269
388
  }
270
389
  /**
271
390
  * The App SDK a bridge App's tests import, in backend/vendor: the Python
@@ -276,7 +395,8 @@ function sharedFolderYaml(options) {
276
395
  function vendorAppSdk(template, targetDir) {
277
396
  if (template === "native-rust")
278
397
  return;
279
- const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend.sdk);
398
+ const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend
399
+ .sdk);
280
400
  const sdk = sdkSource(version).goDir;
281
401
  const vendor = join(targetDir, "backend/vendor");
282
402
  if (template === "native-python")
@@ -311,7 +431,7 @@ function useLocalSdk(targetDir, sdkDir) {
311
431
  if (existsSync(sdkPackage)) {
312
432
  const version = JSON.parse(readFileSync(sdkPackage, "utf8")).version;
313
433
  if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version))
314
- throw new Error('[SDK_DIR_NOT_READY] Local SDK package version must be X.Y.Z.');
434
+ throw new Error("[SDK_DIR_NOT_READY] Local SDK package version must be X.Y.Z.");
315
435
  text = text.replace(/(github\.com\/synopackageland\/syno-pkg-land-sdk v)\d+\.\d+\.\d+\b/g, `$1${version}`);
316
436
  }
317
437
  writeFileSync(goMod, `${text}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
@@ -363,5 +483,9 @@ function substitutionsFor(template, targetDir, options) {
363
483
  throw new Error(`[INIT_PACKAGE_INVALID] package "${packageId}" must start with a letter and contain only alphanumerics (2-64 characters), for example PhotoTagger.`);
364
484
  }
365
485
  const displayName = validDisplayName(options.displayName ?? defaultDisplayName(targetDir));
366
- return { __PACKAGE__: packageId, __DISPLAY_NAME__: displayName, __BACKGROUND_ENABLED__: caps.includes("jobs") || caps.includes("delegate") ? "true" : "false" };
486
+ return {
487
+ __PACKAGE__: packageId,
488
+ __DISPLAY_NAME__: displayName,
489
+ __BACKGROUND_ENABLED__: caps.includes("jobs") || caps.includes("delegate") ? "true" : "false",
490
+ };
367
491
  }
package/dist/inspect.js CHANGED
@@ -18,7 +18,14 @@ export function inspectProject(project) {
18
18
  }
19
19
  const parsed = loadCompose(project.composePath);
20
20
  if (!parsed) {
21
- return { services, publishedPorts, dataMounts, risks, webHints, exposedPorts: [] };
21
+ return {
22
+ services,
23
+ publishedPorts,
24
+ dataMounts,
25
+ risks,
26
+ webHints,
27
+ exposedPorts: [],
28
+ };
22
29
  }
23
30
  const compose = parsed.compose;
24
31
  services.push(...listServiceNames(compose));
@@ -29,12 +36,19 @@ export function inspectProject(project) {
29
36
  collectRisks(compose, risks);
30
37
  if (publishedPorts.length > 0) {
31
38
  const primary = publishedPorts[0];
32
- webHints.push(`Detected published port ${primary.port}; consider setting adminport: ${primary.port} in synology-app.yaml.`);
33
- webHints.push("A Web App usually also needs adminprotocol and adminurl.");
34
- webHints.push("Launch Mode: integration.web.launch is dsm-window (DSM window) or new-page (default, new tab).");
39
+ webHints.push(`Detected published port ${primary.port}; declare the web service/target for no-publish ingress in dsm-package.yaml compose.web.`);
40
+ webHints.push("Use the SDK identity gateway for Web ingress and review its access policy.");
41
+ webHints.push("Launch Mode: ui.launch is dsm-window or new-page (Compose default, new tab).");
35
42
  }
36
43
  const exposedPorts = listPortExposures(compose).filter((exposure) => !exposure.loopbackOnly);
37
- return { services, publishedPorts, dataMounts, risks, webHints, exposedPorts };
44
+ return {
45
+ services,
46
+ publishedPorts,
47
+ dataMounts,
48
+ risks,
49
+ webHints,
50
+ exposedPorts,
51
+ };
38
52
  }
39
53
  function collectDataMounts(compose, out) {
40
54
  if (!compose.services) {
@@ -43,7 +57,8 @@ function collectDataMounts(compose, out) {
43
57
  for (const [serviceName, service] of Object.entries(compose.services)) {
44
58
  const volumes = normalizeVolumeEntries(service.volumes);
45
59
  for (const volume of volumes) {
46
- if (volume.includes("SYNOPKG_PKGVAR") || volume.includes("SYNO_APP_SECRETS_DIR")) {
60
+ if (volume.includes("SYNOPKG_PKGVAR") ||
61
+ volume.includes("SYNO_APP_SECRETS_DIR")) {
47
62
  out.push(`${serviceName}: ${volume}`);
48
63
  }
49
64
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "0.14.1": {
2
+ "0.15.0": {
3
3
  "x86_64": "efe409287770f6b70e7f8465e8767eeb2413b9ab8775020cf4dffb1555d7bf1f",
4
4
  "aarch64": "d68c9889ed2391e918b9c278679ea0a01cd04425528c11090700a3b6341fc543",
5
5
  "armv7": "9149a05efb9f2d7d86a400707099b3d657382b1c0dbad6588be8c500e90445cc",
@@ -10,10 +10,10 @@
10
10
  },
11
11
  "goToolchain": "go1.26.9"
12
12
  },
13
- "0.14.0": {
14
- "x86_64": "efe409287770f6b70e7f8465e8767eeb2413b9ab8775020cf4dffb1555d7bf1f",
15
- "aarch64": "d68c9889ed2391e918b9c278679ea0a01cd04425528c11090700a3b6341fc543",
16
- "armv7": "9149a05efb9f2d7d86a400707099b3d657382b1c0dbad6588be8c500e90445cc",
13
+ "0.15.1": {
14
+ "x86_64": "4f47fb43f6e60dbad064afb0bbec96dd009fa046670e2c4b590b128a855cb404",
15
+ "aarch64": "c903b09ad657a838f0c62b6688e65e9094572b22b969770fca92fcc3640a5364",
16
+ "armv7": "8178d90de7ea720530bb9496e50ed118086b0e57382d84675aadc68f482d219f",
17
17
  "lifecycle": {
18
18
  "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
19
19
  "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
package/dist/skill.js CHANGED
@@ -3,17 +3,59 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  const SKILL_CATALOG = [
5
5
  { id: "orchestrator", file: "SKILL.md", when: "Read first" },
6
- { id: "create", file: "reference/create.md", when: "Start from nothing or a one-liner" },
7
- { id: "wrap", file: "reference/wrap.md", when: "Existing Compose, or adding a contract" },
8
- { id: "identity-gateway", file: "reference/identity-gateway.md", when: "Wrap OSS native or container apps with DSM account SSO" },
9
- { id: "identity", file: "reference/identity.md", when: "Needs a DSM sign-in" },
6
+ { id: "ctl", file: "reference/ctl.md", when: "Operate registered NAS Apps or design AI commands" },
7
+ {
8
+ id: "create",
9
+ file: "reference/create.md",
10
+ when: "Start from nothing or a one-liner",
11
+ },
12
+ {
13
+ id: "compose",
14
+ file: "reference/compose.md",
15
+ when: "Upstream Compose with the SDK planner, fixed identity and support boundary",
16
+ },
17
+ {
18
+ id: "wrap",
19
+ file: "reference/wrap.md",
20
+ when: "Existing Compose, or adding a contract",
21
+ },
22
+ {
23
+ id: "identity-gateway",
24
+ file: "reference/identity-gateway.md",
25
+ when: "Wrap OSS native or container apps with the DSM identity gateway",
26
+ },
27
+ {
28
+ id: "identity",
29
+ file: "reference/identity.md",
30
+ when: "Needs a DSM sign-in",
31
+ },
10
32
  { id: "files", file: "reference/files.md", when: "Needs NAS files" },
11
- { id: "portal", file: "reference/portal.md", when: "Needs a DSM Application Portal alias link" },
12
- { id: "ui-vue", file: "reference/ui-vue.md", when: "Write or check a Vue UI" },
13
- { id: "ui-json", file: "reference/ui-json.md", when: "Generate or check a JSON UI" },
14
- { id: "lifecycle", file: "reference/lifecycle.md", when: "Add work to install, upgrade, uninstall, start or stop (dsm-scripts/)" },
33
+ {
34
+ id: "portal",
35
+ file: "reference/portal.md",
36
+ when: "Needs a DSM Application Portal alias link",
37
+ },
38
+ {
39
+ id: "ui-vue",
40
+ file: "reference/ui-vue.md",
41
+ when: "Write or check a Vue UI",
42
+ },
43
+ {
44
+ id: "ui-json",
45
+ file: "reference/ui-json.md",
46
+ when: "Generate or check a JSON UI",
47
+ },
48
+ {
49
+ id: "lifecycle",
50
+ file: "reference/lifecycle.md",
51
+ when: "Add work to install, upgrade, uninstall, start or stop (dsm-scripts/)",
52
+ },
15
53
  { id: "deploy", file: "reference/deploy.md", when: "Deploy to a NAS" },
16
- { id: "diagnose", file: "reference/diagnose.md", when: "check/deploy/Open failed" },
54
+ {
55
+ id: "diagnose",
56
+ file: "reference/diagnose.md",
57
+ when: "check/deploy/Open failed",
58
+ },
17
59
  ];
18
60
  export function resolveSkillsRoot() {
19
61
  const moduleDir = dirname(fileURLToPath(import.meta.url));
@@ -0,0 +1,4 @@
1
+ export declare function inspectApiDescription(raw: string, info: {
2
+ id: string;
3
+ version: string;
4
+ }, expectedHash: string, registry?: Record<string, string>): string[];
@@ -0,0 +1,12 @@
1
+ import { validateDescriptor } from '../ctl/descriptor.js';
2
+ export function inspectApiDescription(raw, info, expectedHash, registry) {
3
+ try {
4
+ const description = validateDescriptor(JSON.parse(raw), info.id, info.version);
5
+ if (registry && Object.entries(description.payload.bindings).some(([method, binding]) => !Object.hasOwn(registry, method) || registry[method] !== binding.permission))
6
+ return ['CTL_COMMAND_BINDING_MISMATCH command method/permission differs from build registry.'];
7
+ return description.sha256 === expectedHash ? [] : ['CTL_DESCRIPTOR_HASH_MISMATCH API description differs from the build record.'];
8
+ }
9
+ catch {
10
+ return ['CTL_DESCRIPTOR_INVALID API description does not match the schema, commands or final INFO.'];
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ import { type Descriptor } from '../ctl/descriptor.js';
2
+ import { type StandaloneManifest } from './manifest.js';
3
+ import type { CommandRunner } from './build.js';
4
+ export declare function buildApiDescription(m: StandaloneManifest, bridgeMethods: string[] | undefined, run: CommandRunner): Promise<{
5
+ description: Descriptor;
6
+ registry: Record<string, string>;
7
+ } | undefined>;