@synopackageland/cli 0.9.1 → 0.9.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2 (2026-10-12)
4
+
5
+ Builds with SDK 0.9.3 and templates 0.9.2: `init` writes SDK v0.9.3 and
6
+ `@synopackageland/sdk` 0.9.3. The executor digest table records SDK 0.9.2 and
7
+ 0.9.3 (the same digests; 0.9.3 changes only translations). An App rebuilt
8
+ with SDK 0.9.2 or 0.9.3 gets the new executor; the policy digest stays the
9
+ same, so an installed App needs no new approval.
10
+
11
+ 繁中摘要:`check` 與 `build` 的錯誤訊息每個錯誤碼只出現一次;`build --json` 的 SDK
12
+ 下載失敗附上 GOPRIVATE 提示;有 executor 並宣告 `install.sharedFolder` 的 App 通過
13
+ `inspect` 的 postinst 檢查。重新安裝後的核准只需輸入一次密碼:共用資料夾讀回等 40 s,
14
+ 沒到達 executor 的選擇任務在同一次核准內再跑一次,啟動後的健康檢查等 30 s。
15
+
16
+ - `check` and `build` name each error code once. 0.9.1 printed the first code
17
+ twice, for example
18
+ `[STANDALONE_PORTAL_REQUIRES_EXECUTOR] [STANDALONE_PORTAL_REQUIRES_EXECUTOR] …`.
19
+ - A `--json` error without its own hint takes the registry suggestion of its
20
+ code. `build --json` now answers `SDK_DOWNLOAD_FAILED` with the GOPRIVATE and
21
+ git-credential step instead of the generic hint.
22
+ - `inspect` accepts an App with `capabilities.rootOps` or `delegate` that
23
+ declares `install.sharedFolder`. Its `postinst` and `postupgrade` start the
24
+ executor before the shared-folder script, and 0.9.1 reported them as not
25
+ matching.
26
+ - Re-approval after a reinstall takes one password submission for both the
27
+ executor and an install-selected share. The DSM window now reads the share
28
+ selection back for 40 s instead of 20 s, runs a selection task that missed
29
+ the restarting executor once more without a new confirmation, and waits
30
+ 30 s instead of 10 s for a started executor before withdrawing the tasks an
31
+ approval created.
32
+
3
33
  ## 0.9.1 (2026-10-12)
4
34
 
5
35
  Fixes from the 0.9.0 acceptance on DSM (t-0294, t-0290, t-0292).
package/dist/cli.js CHANGED
@@ -5,7 +5,8 @@ import { basename, resolve, dirname, join } from "node:path";
5
5
  import { findProjectRoot } from "./discovery.js";
6
6
  import { loadContract } from "./contract/load.js";
7
7
  import { contractIconPath } from "./contract/icon.js";
8
- import { formatErrors } from "./errors.js";
8
+ import { codedMessage, formatErrors } from "./errors.js";
9
+ import { getErrorCode } from "./error-registry.js";
9
10
  import { formatInspectReport, inspectProject } from "./inspect.js";
10
11
  import { formatPinReport, pinProjectImages } from "./images/pin.js";
11
12
  import { initFromTemplate, isTemplateName, listTemplates } from "./init.js";
@@ -33,7 +34,7 @@ import { checkJobs, commitOutputs, forEachArch } from "./standalone/arch-jobs.js
33
34
  import { checkReproducible } from "./reproducible.js";
34
35
  import { inspectAnySpk } from "./spk/inspect.js";
35
36
  function failure(code, message, hint = "修正設定後重試;使用 --help 查看指令用法。") {
36
- return Object.assign(new Error(`[${code}] ${message}`), { code, hint });
37
+ return Object.assign(new Error(codedMessage(code, message)), { code, hint });
37
38
  }
38
39
  function requireProject(cwd, project) {
39
40
  const found = findProjectRoot(project ? resolve(cwd, project) : cwd);
@@ -531,7 +532,7 @@ export async function runCli(argv) {
531
532
  ok: false,
532
533
  code,
533
534
  message,
534
- hint: detail.hint ?? "修正設定後重試;使用 --help 查看指令用法。",
535
+ hint: detail.hint ?? getErrorCode(code)?.suggestion ?? "修正設定後重試;使用 --help 查看指令用法。",
535
536
  ...(detail.report ? { data: detail.report } : {}),
536
537
  }));
537
538
  }
package/dist/errors.d.ts CHANGED
@@ -3,4 +3,6 @@ export declare function makeError(code: string, span: SourceSpan, message: strin
3
3
  export declare function contractSpan(file: string, line: number, column: number, path?: string): SourceSpan;
4
4
  export declare function composeSpan(file: string, line: number, column: number, path?: string): SourceSpan;
5
5
  export declare function formatError(error: ValidationError): string;
6
+ /** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
7
+ export declare function codedMessage(code: string, message: string): string;
6
8
  export declare function formatErrors(errors: ValidationError[]): string;
package/dist/errors.js CHANGED
@@ -13,6 +13,11 @@ export function formatError(error) {
13
13
  : `${error.span.file}:${error.span.line}:${error.span.column}`;
14
14
  return `[${error.code}] ${location}\n${error.message}\n建議:${error.suggestion}`;
15
15
  }
16
+ /** The message of an error thrown with a code: the code in brackets, then the message; the code only once. */
17
+ export function codedMessage(code, message) {
18
+ const prefix = `[${code}] `;
19
+ return message.startsWith(prefix) ? message : prefix + message;
20
+ }
16
21
  export function formatErrors(errors) {
17
22
  return errors.map(formatError).join("\n\n");
18
23
  }
@@ -9,7 +9,6 @@ import { isScalar, parseDocument } from "yaml";
9
9
  * (Docker Hub and ghcr both issue an anonymous Bearer token for public repos).
10
10
  */
11
11
  const DIGEST_VALUE = /^sha256:[0-9a-f]{64}$/i;
12
- const DIGEST_AT_END = /@sha256:([0-9a-f]{64})$/i;
13
12
  /** A registry returns the index (manifest list) digest when we ask for it. */
14
13
  const MANIFEST_ACCEPT = [
15
14
  "application/vnd.oci.image.index.v1+json",
package/dist/init.js CHANGED
@@ -257,7 +257,6 @@ function substitute(path, substitutions) {
257
257
  writeFileSync(path, text);
258
258
  }
259
259
  function substitutionsFor(template, targetDir, options) {
260
- const ui = resolvedUi(template, options);
261
260
  const caps = capabilityNames(options.with ?? []);
262
261
  if (options.ui !== undefined && template !== "native-go")
263
262
  throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go。");
@@ -20,5 +20,27 @@
20
20
  "armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
21
21
  },
22
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"
23
45
  }
24
46
  }
@@ -118,7 +118,7 @@ export async function inspectSpk(spkPath, expectedKind) {
118
118
  if (metadata.entry)
119
119
  await checkEntryLauncher(dir, info.package, metadata.entry, findings);
120
120
  if (standalone)
121
- checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal);
121
+ checkSharedFolder(dir, findings, info.package, sharedFolder, executor?.policy.scope === 'install-selected', portal, hasExecutor);
122
122
  const icon = inspectPackageIcons(dir, findings);
123
123
  const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
124
124
  if (standalone && !cgiEntry) {
@@ -381,7 +381,7 @@ function checkBackground(dir, info, payload, findings, executor, daemon) {
381
381
  * declaration (without one, any present file must be the default native
382
382
  * install wizard), and no other install/upgrade wizard file may exist.
383
383
  */
384
- function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal) {
384
+ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal, executor = false) {
385
385
  const wizardDir = join(dir, "WIZARD_UIFILES");
386
386
  const present = (existsSync(wizardDir) ? readdirSync(wizardDir) : []).filter(name => /^(install|upgrade)_uifile(_|\.|$)/.test(name)).sort();
387
387
  const postinst = readOptional(join(dir, "scripts/postinst"));
@@ -407,7 +407,7 @@ function checkSharedFolder(dir, findings, pkg, folder, selected = false, portal)
407
407
  if (!folder)
408
408
  return;
409
409
  for (const hook of ["postinst", "postupgrade"]) {
410
- if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, { package: pkg, sharedFolder: folder, ...(selected ? { capabilities: { delegate: { scope: "install-selected" } } } : {}), ...portalManifest(portal) })) {
410
+ if (readOptional(join(dir, "scripts", hook)) !== renderStandaloneScript(hook, { package: pkg, sharedFolder: folder, ...(selected ? { capabilities: { delegate: { scope: "install-selected" } } } : executor ? { capabilities: { rootOps: true } } : {}), ...portalManifest(portal) })) {
411
411
  findings.push(`scripts/${hook} 與 install.sharedFolder 宣告的固定腳本不符`);
412
412
  }
413
413
  }
@@ -155,22 +155,34 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
155
155
  if (current.ref) throw fail("executor_selection_cleanup_conflict");
156
156
  });
157
157
  }
158
+ // The task waits up to 35 s for the executor, whose account, AppPriv and
159
+ // share lookups took 16-20 s on a measured NAS: read back for 40 s,
160
+ // backing off from 500 ms to 2 s like health.
161
+ function readSelection(budget, delay) {
162
+ delay = Math.min(delay || 500, 2000);
163
+ verified.state = "ready";
164
+ return health(verified, 0).then(function (current) {
165
+ if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
166
+ if (budget >= delay && options.wait) return options.wait(delay).then(function () { return readSelection(budget - delay, delay * 2); });
167
+ throw fail("executor_selection_unconfirmed");
168
+ });
169
+ }
170
+ // Right after a reinstall the boot task restarts the executor, so a run can
171
+ // miss it. Past the readback window that run has ended: run the pinned task
172
+ // once more. Its expected generation makes it stale if the first one landed.
173
+ function runAndRead(ref, runs) {
174
+ return send(taskAPI, "run", 2, {tasks: [ref]}).then(function () { return readSelection(40000); }).catch(function (e) {
175
+ if (runs > 1 && e && e.code === "executor_selection_unconfirmed") return runAndRead(ref, runs - 1);
176
+ throw e;
177
+ });
178
+ }
158
179
  return find(task).then(function (current) {
159
180
  if (current.state === "blocked" || current.state === "unsupported") throw fail("executor_selection_conflict");
160
181
  var removed = current.ref ? remove(task, current.ref) : Promise.resolve();
161
182
  return removed.then(function () { return create(false, definition(task), confirmation); });
162
183
  }).then(function () { return find(task); }).then(function (current) {
163
184
  if (current.state !== "ready") throw fail("executor_selection_readback_mismatch");
164
- return send(taskAPI, "run", 2, {tasks: [current.ref]});
165
- }).then(function () {
166
- function readSelection(attempts) {
167
- return health(verified, 0).then(function (current) {
168
- if (current.state === "ready" && current.executor.selection && current.executor.selection.scope === selectedScope && Number.isSafeInteger(current.executor.selection.generation) && current.executor.selection.generation > 0) return current;
169
- if (attempts > 1 && options.wait) return options.wait(500).then(function () { verified.state = "ready"; return readSelection(attempts - 1); });
170
- throw fail("executor_selection_unconfirmed");
171
- });
172
- }
173
- return readSelection(40);
185
+ return runAndRead(current.ref, 2);
174
186
  }).then(function (result) { return removeExact().then(function () { return result; }); }, function (error) {
175
187
  return removeExact().then(function () { throw error; });
176
188
  });
@@ -188,6 +200,9 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
188
200
  verified.executor = status; return verified;
189
201
  }, function () { return null; });
190
202
  }
203
+ // A started executor answered only after its own DSM lookups, which took
204
+ // 16-20 s on a measured NAS; giving up withdraws a fresh approval.
205
+ var startBudget = 30000;
191
206
  // budget: how long to keep asking after a start (ms). The wait doubles from
192
207
  // 500 ms to 2 s so an open window does not poll the executor twice a second.
193
208
  function health(verified, budget, delay) {
@@ -211,7 +226,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
211
226
  return send(eventAPI, "run", 1, verified.tasks.boot.ref);
212
227
  }).then(function () {
213
228
  // run can succeed for a nonexistent task. Read both definitions again.
214
- return read().then(function (after) { return health(after, 10000); });
229
+ return read().then(function (after) { return health(after, startBudget); });
215
230
  }).catch(function (e) { verified.state = "blocked"; verified.reason = reason(e); return verified; });
216
231
  }
217
232
  // The window panel and the app's own settings share this access; each
@@ -230,7 +245,7 @@ export const ROOT_RUNTIME = `function createRootAccess(options) {
230
245
  if (verified.state !== "ready") throw fail("executor_not_ready");
231
246
  return options.call("executor.control", {action: action}).then(function () {
232
247
  return send(taskAPI, "run", 2, {tasks: [verified.tasks.control.ref]});
233
- }).then(read).then(function (after) { return action === "start" ? health(after, 10000) : after; });
248
+ }).then(read).then(function (after) { return action === "start" ? health(after, startBudget) : after; });
234
249
  }).then(publish);
235
250
  },
236
251
  subscribe: function (listener) { listeners.push(listener); }
@@ -12,6 +12,7 @@ import { writeWizardUiFiles } from "../build-spk/wizard-i18n.js";
12
12
  import { renderDelegateScopeSteps } from '../build-spk/delegate-scope.js';
13
13
  import { createReproducibleTar } from "../build-spk/reproducible-archive.js";
14
14
  import { renderStandaloneScript } from "./lifecycle.js";
15
+ import { codedMessage } from "../errors.js";
15
16
  import { DSM_LANGS, LOCALES } from "./i18n.js";
16
17
  import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName } from "./dsm-window.js";
17
18
  import { writeUiTexts } from "../build-spk/ui-texts.js";
@@ -40,7 +41,7 @@ export const LIFECYCLE_SCRIPTS = [
40
41
  export class StandaloneBuildError extends Error {
41
42
  code;
42
43
  constructor(code, message) {
43
- super(`[${code}] ${message}`);
44
+ super(codedMessage(code, message));
44
45
  this.code = code;
45
46
  }
46
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Syno Package Land Developer Kit CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "dependencies": {
27
- "@synopackageland/templates": "0.9.1",
27
+ "@synopackageland/templates": "0.9.2",
28
28
  "ajv": "^8.17.1",
29
29
  "commander": "^13.1.0",
30
30
  "tar": "^7.4.3",