@synopackageland/cli 0.9.6 → 0.10.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 (111) hide show
  1. package/CHANGELOG.md +144 -5
  2. package/README.md +5 -0
  3. package/dist/add.js +7 -8
  4. package/dist/build-spk/dsm-arch.js +1 -1
  5. package/dist/build-spk/dsm-icons.d.ts +8 -1
  6. package/dist/build-spk/dsm-icons.js +26 -12
  7. package/dist/build-spk/dsm-version.d.ts +1 -1
  8. package/dist/build-spk/dsm-version.js +4 -4
  9. package/dist/build-spk/gateway.js +2 -2
  10. package/dist/build-spk/generator.js +1 -1
  11. package/dist/build-spk/png.js +16 -16
  12. package/dist/capabilities.js +1 -1
  13. package/dist/capability-overlays.d.ts +16 -5
  14. package/dist/capability-overlays.js +30 -16
  15. package/dist/cli.js +107 -107
  16. package/dist/compose/analyze.js +23 -23
  17. package/dist/compose/privilege.js +12 -12
  18. package/dist/contract/i18n.js +3 -3
  19. package/dist/contract/icon.js +2 -2
  20. package/dist/contract/posix-all-shares.js +2 -2
  21. package/dist/contract/schema-validate.js +3 -3
  22. package/dist/contract/validate.js +16 -16
  23. package/dist/contract/web-launch.d.ts +2 -1
  24. package/dist/contract/web-launch.js +3 -2
  25. package/dist/deploy/deploy.js +8 -8
  26. package/dist/deploy/homes-values.js +1 -1
  27. package/dist/deploy/identity-shared.js +1 -1
  28. package/dist/deploy/native-install-values.js +1 -1
  29. package/dist/deploy/param-values.js +4 -4
  30. package/dist/deploy/plan.d.ts +1 -1
  31. package/dist/deploy/port-preflight.js +3 -3
  32. package/dist/deploy/run-as-values.js +2 -2
  33. package/dist/deploy/share-permission-preflight.js +6 -6
  34. package/dist/deploy/share-values.js +3 -3
  35. package/dist/error-registry.js +369 -346
  36. package/dist/errors.js +1 -1
  37. package/dist/host/dsm-port-profile.d.ts +1 -1
  38. package/dist/host/dsm-port-profile.js +1 -1
  39. package/dist/info.js +11 -11
  40. package/dist/init.d.ts +2 -0
  41. package/dist/init.js +85 -38
  42. package/dist/inspect.js +5 -5
  43. package/dist/portal/alias.d.ts +1 -1
  44. package/dist/portal/alias.js +4 -4
  45. package/dist/runtime/executor-build.js +12 -1
  46. package/dist/runtime/executor-digests.json +8 -85
  47. package/dist/runtime/executor-inspect.js +11 -4
  48. package/dist/runtime/executor-policy.d.ts +9 -2
  49. package/dist/runtime/executor-policy.js +27 -8
  50. package/dist/runtime/root-programs.d.ts +47 -0
  51. package/dist/runtime/root-programs.js +153 -0
  52. package/dist/runtime/sdk-source.d.ts +5 -4
  53. package/dist/runtime/sdk-source.js +8 -4
  54. package/dist/skill.js +14 -14
  55. package/dist/spk/check-info.js +17 -17
  56. package/dist/spk/check-metadata.js +18 -18
  57. package/dist/spk/check-payload.js +10 -10
  58. package/dist/spk/rules.d.ts +57 -57
  59. package/dist/spk/rules.js +50 -50
  60. package/dist/spk/validate-spk.js +5 -5
  61. package/dist/standalone/api-permissions.js +1 -1
  62. package/dist/standalone/background.d.ts +3 -1
  63. package/dist/standalone/background.js +8 -4
  64. package/dist/standalone/binary.js +7 -7
  65. package/dist/standalone/bridge-app.d.ts +23 -0
  66. package/dist/standalone/bridge-app.js +141 -0
  67. package/dist/standalone/bridge-backend.d.ts +40 -0
  68. package/dist/standalone/bridge-backend.js +95 -0
  69. package/dist/standalone/bridge-build.d.ts +21 -0
  70. package/dist/standalone/bridge-build.js +62 -0
  71. package/dist/standalone/bridge-json.d.ts +16 -0
  72. package/dist/standalone/bridge-json.js +59 -0
  73. package/dist/standalone/bridge-runtime.d.ts +16 -0
  74. package/dist/standalone/bridge-runtime.js +46 -0
  75. package/dist/standalone/bridge-rust.d.ts +16 -0
  76. package/dist/standalone/bridge-rust.js +89 -0
  77. package/dist/standalone/build.js +18 -14
  78. package/dist/standalone/daemon.js +6 -6
  79. package/dist/standalone/dsm-window.d.ts +10 -5
  80. package/dist/standalone/dsm-window.js +64 -16
  81. package/dist/standalone/i18n.js +18 -18
  82. package/dist/standalone/inspect-bridge.d.ts +31 -0
  83. package/dist/standalone/inspect-bridge.js +111 -0
  84. package/dist/standalone/inspect-spk.d.ts +1 -1
  85. package/dist/standalone/inspect-spk.js +77 -69
  86. package/dist/standalone/lifecycle.js +22 -3
  87. package/dist/standalone/manifest.d.ts +17 -9
  88. package/dist/standalone/manifest.js +143 -65
  89. package/dist/standalone/portal-window.js +14 -7
  90. package/dist/standalone/request-timeout.d.ts +9 -0
  91. package/dist/standalone/request-timeout.js +13 -0
  92. package/dist/standalone/root-ops.d.ts +23 -2
  93. package/dist/standalone/root-ops.js +118 -13
  94. package/dist/standalone/sdk-check.js +4 -4
  95. package/dist/standalone/shared-folder.js +7 -7
  96. package/dist/standalone/spk.d.ts +2 -0
  97. package/dist/standalone/spk.js +30 -11
  98. package/dist/standalone/uninstall-hook.js +22 -4
  99. package/dist/standalone/uninstall.js +7 -7
  100. package/dist/standalone/window-size.d.ts +17 -0
  101. package/dist/standalone/window-size.js +31 -0
  102. package/dist/types.d.ts +1 -1
  103. package/dist/validate.js +13 -13
  104. package/package.json +2 -2
  105. package/schema/bridge-manifest.v1.schema.json +86 -0
  106. package/skills/reference/create.md +76 -2
  107. package/skills/reference/ui-json.md +2 -1
  108. package/skills/reference/ui-vue.md +9 -3
  109. package/skills/reference/wrap.md +1 -1
  110. package/dist/standalone/rust-build.d.ts +0 -35
  111. package/dist/standalone/rust-build.js +0 -127
package/dist/errors.js CHANGED
@@ -11,7 +11,7 @@ export function formatError(error) {
11
11
  const location = error.span.path
12
12
  ? `${error.span.file}:${error.span.line}:${error.span.column} (${error.span.path})`
13
13
  : `${error.span.file}:${error.span.line}:${error.span.column}`;
14
- return `[${error.code}] ${location}\n${error.message}\n建議:${error.suggestion}`;
14
+ return `[${error.code}] ${location}\n${error.message}\nSuggestion: ${error.suggestion}`;
15
15
  }
16
16
  /** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
17
17
  export function codedMessage(code, message) {
@@ -1,5 +1,5 @@
1
1
  /** The precision caveat for the administrator-only fallback. */
2
- export declare const DSM_PORT_PROFILE_PRECISION_WARNING = "API \u7248\u672A\u8B80\u53D6 conf/resource\uFF1B\u82E5\u6709 App \u81EA\u8A02 Compose project \u540D\u53EF\u80FD\u8AA4\u5831";
2
+ export declare const DSM_PORT_PROFILE_PRECISION_WARNING = "The API version does not read conf/resource; a custom Compose project name may cause a false positive";
3
3
  export interface DsmPortRange {
4
4
  start: number;
5
5
  end: number;
@@ -2,7 +2,7 @@ import { basename } from "node:path";
2
2
  import { composeProjectName } from "../build-spk/resource-worker.js";
3
3
  import { dsmFetch } from "./dsm-http.js";
4
4
  /** The precision caveat for the administrator-only fallback. */
5
- export const DSM_PORT_PROFILE_PRECISION_WARNING = "API 版未讀取 conf/resource;若有 App 自訂 Compose project 名可能誤報";
5
+ export const DSM_PORT_PROFILE_PRECISION_WARNING = "The API version does not read conf/resource; a custom Compose project name may cause a false positive";
6
6
  /**
7
7
  * parsePortProfileFilename(). In particular, projectName is everything before
8
8
  * the final two dash-separated container segments.
package/dist/info.js CHANGED
@@ -67,9 +67,9 @@ export function classifyRootTasks(expected, inventory) {
67
67
  ? result("needs-admin-approval", "Approve missing or outdated root tasks in the DSM package UI.")
68
68
  : result("ready", "Boot and control tasks match the executor and policy digests. Executor health must be checked in the App window.");
69
69
  }
70
- async function liveRootCapability(m, host) {
70
+ async function liveRootCapability(m, host, arch) {
71
71
  try {
72
- const policyDigest = sha256(executorPolicyText(executorPolicy(m)));
72
+ const policyDigest = sha256(executorPolicyText(executorPolicy(m, arch)));
73
73
  const classified = classifyRootTasks(executorTasks({ package: m.package, policyDigest }, requiredSdk(sdkBackend(m)).executorDir), await host.getRootTaskInventory?.(m.package));
74
74
  if (classified.state !== "ready")
75
75
  return classified;
@@ -101,14 +101,14 @@ async function rootCapability(m, host, version) {
101
101
  return blocked("NAS architecture cannot be matched to the local executor SPK artifact.");
102
102
  const spk = join(m.root, "dist", `${m.package}-${version}-${arch}.spk`);
103
103
  if (!existsSync(spk))
104
- return liveRootCapability(m, host);
104
+ return liveRootCapability(m, host, arch);
105
105
  try {
106
106
  const inspected = await inspectSpk(spk);
107
107
  if (inspected.findings.length ||
108
108
  inspected.package !== m.package ||
109
109
  inspected.version !== version)
110
110
  return blocked("Executor SPK artifact failed inspection or identity checks.");
111
- if (!inspected.executor || JSON.stringify(inspected.executor.policy) !== JSON.stringify(executorPolicy(m)))
111
+ if (!inspected.executor || JSON.stringify(inspected.executor.policy) !== JSON.stringify(executorPolicy(m, arch)))
112
112
  return blocked("Executor policy differs from source; rebuild and deploy before approval.");
113
113
  const inventory = await host.getRootTaskInventory?.(m.package);
114
114
  const classified = classifyRootTasks(inspected.executor.tasks, inventory);
@@ -204,13 +204,13 @@ async function packageState(pkg, host) {
204
204
  }
205
205
  export function formatProjectInfo(info) {
206
206
  return [
207
- `Package Identity:${info.packageIdentity}`,
208
- `開發用 Instance:${info.developmentInstance ? "是" : "否"}`,
209
- `已安裝:${info.installed ? "是" : "否"}`,
210
- info.openUrl ? `Open URL:${info.openUrl}` : "Open URL:(未設定)",
211
- info.readiness ? `Readiness:${info.readiness}` : "",
212
- ...info.capabilities.map((cap) => `能力 ${cap.name}:${cap.state} — ${cap.reason}`),
213
- ...(info.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed):${item.target} — ${item.condition} ${item.action}`),
207
+ `Package Identity: ${info.packageIdentity}`,
208
+ `Development instance: ${info.developmentInstance ? "yes" : "no"}`,
209
+ `Installed: ${info.installed ? "yes" : "no"}`,
210
+ info.openUrl ? `Open URL: ${info.openUrl}` : "Open URL: (not set)",
211
+ info.readiness ? `Readiness: ${info.readiness}` : "",
212
+ ...info.capabilities.map((cap) => `Capability ${cap.name}: ${cap.state} — ${cap.reason}`),
213
+ ...(info.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
214
214
  ]
215
215
  .filter(Boolean)
216
216
  .join("\n");
package/dist/init.d.ts CHANGED
@@ -31,3 +31,5 @@ export interface InitOptions {
31
31
  */
32
32
  export declare function copyTemplateTree(source: string, target: string): void;
33
33
  export declare function initFromTemplate(template: TemplateName, targetDir: string, options?: InitOptions): void;
34
+ /** What to run after init in `dir`, for a template built from source; undefined for the others. */
35
+ export declare function initNextSteps(template: TemplateName, dir: string, frontend: boolean, localSdk: boolean): string | undefined;
package/dist/init.js CHANGED
@@ -5,9 +5,10 @@ import { capabilityNames, capabilitySettings } from "./capabilities.js";
5
5
  import { MOVE_FROM_STAGING } from './runtime/delegate-contract.js';
6
6
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
7
7
  import { basename, dirname, join, relative, resolve } from "node:path";
8
- import { SDK_MODULE, SDK_PACKAGE } from "./runtime/sdk-source.js";
8
+ import { SDK_MODULE, SDK_PACKAGE, sdkSource } from "./runtime/sdk-source.js";
9
9
  import { templatesRoot } from "./template-paths.js";
10
- import { borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
10
+ import { BASE_BORROWED, BRIDGE_TEMPLATES, borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
11
+ import { copyAppTree, copyNodeSdk, NODE_SDK_PACKAGE } from "./standalone/bridge-app.js";
11
12
  import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
12
13
  import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
13
14
  import { readSharedFolder } from "./standalone/shared-folder.js";
@@ -15,12 +16,17 @@ const TEMPLATE_NAMES = [
15
16
  "compose",
16
17
  "binary",
17
18
  "native-go",
18
- "native-rust",
19
+ ...BRIDGE_TEMPLATES,
19
20
  ];
20
- /** Templates built from source with the SDK: capabilities, a Vue or plain UI, a shared folder. */
21
- const NATIVE_TEMPLATES = ["native-go", "native-rust"];
21
+ /** Templates built from source with the SDK: capabilities, a Vue or plain UI. */
22
+ const NATIVE_TEMPLATES = ["native-go", ...BRIDGE_TEMPLATES];
22
23
  const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
23
- const BACKEND_KIND = { "native-go": "go", "native-rust": "rust" };
24
+ const isBridgeTemplate = (template) => BRIDGE_TEMPLATES.includes(template);
25
+ /** Where each native template keeps its API permissions. */
26
+ const PERMISSIONS_FILE = {
27
+ "native-go": `backend/${API_PERMISSIONS_FILE.go}`, "native-python": `backend/app/${API_PERMISSIONS_FILE.python}`,
28
+ "native-node": `backend/app/${API_PERMISSIONS_FILE.node}`, "native-rust": `backend/${API_PERMISSIONS_FILE.rust}`,
29
+ };
24
30
  /** Templates whose App runs through the DSM docker-project resource worker. */
25
31
  const CONTAINER_TEMPLATES = [
26
32
  "compose",
@@ -92,22 +98,24 @@ function isTemplateBuildArtifact(path) {
92
98
  }
93
99
  export function initFromTemplate(template, targetDir, options = {}) {
94
100
  if (!isTemplateName(template))
95
- throw new Error(`[INIT_TEMPLATE_UNKNOWN] 未知模板:${template}`);
96
- const caps = capabilityNames(options.with ?? []);
101
+ throw new Error(`[INIT_TEMPLATE_UNKNOWN] Unknown template: ${template}`);
102
+ const requested = capabilityNames(options.with ?? []);
103
+ const capability = isNativeTemplate(template) ? capabilityOverlays(template, requested) : { overlays: [], borrowed: [], removed: [], permissions: {}, implied: [], methods: [] };
104
+ const caps = capabilityNames([...new Set([...capability.implied ?? [], ...requested])]);
97
105
  if (caps.includes("login") && resolvedUi(template, options) === "plain")
98
- throw new Error("[CAPABILITY_COMBINATION_INVALID] login 需要獨立登入頁;plain UI 不支援,請改用 --ui vue。");
106
+ throw new Error("[CAPABILITY_COMBINATION_INVALID] login needs a standalone sign-in page; plain UI does not support it, use --ui vue.");
99
107
  if (caps.includes("delegate") && resolvedUi(template, options) === "plain")
100
- throw new Error("[CAPABILITY_COMBINATION_INVALID] delegate 的 install-selected scope 只能在 Vue 面板確認;plain UI 不支援,請改用 --ui vue。");
108
+ throw new Error("[CAPABILITY_COMBINATION_INVALID] the delegate install-selected scope can only be confirmed in a Vue panel; plain UI does not support it, use --ui vue.");
101
109
  if (options.from && template !== "compose")
102
- throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from 只適用於 compose。");
110
+ throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from applies only to compose.");
103
111
  if (caps.length && !isNativeTemplate(template))
104
- throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose 不支援 --with 能力。");
112
+ throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose does not support --with capabilities.");
105
113
  const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR ? resolve(process.env.SYNOPKGLAND_SDK_DIR) : undefined;
106
114
  if (localSdk)
107
115
  assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
108
116
  const source = join(templatesRoot(), template);
109
117
  if (!existsSync(source)) {
110
- throw new Error(`找不到模板:${template}`);
118
+ throw new Error(`Template not found: ${template}`);
111
119
  }
112
120
  const minDsm = options.minDsm === undefined ? undefined : resolveMinDsm(options.minDsm);
113
121
  if (minDsm !== undefined && isContainerTemplate(template)) {
@@ -119,9 +127,11 @@ export function initFromTemplate(template, targetDir, options = {}) {
119
127
  if (rootExample && minDsm !== undefined && !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
120
128
  throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
121
129
  }
122
- const capability = isNativeTemplate(template) ? capabilityOverlays(template, caps) : { overlays: [], borrowed: [], removed: [] };
123
- const borrowed = borrowedFiles(templatesRoot(), capability.borrowed);
124
- const overlays = [...(ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : []), ...capability.overlays].map(name => join(templatesRoot(), name));
130
+ const vue = ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : { overlays: [], borrowed: [] };
131
+ const base = isNativeTemplate(template) ? BASE_BORROWED[template] : [];
132
+ // Later borrowed files replace earlier ones, as later overlays do.
133
+ const borrowed = borrowedFiles(templatesRoot(), [...base, ...vue.borrowed, ...capability.borrowed]);
134
+ const overlays = [...vue.overlays, ...capability.overlays].map(name => join(templatesRoot(), name));
125
135
  // Shared legal resources also live in the CLI package; only the template
126
136
  // terms are an unconditional part of a generated project.
127
137
  const shared = join(templatesRoot(), "_shared");
@@ -130,7 +140,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
130
140
  ...borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
131
141
  const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
132
142
  if (collisions.length > 0) {
133
- throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
143
+ throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} already has ${collisions.join(", ")}; init does not overwrite existing files. Use an empty directory.`);
134
144
  }
135
145
  mkdirSync(targetDir, { recursive: true });
136
146
  copyTemplateTree(source, targetDir);
@@ -148,10 +158,10 @@ export function initFromTemplate(template, targetDir, options = {}) {
148
158
  }
149
159
  for (const file of capability.removed)
150
160
  rmSync(join(targetDir, file));
151
- if (caps.includes("delegate") && isNativeTemplate(template)) {
152
- const path = join(targetDir, "backend", API_PERMISSIONS_FILE[BACKEND_KIND[template]]);
161
+ if (Object.keys(capability.permissions).length && isNativeTemplate(template)) {
162
+ const path = join(targetDir, PERMISSIONS_FILE[template]);
153
163
  const policy = JSON.parse(readFileSync(path, "utf8"));
154
- policy.methods["app.status"] = "authenticated";
164
+ Object.assign(policy.methods, capability.permissions);
155
165
  writeFileSync(path, JSON.stringify(policy, null, 2) + "\n");
156
166
  }
157
167
  for (const [from, to] of Object.entries(RENAMED)) {
@@ -162,7 +172,14 @@ export function initFromTemplate(template, targetDir, options = {}) {
162
172
  for (const file of files.map(targetName))
163
173
  substitute(join(targetDir, file), substitutions);
164
174
  if (localSdk && isNativeTemplate(template))
165
- useLocalSdk(targetDir, localSdk, BACKEND_KIND[template]);
175
+ useLocalSdk(targetDir, localSdk);
176
+ if (isBridgeTemplate(template))
177
+ vendorAppSdk(template, targetDir);
178
+ if (capability.methods?.length && template === "native-rust") {
179
+ // A cross-compiled Rust App cannot describe itself: its manifest lists the methods.
180
+ const manifest = join(targetDir, "dsm-package.yaml");
181
+ writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^( {2}methods: \[.*)\]$/m, (_, list) => `${list}${capability.methods.map(name => `, ${name}`).join("")}]`));
182
+ }
166
183
  if (minDsm) {
167
184
  const manifest = join(targetDir, template !== "compose" ? "dsm-package.yaml" : "synology-app.yaml");
168
185
  writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
@@ -186,7 +203,7 @@ export function initFromTemplate(template, targetDir, options = {}) {
186
203
  for (const [arch, goarch] of [["x86_64", "amd64"], ["aarch64", "arm64"]]) {
187
204
  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: { ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: goarch }, encoding: "utf8" });
188
205
  if (result.error || result.status !== 0)
189
- throw new Error(`[INIT_BINARY_BUILD_FAILED] 安裝 Go 並重新 init,或提供自己的靜態 CGI binary。${result.stderr ?? result.error}`);
206
+ throw new Error(`[INIT_BINARY_BUILD_FAILED] Install Go and run init again, or provide your own static CGI binary. ${result.stderr ?? result.error}`);
190
207
  }
191
208
  }
192
209
  if (options.sharedFolder) {
@@ -194,16 +211,29 @@ export function initFromTemplate(template, targetDir, options = {}) {
194
211
  writeFileSync(manifest, readFileSync(manifest, "utf8") + sharedFolderYaml(options.sharedFolder));
195
212
  }
196
213
  }
214
+ /** Each native template's method tests, as its README runs them; Node.js also compiles the backend/app the SPK ships. */
215
+ const BACKEND_TESTS = {
216
+ "native-go": "go test ./...", "native-python": "python3 -m unittest discover -s tests",
217
+ "native-node": "npm ci && npm test && npm run build", "native-rust": "cargo test --locked",
218
+ };
219
+ /** What to run after init in `dir`, for a template built from source; undefined for the others. */
220
+ export function initNextSteps(template, dir, frontend, localSdk) {
221
+ if (!isNativeTemplate(template))
222
+ return undefined;
223
+ // --install-links packs a local SDK like the released package; a symlink would load the checkout's own node_modules.
224
+ const install = frontend ? ` (cd frontend && npm install${localSdk ? " --install-links" : ""})\n` : "";
225
+ return `Next steps:\n cd ${dir}\n (cd backend && ${BACKEND_TESTS[template]})\n${install} synopkgland build\nSee README.md.`;
226
+ }
197
227
  /**
198
228
  * --from replaces the example Compose. The manifest takes the requested (or
199
229
  * directory-derived) identity and keeps only the example install entries the
200
- * imported Compose references as `${KEY}` (or `${KEY_UID}`/`${KEY_GID}`).
230
+ * imported Compose references as `${KEY}` (or `${KEY_UID}`/`${KEY_GID}`).
201
231
  */
202
232
  function importCompose(from, targetDir, options) {
203
233
  const imported = readFileSync(from, "utf8");
204
234
  const compose = parse(imported);
205
235
  if (!compose || typeof compose !== "object" || !compose.services)
206
- throw new Error("[INIT_COMPOSE_INVALID] --from 需要包含 services 的 Compose YAML。");
236
+ throw new Error("[INIT_COMPOSE_INVALID] --from needs a Compose YAML with services.");
207
237
  writeFileSync(join(targetDir, "compose.yaml"), imported);
208
238
  const path = join(targetDir, "synology-app.yaml");
209
239
  const doc = parseDocument(readFileSync(path, "utf8"));
@@ -219,7 +249,7 @@ function importCompose(from, targetDir, options) {
219
249
  }
220
250
  function validDisplayName(displayName) {
221
251
  if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
222
- throw new Error("[INIT_DISPLAY_NAME_INVALID] 顯示名稱必須是單行,且不含 \" 或 \\。");
252
+ throw new Error("[INIT_DISPLAY_NAME_INVALID] The display name must be single-line and without \" or \\.");
223
253
  }
224
254
  return displayName;
225
255
  }
@@ -232,29 +262,46 @@ function sharedFolderYaml(options) {
232
262
  }
233
263
  return `\ninstall:\n sharedFolder:\n${Object.entries(folder).map(([k, v]) => ` ${k}: ${v}\n`).join("")}`;
234
264
  }
265
+ /**
266
+ * The App SDK a bridge App's tests import, in backend/vendor: the Python
267
+ * package, or the Node.js package without what npm would install for its own
268
+ * development. The build ships the release backend.sdk names, from the same
269
+ * source (SYNOPKGLAND_SDK_DIR, or that release's Go module).
270
+ */
271
+ function vendorAppSdk(template, targetDir) {
272
+ if (template === "native-rust")
273
+ return;
274
+ const version = String(parse(readFileSync(join(targetDir, "dsm-package.yaml"), "utf8")).backend.sdk);
275
+ const sdk = sdkSource(version).goDir;
276
+ const vendor = join(targetDir, "backend/vendor");
277
+ if (template === "native-python")
278
+ return copyAppTree(join(sdk, "python/synopkgland_app"), join(vendor, "synopkgland_app"));
279
+ const target = join(vendor, NODE_SDK_PACKAGE);
280
+ copyNodeSdk(join(sdk, "node"), target);
281
+ const { devDependencies: _dev, scripts: _scripts, ...pkg } = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
282
+ writeFileSync(join(target, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
283
+ }
235
284
  /**
236
285
  * A local sdk checkout must hold the Go module, and for a Vue frontend the
237
286
  * built ts package: npm installs a file: package as it is, without its build.
238
287
  */
239
288
  function assertLocalSdkReady(sdkDir, vue) {
240
289
  if (!existsSync(join(sdkDir, "go.mod"))) {
241
- throw new Error(`[SDK_DIR_NOT_READY] SYNOPKGLAND_SDK_DIR=${sdkDir} 沒有 go.mod;請指向 syno-pkg-land-sdk 的 checkout。`);
290
+ throw new Error(`[SDK_DIR_NOT_READY] SYNOPKGLAND_SDK_DIR=${sdkDir} has no go.mod; point it at a syno-pkg-land-sdk checkout.`);
242
291
  }
243
292
  const ts = join(sdkDir, "ts");
244
293
  if (vue && !existsSync(join(ts, "dist"))) {
245
- throw new Error(`[SDK_DIR_NOT_READY] ${ts} 尚未建置;前端安裝本機 SDK 時 npm 不會替它 build。請先執行 (cd ${ts} && npm ci && npm run build),SDK 原始碼改動後也要重新 build。`);
294
+ throw new Error(`[SDK_DIR_NOT_READY] ${ts} is not built; npm does not build a local SDK when the frontend installs it. Run (cd ${ts} && npm ci && npm run build) first, and rebuild after SDK source changes.`);
246
295
  }
247
296
  }
248
297
  /**
249
298
  * SYNOPKGLAND_SDK_DIR: the project builds against a local sdk checkout instead
250
- * of the released version. A Rust backend keeps its Cargo files; at build time
251
- * git reads the SDK repository from the checkout.
299
+ * of the released version.
252
300
  */
253
- function useLocalSdk(targetDir, sdkDir, backend) {
254
- if (backend === "go") {
255
- const goMod = join(targetDir, "backend/go.mod");
301
+ function useLocalSdk(targetDir, sdkDir) {
302
+ const goMod = join(targetDir, "backend/go.mod");
303
+ if (existsSync(goMod))
256
304
  writeFileSync(goMod, `${readFileSync(goMod, "utf8")}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
257
- }
258
305
  const frontend = join(targetDir, "frontend/package.json");
259
306
  if (!existsSync(frontend))
260
307
  return;
@@ -284,14 +331,14 @@ function substitute(path, substitutions) {
284
331
  function substitutionsFor(template, targetDir, options) {
285
332
  const caps = capabilityNames(options.with ?? []);
286
333
  if (options.ui !== undefined && !isNativeTemplate(template))
287
- throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go 與 native-rust。");
334
+ throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui applies only to native-go, native-python, native-node and native-rust.");
288
335
  if (options.sharedFolder) {
289
- if (!isNativeTemplate(template))
290
- throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder 只適用於 -t native-go 與 native-rust。");
336
+ if (template !== "native-go")
337
+ throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder applies only to -t native-go.");
291
338
  sharedFolderYaml(options.sharedFolder);
292
339
  }
293
340
  if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
294
- throw new Error("[INIT_UI_INVALID] --ui 只接受 plain 或 vue。");
341
+ throw new Error("[INIT_UI_INVALID] --ui accepts only plain or vue.");
295
342
  if (!IDENTIFIED.includes(template)) {
296
343
  if (options.from && options.displayName !== undefined)
297
344
  validDisplayName(options.displayName);
@@ -299,7 +346,7 @@ function substitutionsFor(template, targetDir, options) {
299
346
  }
300
347
  const packageId = options.packageId ?? defaultPackageId(targetDir);
301
348
  if (!PACKAGE_ID.test(packageId)) {
302
- throw new Error(`[INIT_PACKAGE_INVALID] package「${packageId}」必須以英文字母開頭,只含英數字(2–64 字),例如 PhotoTagger。`);
349
+ throw new Error(`[INIT_PACKAGE_INVALID] package "${packageId}" must start with a letter and contain only alphanumerics (2-64 characters), for example PhotoTagger.`);
303
350
  }
304
351
  const displayName = validDisplayName(options.displayName ?? defaultDisplayName(targetDir));
305
352
  return { __PACKAGE__: packageId, __DISPLAY_NAME__: displayName, __BACKGROUND_ENABLED__: caps.includes("jobs") || caps.includes("delegate") ? "true" : "false" };
package/dist/inspect.js CHANGED
@@ -11,7 +11,7 @@ export function inspectProject(project) {
11
11
  services,
12
12
  publishedPorts,
13
13
  dataMounts,
14
- risks: ["缺少 compose.yaml"],
14
+ risks: ["compose.yaml is missing"],
15
15
  webHints,
16
16
  exposedPorts: [],
17
17
  };
@@ -29,9 +29,9 @@ export function inspectProject(project) {
29
29
  collectRisks(compose, risks);
30
30
  if (publishedPorts.length > 0) {
31
31
  const primary = publishedPorts[0];
32
- webHints.push(`偵測到 published port ${primary.port},可考慮在 synology-app.yaml 設定 adminport: ${primary.port}。`);
33
- webHints.push("Web App 通常還需要 adminprotocol 與 adminurl。");
34
- webHints.push("Launch Mode:integration.web.launch 為 dsm-window(預設,DSM 視窗)或 new-page(新分頁)。");
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 (default, DSM window) or new-page (new tab).");
35
35
  }
36
36
  const exposedPorts = listPortExposures(compose).filter((exposure) => !exposure.loopbackOnly);
37
37
  return { services, publishedPorts, dataMounts, risks, webHints, exposedPorts };
@@ -104,7 +104,7 @@ export function formatInspectReport(report) {
104
104
  lines.push("Port exposure:");
105
105
  lines.push(report.exposedPorts.length > 0
106
106
  ? report.exposedPorts
107
- .map((e) => `CONTAINER_PORT_EXPOSED ${e.service}: ${e.entry} 綁在所有網路介面,可不經 DSM 驗證直接連線;請改綁 127.0.0.1。`)
107
+ .map((e) => `CONTAINER_PORT_EXPOSED ${e.service}: ${e.entry} binds to every network interface, so it is reachable without DSM verification; bind it to 127.0.0.1 instead.`)
108
108
  .join("\n")
109
109
  : "(none)");
110
110
  lines.push("");
@@ -16,7 +16,7 @@ export declare const RESERVED_PORTAL_ALIASES: readonly string[];
16
16
  * but leaves nginx serving the alias, and only the root executor purge can
17
17
  * reload it. A no-privilege App never runs anything as root.
18
18
  */
19
- export declare const PORTAL_REQUIRES_EXECUTOR = "portal \u9700\u8981 executor \u5C64\u7D1A\uFF08capabilities.rootOps \u6216 delegate\uFF09\uFF1A\u89E3\u9664\u5B89\u88DD\u5F8C DSM \u4E0D\u6703 reload nginx\uFF0C\u767B\u5165\u5165\u53E3\u5225\u540D\u8981\u7531 root executor \u6E05\u9664\u3002\u6C92\u6709 executor \u7684 App \u8ACB\u79FB\u9664 portal\uFF0C\u6539\u7531\u7BA1\u7406\u54E1\u5728 DSM \u624B\u52D5\u8A2D\u5B9A\u53CD\u5411\u4EE3\u7406\u3002";
19
+ export declare const PORTAL_REQUIRES_EXECUTOR = "portal needs the executor level (capabilities.rootOps or delegate): after uninstall DSM does not reload nginx, and a root executor must remove the sign-in alias. An App without an executor should remove portal and let an administrator configure a reverse proxy in DSM.";
20
20
  export interface PortalDeclaration {
21
21
  alias: string;
22
22
  }
@@ -20,7 +20,7 @@ export const RESERVED_PORTAL_ALIASES = [
20
20
  * but leaves nginx serving the alias, and only the root executor purge can
21
21
  * reload it. A no-privilege App never runs anything as root.
22
22
  */
23
- export const PORTAL_REQUIRES_EXECUTOR = "portal 需要 executor 層級(capabilities.rootOps 或 delegate):解除安裝後 DSM 不會 reload nginx,登入入口別名要由 root executor 清除。沒有 executor 的 App 請移除 portal,改由管理員在 DSM 手動設定反向代理。";
23
+ export const PORTAL_REQUIRES_EXECUTOR = "portal needs the executor level (capabilities.rootOps or delegate): after uninstall DSM does not reload nginx, and a root executor must remove the sign-in alias. An App without an executor should remove portal and let an administrator configure a reverse proxy in DSM.";
24
24
  export function isPortalAlias(value) {
25
25
  return typeof value === "string" && PORTAL_ALIAS_PATTERN.test(value) && !RESERVED_PORTAL_ALIASES.includes(value);
26
26
  }
@@ -50,10 +50,10 @@ const ENTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
50
50
  /** Why the executor would refuse this entry; undefined when it accepts it. */
51
51
  export function portalEntryProblem(packageId, entry) {
52
52
  if (packageId.length > 64 || !ENTRY_PACKAGE.test(packageId))
53
- return `套件名稱「${packageId}」不能當作 Application Portal 的檔名:只能用英數字,以 . _ - 分隔,最多 64 字。`;
53
+ return `The package name "${packageId}" cannot be an Application Portal file name: it may contain only alphanumerics separated by . _ -, at most 64 characters.`;
54
54
  if (!ENTRY_ALIAS.test(entry.alias))
55
- return `portal 別名「${entry.alias}」不能當作 Application Portal 的路徑。`;
55
+ return `The portal alias "${entry.alias}" cannot be an Application Portal path.`;
56
56
  if (!entry.application || [...entry.application].length > 64 || /\p{Cc}/u.test(entry.application))
57
- return `displayname「${entry.application}」不能當作 Application Portal 的名稱:1–64 個字,不可含控制字元。`;
57
+ return `The displayname "${entry.application}" cannot be an Application Portal name: 1-64 characters without control characters.`;
58
58
  return undefined;
59
59
  }
@@ -8,6 +8,7 @@ import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js
8
8
  import { executorTasks } from './executor-tasks.js';
9
9
  import { copyModule, verifyExecutorDigests } from './executor-source.js';
10
10
  import { requiredSdk } from './sdk-source.js';
11
+ import { rootProgramsUnsupported } from './root-programs.js';
11
12
  /** CLI-owned build flags; the release digest table is computed the same way, so they must not change within an SDK minor. */
12
13
  const GO_BUILD = ['build', '-trimpath', '-buildvcs=false', '-ldflags', '-s -w -buildid=', '-o'];
13
14
  function failure(error) {
@@ -63,7 +64,17 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
63
64
  catch (error) {
64
65
  throw failure(error);
65
66
  }
66
- const policy = executorPolicy(m), policyText = executorPolicyText(policy);
67
+ let policy;
68
+ try {
69
+ policy = executorPolicy(m, arch);
70
+ }
71
+ catch (error) {
72
+ throw failure(error);
73
+ }
74
+ const unsupported = rootProgramsUnsupported(policy, sdk.executorDir);
75
+ if (unsupported)
76
+ throw new StandaloneBuildError('EXECUTOR_ROOT_PROGRAMS_UNSUPPORTED', unsupported);
77
+ const policyText = executorPolicyText(policy);
67
78
  const pins = { package: m.package, executorDigest, policyDigest: sha256(policyText) };
68
79
  return { ...pins, binary, lifecycleBinary, policy, policyText, tasks: executorTasks(pins, sdk.executorDir), implementation: 'executor', sdk: sdk.version, warnings };
69
80
  }
@@ -1,89 +1,12 @@
1
1
  {
2
- "0.9.0": {
3
- "x86_64": "e009eb882e79e001fa8e6faf43e58525948c132f01f873562353284e5ac53b69",
4
- "aarch64": "f9b84fb9b4a945abf28af6efe1bee0e11067dd0cc5ae26e53f8a949175d519a9",
5
- "armv7": "05b670757c9a6cdea17596769ddd471c0563c044af75a564e1570caac8e37bd2",
6
- "lifecycle": {
7
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
8
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
9
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
10
- },
11
- "goToolchain": "go1.26.9"
12
- },
13
- "0.9.1": {
14
- "x86_64": "5d75af3ce1de5b1d3ef5d89e062be1651a8e8b84edbca9903eb1a0b81d122fb5",
15
- "aarch64": "b0e1f8174bf53eb435616ae0faf39ac929b515056f0b2fad5a6c264a1ff7f6e2",
16
- "armv7": "17db613a4bdb181e0aa0f127822bfc13d7473fa846aadfbac3344b7c17700a84",
17
- "lifecycle": {
18
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
19
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
20
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
21
- },
22
- "goToolchain": "go1.26.9"
23
- },
24
- "0.9.2": {
25
- "x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
26
- "aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
27
- "armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
28
- "lifecycle": {
29
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
30
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
31
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
32
- },
33
- "goToolchain": "go1.26.9"
34
- },
35
- "0.9.3": {
36
- "x86_64": "89c26f18ef05b601963341da6cce43f588bdf2a74039feaf3dfcffdd1295207f",
37
- "aarch64": "6616953ac1721da64a134898ed1bec93d8e5a81fc044f8bc822751df1a871bdd",
38
- "armv7": "d9c62b601454b8806a6eeef18a503385b508a8a3b947afa9b433a092979cc6da",
39
- "lifecycle": {
40
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
41
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
42
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
43
- },
44
- "goToolchain": "go1.26.9"
45
- },
46
- "0.9.4": {
47
- "x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
48
- "aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
49
- "armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
50
- "lifecycle": {
51
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
52
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
53
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
54
- },
55
- "goToolchain": "go1.26.9"
56
- },
57
- "0.9.5": {
58
- "x86_64": "fc3c206b6a8e30b7470cedabf6676c027960311469685eab2fe544fe6cc671ac",
59
- "aarch64": "aa1a8a1b88758ad47ca69d30e9649a24a93b94b5b5fc181ce782035c2c77c497",
60
- "armv7": "0c39cb98b67735fede6bbbe91471695e26707f377cbc50dd860a1d9a7fe06fcf",
61
- "lifecycle": {
62
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
63
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
64
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
65
- },
66
- "goToolchain": "go1.26.9"
67
- },
68
- "0.9.6": {
69
- "x86_64": "4241dfebd2b819c2566eca062ee0021b99e7f86bbe4703f50833874f79b3cf54",
70
- "aarch64": "923514aca8cf6576c6c52c737d53c2c7cc4bfcec40591dcec995005be41add95",
71
- "armv7": "df959648ac8e6201f1cf9b86b57730c09ba1afb7ecc151ba2f530ad15dd90ccf",
72
- "lifecycle": {
73
- "x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
74
- "aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
75
- "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
76
- },
77
- "goToolchain": "go1.26.9"
78
- },
79
- "0.9.7": {
80
- "x86_64": "d029ca6129e76cdc5b7016378722af166b511fb346fd1002082e611bec0aef4d",
81
- "aarch64": "ac8018a9846a8aaca6f06323f033a27f7abef6705601efde57ea02565105a48d",
82
- "armv7": "f7b4ee5fb90c8f2dd2c56ec1eeb7749300ad117a23d9df6da6840d8b2eff83b8",
83
- "lifecycle": {
84
- "x86_64": "f2f0a6aa83399688441b111adee3958d7df50e9d8e33939e3951da393f013819",
85
- "aarch64": "d0f0d42f367093d5c9318afb4719b9ac76b1019574f1db34dd0fcb3aba44945b",
86
- "armv7": "02da8b7016abfe00b8f18e27ef2d69bcff1ca376b43e17b8c04983ca6a6ef37d"
2
+ "0.10.0": {
3
+ "x86_64": "fb2df6e3421361c73a1cd720ec12198e968f0d2a58b18b53151c7781c7f4c1ef",
4
+ "aarch64": "0a0f6d013c0174f3ea54585182ddda14c94064f331189ac4315de813099efcdd",
5
+ "armv7": "3fbb5bc74a03a9767ac06a389a10ff07a353c4c56be6fda7b7555a6759992aba",
6
+ "lifecycle": {
7
+ "x86_64": "d65500c4f04713a32b45d360059281391acfa5e00ee24895fdd2de7b82735372",
8
+ "aarch64": "94f6cd119186ff482b8aea56541dd1f9d53c147566bc1efdca6f5a1c86198331",
9
+ "armv7": "cafc97c89f5645c14a8cf31d6d9f258394ed6f2d086478a7aebd04d57c274b4e"
87
10
  },
88
11
  "goToolchain": "go1.26.9"
89
12
  }
@@ -22,10 +22,17 @@ export function inspectExecutor(dir, pkg, arch) {
22
22
  const policy = JSON.parse(text);
23
23
  if (parseDocument(text, { uniqueKeys: true }).errors.length || !validExecutorPolicy(policy, pkg))
24
24
  throw new Error('Invalid executor policy.');
25
- for (const [name, digest] of Object.entries(policy.root)) {
26
- const path = `root/${name}.sh`;
27
- if (!/^[a-z][a-z0-9_]{0,31}$/.test(name) || !existsSync(join(dir, path)) || !lstatSync(join(dir, path)).isFile() || sha256(readFileSync(join(dir, path))) !== digest)
28
- throw new Error('Root script digest mismatch.');
25
+ const machine = ELF_MACHINE[familyOfArchToken(arch.split(' ')[0]) ?? 'x86_64'];
26
+ for (const [name, entry] of Object.entries(policy.root)) {
27
+ const program = typeof entry !== 'string' && entry.kind === 'binary' ? 'binary' : 'script';
28
+ const path = join(dir, program === 'binary' ? `root/${name}` : `root/${name}.sh`);
29
+ if (!existsSync(path) || !lstatSync(path).isFile())
30
+ throw new Error(`Root program ${name} is missing.`);
31
+ const bytes = readFileSync(path);
32
+ if (sha256(bytes) !== (typeof entry === 'string' ? entry : entry.digest))
33
+ throw new Error(`Root program ${name} digest mismatch.`);
34
+ if (program === 'binary' && (!(lstatSync(path).mode & 0o100) || !bytes.subarray(0, 4).equals(Buffer.from([127, 69, 76, 70])) || bytes.readUInt16LE(18) !== machine))
35
+ throw new Error(`Root program ${name} must be an executable for ${arch}.`);
29
36
  }
30
37
  const executorDigest = sha256(binary), policyDigest = sha256(text);
31
38
  if (typeof plan.sdk !== 'string' || !/^\d+\.\d+\.\d+$/.test(plan.sdk))
@@ -1,6 +1,8 @@
1
1
  import type { RootOp } from '../standalone/root-ops.js';
2
2
  import type { EntryMode } from '../standalone/manifest.js';
3
3
  import { type PortalDeclaration, type PortalEntry } from '../portal/alias.js';
4
+ import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
5
+ import { type RootPolicyEntry, type RootSettings } from './root-programs.js';
4
6
  export declare const FILE_OPERATIONS: readonly ["files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write", "files.read", "files.rename", "files.copy", "files.move", "files.delete"];
5
7
  /** Started File Station tasks are followed with these; they are implied, never declared. */
6
8
  export declare const FILE_TASK_OPERATIONS: readonly ["files.taskStatus", "files.taskCancel"];
@@ -28,7 +30,11 @@ export interface ExecutorPolicy {
28
30
  portal?: PortalEntry;
29
31
  /** The root op the executor runs at uninstall while the approval is live; a key of root. */
30
32
  purge?: string;
31
- root: Record<string, string>;
33
+ /** The root entry the executor runs when the package stops or upgrades. */
34
+ stop?: string;
35
+ root: Record<string, RootPolicyEntry>;
36
+ /** Root programs get a directory only root can read, kept across upgrades. */
37
+ root_state?: true;
32
38
  }
33
39
  /** The exact policy file bytes whose sha256 is the policy pin. */
34
40
  export declare const executorPolicyText: (policy: ExecutorPolicy) => string;
@@ -41,6 +47,7 @@ export declare function executorPolicy(m: {
41
47
  rootOps?: true;
42
48
  };
43
49
  rootOps?: RootOp[];
50
+ rootPrograms?: RootSettings;
44
51
  portal?: PortalDeclaration;
45
52
  os_min_ver?: string;
46
53
  displayname?: string;
@@ -49,6 +56,6 @@ export declare function executorPolicy(m: {
49
56
  root?: string;
50
57
  };
51
58
  };
52
- }): ExecutorPolicy;
59
+ }, arch?: NativeArchFamily): ExecutorPolicy;
53
60
  /** Inventory rejects malformed extensions even if a caller recomputes every pin. */
54
61
  export declare function validExecutorPolicy(value: unknown, pkg: string): value is ExecutorPolicy;