@synopackageland/cli 0.9.7 → 0.10.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 (111) hide show
  1. package/CHANGELOG.md +146 -0
  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 +14 -91
  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 +69 -18
  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
@@ -1,38 +1,52 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  const EXECUTOR_SHELL = ['backend/internal/app/executor.go', 'backend/internal/app/executor_test.go'];
4
+ export const BRIDGE_TEMPLATES = ['native-python', 'native-node', 'native-rust'];
4
5
  export const CAPABILITY_OVERLAYS = { 'native-go': {
5
- 'root-ops': { overlays: ['native-go-root-ops'], borrowed: [], removed: ['backend/internal/app/root_none.go'] },
6
- jobs: { overlays: ['native-go-jobs'], borrowed: [], removed: ['backend/internal/app/background_none.go'] },
6
+ 'root-ops': { overlays: ['native-go-root-ops'], borrowed: [], removed: ['backend/internal/app/root_none.go'], permissions: {} },
7
+ jobs: { overlays: ['native-go-jobs'], borrowed: [], removed: ['backend/internal/app/background_none.go'], permissions: {} },
7
8
  delegate: {
8
9
  overlays: ['native-go-jobs', 'native-go-delegate'],
9
10
  borrowed: EXECUTOR_SHELL.map(file => `native-go-root-ops/${file}`),
10
11
  removed: ['backend/internal/app/background_none.go', 'backend/internal/app/root_none.go', 'backend/internal/app/delegate_none.go'],
12
+ // Every signed-in user opens a delegate App to pick where reports go.
13
+ permissions: { 'app.status': 'authenticated' },
11
14
  },
12
- },
13
- // Rust has no build tags: an overlay overwrites the base's same-named files and
14
- // nothing is removed. The frontend and root/ files come from native-go's overlays.
15
- 'native-rust': {
16
- 'root-ops': { overlays: ['native-rust-root-ops'], borrowed: ['native-go-root-ops/root'], removed: [] },
17
- jobs: { overlays: ['native-rust-jobs'], borrowed: ['native-go-jobs/frontend'], removed: [] },
15
+ }, ...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, bridgeOverlays(template)])) };
16
+ /**
17
+ * A bridge App runs the Go template's example in its own language: each
18
+ * overlay replaces a stub module, and the frontend and root programs come
19
+ * from the Go overlays. In this order, a Rust manifest lists the methods.
20
+ */
21
+ function bridgeOverlays(template) {
22
+ return {
23
+ // The example hashes a file the caller picks, which the runtime reads only for an App with files.
24
+ jobs: { overlays: [`${template}-jobs`], borrowed: ['native-go-jobs/frontend'], removed: [], permissions: {}, implied: ['files'], methods: ['hash.submit', 'hash.get', 'hash.cancel'] },
25
+ 'root-ops': { overlays: [`${template}-root-ops`], borrowed: ['native-go-root-ops/root', 'native-go-root-ops/frontend'], removed: [], permissions: {}, methods: ['whoami.run'] },
18
26
  delegate: {
19
- overlays: ['native-rust-jobs', 'native-rust-delegate'],
20
- borrowed: ['native-go-jobs/frontend', 'native-go-delegate/frontend', 'native-rust-root-ops/backend/src/app/root_routes.rs'],
21
- removed: [],
27
+ overlays: [`${template}-delegate`], borrowed: ['native-go-jobs/frontend', 'native-go-delegate/frontend'], removed: [],
28
+ permissions: { 'app.status': 'authenticated', 'report.save': 'authenticated' }, methods: ['report.save'],
22
29
  },
23
- } };
24
- /** The overlays of a native template's Vue frontend, copied before the capabilities'. */
25
- export const VUE_OVERLAYS = { 'native-go': ['native-go-vue'], 'native-rust': ['native-go-vue', 'native-rust-vue'] };
30
+ };
31
+ }
32
+ /** What a native template's base and Vue frontend add before the capabilities: a bridge App borrows the Go template's UI. */
33
+ export const BASE_BORROWED = { 'native-go': [],
34
+ ...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, ['native-go/ui', 'native-go/i18n']])) };
35
+ export const VUE_OVERLAYS = { 'native-go': { overlays: ['native-go-vue'], borrowed: [] },
36
+ ...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, { overlays: [`${template}-vue`], borrowed: ['native-go-vue/frontend'] }])) };
26
37
  /** The overlays of these capabilities, in table order, each once; a capability without one adds nothing. */
27
38
  export function capabilityOverlays(template, names) {
28
39
  const chosen = Object.entries(CAPABILITY_OVERLAYS[template]).filter(([name]) => names.includes(name)).map(([, overlay]) => overlay);
29
40
  const unique = (pick) => [...new Set(chosen.flatMap(pick))];
30
- return { overlays: unique(o => o.overlays), borrowed: unique(o => o.borrowed), removed: unique(o => o.removed) };
41
+ return { overlays: unique(o => o.overlays), borrowed: unique(o => o.borrowed), removed: unique(o => o.removed),
42
+ permissions: Object.assign({}, ...chosen.map(o => o.permissions)),
43
+ implied: [...new Set(chosen.flatMap(o => o.implied ?? []))], methods: unique(o => o.methods ?? []) };
31
44
  }
32
45
  /** Each file of the borrowed paths, a borrowed folder listed file by file, under `templates`. */
33
46
  export function borrowedFiles(templates, borrowed) {
47
+ // A template folder may carry what developing it installs or builds.
34
48
  const walk = (path) => statSync(join(templates, path)).isDirectory()
35
- ? readdirSync(join(templates, path)).sort().flatMap(name => walk(`${path}/${name}`))
49
+ ? readdirSync(join(templates, path)).sort().filter(name => name !== 'node_modules' && name !== 'dist').flatMap(name => walk(`${path}/${name}`))
36
50
  : [path];
37
51
  return [...new Set(borrowed.flatMap(walk))];
38
52
  }
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { codedMessage, formatErrors } from "./errors.js";
9
9
  import { getErrorCode } from "./error-registry.js";
10
10
  import { formatInspectReport, inspectProject } from "./inspect.js";
11
11
  import { formatPinReport, pinProjectImages } from "./images/pin.js";
12
- import { initFromTemplate, isTemplateName, listTemplates } from "./init.js";
12
+ import { initFromTemplate, initNextSteps, isTemplateName, listTemplates } from "./init.js";
13
13
  import { templatesRoot } from "./template-paths.js";
14
14
  import { addCapabilities } from "./add.js";
15
15
  import { CAPABILITY_NAMES } from "./capabilities.js";
@@ -33,13 +33,13 @@ import { buildStandalone, runnerFor } from "./standalone/build.js";
33
33
  import { checkJobs, commitOutputs, forEachArch } from "./standalone/arch-jobs.js";
34
34
  import { checkReproducible } from "./reproducible.js";
35
35
  import { inspectAnySpk } from "./spk/inspect.js";
36
- function failure(code, message, hint = "修正設定後重試;使用 --help 查看指令用法。") {
36
+ function failure(code, message, hint = "Fix the configuration and retry; use --help to see command usage.") {
37
37
  return Object.assign(new Error(codedMessage(code, message)), { code, hint });
38
38
  }
39
39
  function requireProject(cwd, project) {
40
40
  const found = findProjectRoot(project ? resolve(cwd, project) : cwd);
41
41
  if (!found)
42
- throw failure("PROJECT_NOT_FOUND", "找不到 synology-app.yaml 或 dsm-package.yaml。", "在專案根目錄執行,或使用 --project 指定。");
42
+ throw failure("PROJECT_NOT_FOUND", "synology-app.yaml or dsm-package.yaml was not found.", "Run from the project root, or pass --project.");
43
43
  return found;
44
44
  }
45
45
  function standaloneRoot(cwd, project) {
@@ -48,7 +48,7 @@ function standaloneRoot(cwd, project) {
48
48
  function collectKv(value, previous, flag) {
49
49
  const separator = value.indexOf("=");
50
50
  if (separator <= 0)
51
- throw failure("CLI_OPTION_INVALID", `${flag} 格式應為 SLOT=值。`);
51
+ throw failure("CLI_OPTION_INVALID", `${flag} expects the format SLOT=value.`);
52
52
  return {
53
53
  ...previous,
54
54
  [value.slice(0, separator)]: value.slice(separator + 1),
@@ -58,14 +58,14 @@ function collectKv(value, previous, flag) {
58
58
  function formatIconReport(report, manifestFile) {
59
59
  return {
60
60
  lines: report.source === "custom"
61
- ? [`圖示:自訂 ${report.path}`]
61
+ ? [`Icon: custom ${report.path}`]
62
62
  : report.source === "layout"
63
- ? ["圖示:由 layout metadata 的 PACKAGE_ICON*.PNG 提供(若有)", `info:在 ${manifestFile} 設定 icon: 可改由 CLI 產生 Package Center 圖示。`]
64
- : ["圖示:SDK 預設圖示", `info:在 ${manifestFile} 設定 icon: <正方形 PNG(至少 256×256)或 icon_<size>.png 目錄> 換成自己的圖示。`],
65
- ...(report.solid ? { warning: "warning:圖示只有單一顏色,Package Center 與 DSM 主選單會像一個色塊;建議換成有圖形的圖示。" } : {}),
63
+ ? ["Icon: provided by the layout metadata's PACKAGE_ICON*.PNG (if present)", `info: set icon: in ${manifestFile} to have the CLI generate the Package Center icon.`]
64
+ : ["Icon: SDK default icon", `info: set icon: in ${manifestFile} to <a square PNG (at least 256x256) or an icon_<size>.png directory> to use your own icon.`],
65
+ ...(report.solid ? { warning: "warning: the icon is a single colour, so Package Center and the DSM main menu show a flat block; use an icon with a graphic." } : {}),
66
66
  };
67
67
  }
68
- const LABEL = { error: "錯誤", warning: "警告" };
68
+ const LABEL = { error: "error", warning: "warning" };
69
69
  /** One line per finding: severity, stable code, where, and what to fix. */
70
70
  function formatFindings(findings) {
71
71
  return findings.map((f) => `${LABEL[f.severity]} ${f.code}${f.path ? ` ${f.path}` : ""}: ${f.message}`);
@@ -74,17 +74,17 @@ function formatFindings(findings) {
74
74
  function formatSpkChecks(report) {
75
75
  const { errors, warnings } = report.dsm;
76
76
  return [
77
- "DSM 格式:",
77
+ "DSM format:",
78
78
  ...formatFindings([...errors, ...warnings]).map((line) => ` ${line}`),
79
- ` ${report.dsm.ok ? "符合" : "不符合"} DSM 7 套件格式:${errors.length} 個錯誤、${warnings.length} 個警告。`,
79
+ ` ${report.dsm.ok ? "matches" : "does not match"} the DSM 7 package format: ${errors.length} errors, ${warnings.length} warnings.`,
80
80
  ...(report.synopkgland
81
81
  ? [
82
- "synopkgland 合約:",
82
+ "synopkgland contract:",
83
83
  report.findings.length
84
- ? " 問題:"
84
+ ? " Problems:"
85
85
  : report.executor
86
- ? " 檢查:沒有異常。管理員核准後會建立 root executor 排程(boot 與 control task),以 root 執行此 SPK 固定的 executor 與 policy。"
87
- : " 檢查:沒有 root 權限需求或異常。",
86
+ ? " Check: no problems. After an administrator approves, it creates root executor tasks (boot and control task) that run this SPK's fixed executor and policy as root."
87
+ : " Check: no root privilege requirement or problem.",
88
88
  ...report.findings.map((f) => ` - ${f}`),
89
89
  ]
90
90
  : []),
@@ -92,25 +92,25 @@ function formatSpkChecks(report) {
92
92
  }
93
93
  function formatSpkReport(spk, report) {
94
94
  return [
95
- `SPK:${spk}`,
96
- `package:${report.package ?? "?"} version:${report.version ?? "?"}`,
97
- report.synopkgland ? `arch:${report.arch ?? "?"} runtime:${report.kind ?? "?"}` : `arch:${report.arch ?? "?"} 建置:不是 synopkgland(只檢查 DSM 格式)`,
98
- `簽署:${report.signed ? "有 Synology 簽章" : "未簽署(Package Center 需允許任何發行者)"}`,
99
- ...(report.synopkgland ? [`CGI:${report.cgi ?? "無"}`] : []),
100
- ...(report.icon ? [`圖示:${report.icon.source === "default" ? "SDK 預設圖示" : report.icon.source === "custom" ? `自訂(PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "無 PACKAGE_ICON"}`] : []),
101
- ...(report.entry === "admin-only" ? ["入口:entry: admin-only,只有 DSM 管理員能開啟與呼叫任何 route(含 SDK routes)"] : []),
102
- ...(report.entry === "app-privilege" ? ["入口:entry: app-privilege,由 DSM 控制台「應用程式權限」決定誰能使用"] : []),
103
- ...(report.portal ? [`Application Portal:預設別名 /${report.portal.alias}/(${report.portal.appId}),由管理員套用後啟用`] : []),
104
- ...report.capabilities.map((cap) => `能力 ${cap.name}:${cap.requiresAdminApproval ? "需要 DSM 管理員核准" : "安裝後 ready"}`),
105
- "payload:",
106
- ...(report.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed):${item.target} — ${item.condition} ${item.action}`),
95
+ `SPK: ${spk}`,
96
+ `package: ${report.package ?? "?"} version: ${report.version ?? "?"}`,
97
+ report.synopkgland ? `arch: ${report.arch ?? "?"} runtime: ${report.kind ?? "?"}` : `arch: ${report.arch ?? "?"} build: not synopkgland (DSM format only)`,
98
+ `Signature: ${report.signed ? "Synology-signed" : "unsigned (Package Center must allow any publisher)"}`,
99
+ ...(report.synopkgland ? [`CGI: ${report.cgi ?? "none"}`] : []),
100
+ ...(report.icon ? [`Icon: ${report.icon.source === "default" ? "SDK default icon" : report.icon.source === "custom" ? `custom (PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "no PACKAGE_ICON"}`] : []),
101
+ ...(report.entry === "admin-only" ? ["Entry: entry: admin-only; only a DSM administrator can open it and call any route (including SDK routes)"] : []),
102
+ ...(report.entry === "app-privilege" ? ["Entry: entry: app-privilege; DSM Control Panel > Application Privileges decides who can use it"] : []),
103
+ ...(report.portal ? [`Application Portal: default alias /${report.portal.alias}/ (${report.portal.appId}), active after an administrator applies it`] : []),
104
+ ...report.capabilities.map((cap) => `Capability ${cap.name}: ${cap.requiresAdminApproval ? "needs DSM administrator approval" : "ready after install"}`),
105
+ "payload:",
106
+ ...(report.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
107
107
  ...report.payload.map((e) => ` ${e.mode} ${e.path}`),
108
108
  ...formatSpkChecks(report),
109
109
  ].join("\n");
110
110
  }
111
111
  async function standaloneBuild(root, arches, options) {
112
112
  if (options.output && arches?.length !== 1)
113
- throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o 只能搭配 --arch 建置單一架構;多架構請用 --out-dir。");
113
+ throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o requires a single --arch; use --out-dir for multiple architectures.");
114
114
  const outDir = options.output
115
115
  ? dirname(resolve(options.output))
116
116
  : options.outDir
@@ -146,10 +146,10 @@ function reproducibleTarget(standalone, project, arches) {
146
146
  }
147
147
  function reproducibleLines(report) {
148
148
  if (report.skipped)
149
- return [`可重現檢查:依賴指紋 ${report.fingerprint.slice(0, 12)} 與上次通過時相同,略過。`];
149
+ return [`Reproducible check: the dependency fingerprint ${report.fingerprint.slice(0, 12)} matches the last passing run, skipping.`];
150
150
  return report.results.map((result) => result.identical
151
- ? `可重現檢查:${basename(result.output)} 相同(sha256 ${result.sha256[0].slice(0, 12)})`
152
- : `可重現檢查:${basename(result.output)} 不同(sha256 ${result.sha256[0].slice(0, 12)} ≠ ${result.sha256[1].slice(0, 12)})\n` +
151
+ ? `Reproducible check: ${basename(result.output)} is identical (sha256 ${result.sha256[0].slice(0, 12)})`
152
+ : `Reproducible check: ${basename(result.output)} differs (sha256 ${result.sha256[0].slice(0, 12)} ≠ ${result.sha256[1].slice(0, 12)})\n` +
153
153
  [...result.payload.map((file) => ` payload ${file}`), ...result.metadata.map((file) => ` metadata ${file}`)].join("\n"));
154
154
  }
155
155
  const ARCH_REQUIRED = [
@@ -160,12 +160,12 @@ async function containerBuild(project, arches, options) {
160
160
  const jobs = checkJobs(options.jobs);
161
161
  const unsupported = arches?.find((arch) => !isContainerArchFamily(arch));
162
162
  if (unsupported)
163
- throw failure("CONTAINER_ARCH_UNSUPPORTED", `Container Manager App 只支援 ${CONTAINER_ARCH_FAMILIES.join("、")};DSM 不為 ${unsupported} 提供 Container Manager。`);
163
+ throw failure("CONTAINER_ARCH_UNSUPPORTED", `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${unsupported}.`);
164
164
  const output = options.output ? resolve(options.output) : undefined;
165
165
  const build = async (archFamily, outputPath = output) => {
166
166
  const result = await buildSpk({ project, archFamily, outputPath, minDsm: options.minDsm, gatewayRun: archFamily && runnerFor(undefined, jobs, archFamily) });
167
167
  if (!result.ok && !(archFamily === undefined && result.errors.every((code) => ARCH_REQUIRED.includes(code))))
168
- throw failure(result.errors[0] ?? "BUILD_FAILED", `建置失敗:${result.errors.join(", ")}`);
168
+ throw failure(result.errors[0] ?? "BUILD_FAILED", `Build failed: ${result.errors.join(", ")}`);
169
169
  return result;
170
170
  };
171
171
  if (arches?.length === 1)
@@ -174,7 +174,7 @@ async function containerBuild(project, arches, options) {
174
174
  if (first.ok)
175
175
  return [first.outputPath];
176
176
  if (output)
177
- throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o 只能搭配 --arch 建置單一架構。");
177
+ throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o requires a single --arch.");
178
178
  const packageId = String(loadContract(project.contractPath).contract.package);
179
179
  return commitOutputs(options.outDir ? resolve(options.outDir) : project.root, (stage) => forEachArch(arches ?? CONTAINER_ARCH_FAMILIES, jobs, async (arch) => (await build(arch, join(stage, `${packageId}-${arch}.spk`))).outputPath));
180
180
  }
@@ -186,21 +186,21 @@ export async function runCli(argv) {
186
186
  const program = new Command()
187
187
  .name("synopkgland")
188
188
  .description("Syno Package Land Developer Kit CLI")
189
- .option("--project <dir>", "App 專案根目錄")
189
+ .option("--project <dir>", "App project root")
190
190
  .exitOverride();
191
191
  const rootOf = (command) => resolve(process.cwd(), command.parent?.opts().project ?? ".");
192
192
  const hostOf = (host) => {
193
193
  if (!host)
194
- throw failure("HOST_REQUIRED", "請指定 --host。", "設定 synoagentcli profile,然後使用 --host <name>。");
194
+ throw failure("HOST_REQUIRED", "--host is required.", "Set a synoagentcli profile, then use --host <name>.");
195
195
  const nas = createNasHost({ host });
196
196
  if (!nas)
197
- throw failure("NAS_CREDENTIALS_MISSING", "找不到 NAS 憑證。", "設定 synoagentcli profile 或 DSM_URL/DSM_PASSWORD。");
197
+ throw failure("NAS_CREDENTIALS_MISSING", "NAS credentials were not found.", "Set a synoagentcli profile or DSM_URL/DSM_PASSWORD.");
198
198
  return nas;
199
199
  };
200
200
  program
201
201
  .command("check")
202
- .description("驗證 manifest、能力組合與 Compose")
203
- .option("--json", "輸出單一 JSON 物件")
202
+ .description("Validate the manifest, capability combination and Compose")
203
+ .option("--json", "Print a single JSON object")
204
204
  .action((_options, command) => {
205
205
  const validation = validateProject(requireProject(rootOf(command)), {
206
206
  profile: "personal",
@@ -211,7 +211,7 @@ export async function runCli(argv) {
211
211
  if (standalone) {
212
212
  const loaded = isNativeLayout(standalone) ? loadNativeLayout(standalone) : loadStandaloneManifest(standalone);
213
213
  if (!loaded.ok)
214
- throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "將 dsm-package.yaml 更新為 schema: 2,修正宣告後重試。");
214
+ throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "Update dsm-package.yaml to schema: 2, fix the declaration and retry.");
215
215
  const declared = "icon" in loaded.manifest && loaded.manifest.icon ? resolve(standalone, loaded.manifest.icon) : undefined;
216
216
  const icon = isNativeLayout(standalone) && !declared ? { source: "layout", solid: false } : iconReport(standalone, declared);
217
217
  const iconText = formatIconReport(icon, "dsm-package.yaml");
@@ -221,7 +221,7 @@ export async function runCli(argv) {
221
221
  package: loaded.manifest.package,
222
222
  capabilities: "capabilities" in loaded.manifest ? loaded.manifest.capabilities : {},
223
223
  icon,
224
- }, ["驗證通過。", ...iconText.lines].join("\n"));
224
+ }, ["Validation passed.", ...iconText.lines].join("\n"));
225
225
  return;
226
226
  }
227
227
  const project = requireProject(rootOf(command));
@@ -235,26 +235,26 @@ export async function runCli(argv) {
235
235
  const iconText = formatIconReport(icon, "synology-app.yaml");
236
236
  if (iconText.warning)
237
237
  console.error(iconText.warning);
238
- emit("check", { report, warnings: result.warnings ?? [], icon }, [`驗證通過。`, formatInspectReport(report), ...iconText.lines].join("\n"));
238
+ emit("check", { report, warnings: result.warnings ?? [], icon }, [`Validation passed.`, formatInspectReport(report), ...iconText.lines].join("\n"));
239
239
  });
240
240
  program
241
241
  .command("inspect <spk>")
242
- .description("不安裝,依 DSM 7 套件格式檢查任何 SPK;synopkgland 建置的 SPK 另查建置合約與 root 權限需求")
243
- .option("--json", "輸出單一 JSON 物件")
242
+ .description("Inspect any SPK against the DSM 7 package format without installing; for a synopkgland-built SPK also check the build contract and root privilege requirements")
243
+ .option("--json", "Print a single JSON object")
244
244
  .action(async (input) => {
245
245
  const spk = resolve(input);
246
246
  if (existsSync(spk) && statSync(spk).isDirectory())
247
- throw failure("SPK_INSPECTION_FAILED", `${spk} 是目錄;inspect 檢查 .spk 檔。`, "檢查專案設定請用 synopkgland check。");
247
+ throw failure("SPK_INSPECTION_FAILED", `${spk} is a directory; inspect checks .spk files.`, "Use synopkgland check to inspect project settings.");
248
248
  const report = await inspectAnySpk(spk);
249
249
  if (!report.dsm.ok || report.findings.length)
250
- throw Object.assign(failure("SPK_INSPECTION_FAILED", formatSpkChecks(report).join("\n"), "依錯誤碼修正 SPK 並重新打包,錯誤碼說明見 reference/errors;勿安裝檢查未通過的 SPK。"), { report: { spk, ...report } });
250
+ throw Object.assign(failure("SPK_INSPECTION_FAILED", formatSpkChecks(report).join("\n"), "Fix the SPK by error code and repackage; see reference/errors for the codes; do not install an SPK that fails inspection."), { report: { spk, ...report } });
251
251
  emit("inspect", { spk, ...report }, formatSpkReport(spk, report));
252
252
  });
253
- const images = program.command("images").description("Container image 工具");
253
+ const images = program.command("images").description("Container image tools");
254
254
  images
255
255
  .command("pin")
256
- .description("把 compose.yaml 的 image tag 解析為 digest 並寫回(匿名 registry 查詢)")
257
- .option("--check", "只檢查,不寫入;未 pin 或過期時失敗")
256
+ .description("Resolve compose.yaml image tags to digests and write them back (anonymous registry lookup)")
257
+ .option("--check", "Check only, do not write; fail when unpinned or stale")
258
258
  .action(async (options, command) => {
259
259
  const project = requireProject(process.cwd(), command.parent?.parent?.opts().project);
260
260
  const result = await pinProjectImages({
@@ -263,31 +263,31 @@ export async function runCli(argv) {
263
263
  });
264
264
  stdout(formatPinReport(result, { check: options.check === true }));
265
265
  if (options.check && result.findings.length)
266
- throw failure("IMAGE_PIN_REQUIRED", "Container images 未 pin 或已過期。");
266
+ throw failure("IMAGE_PIN_REQUIRED", "Container images are unpinned or stale.");
267
267
  });
268
268
  program
269
269
  .command("init")
270
- .description("從模板建立 App 專案(不覆蓋既有檔案)")
271
- .option("-t, --template <name>", `模板:${listTemplates().join(" | ")}`)
272
- .option("--package <id>", "INFO.package(預設由目錄名產生)")
273
- .option("--name <display>", "顯示名稱")
274
- .option("--ui <vue|plain>", `原生模板 UI,預設 vue(UI ${JSON.parse(readFileSync(join(templatesRoot(), "native-go-vue/frontend/package.json"), "utf8")).dependencies["@synopackageland/ui"]});plain 使用 ui/app.js`)
275
- .option("--with <caps>", `能力(逗號分隔):${CAPABILITY_NAMES.join(",")}`)
276
- .option("--from <compose.yaml>", "compose:匯入現有 Compose;省略時建立範例")
277
- .option("--shared-folder <select|create>", "安裝精靈選擇或建立共用資料夾")
278
- .option("--shared-folder-name <name>", "建立的共用資料夾名稱")
279
- .option("--shared-folder-key <key>", "設定鍵,預設 shared")
280
- .option("--shared-folder-permission <ro|rw>", "套件帳號權限,預設 rw")
281
- .option("--min-dsm <version>", `最低 DSM 版本(native-go/native-rust/binary 預設 ${DEFAULT_MIN_DSM},compose 預設 ${CONTAINER_MIN_DSM})`)
282
- .argument("[dir]", "目標目錄", ".")
283
- .addHelpText("after", "\n原生專案:native-go、native-rust 與 binary 使用 dsm-package.yaml schema 2。\nCompose 專案:compose 使用 synology-app.yaml;--from 匯入 compose.yaml。\n快速開始:synopkgland init -t native-go --with files,jobs,login my-app\n cd my-app && synopkgland build\n")
270
+ .description("Create an App project from a template (never overwrites existing files)")
271
+ .option("-t, --template <name>", `Template: ${listTemplates().join(" | ")}`)
272
+ .option("--package <id>", "INFO.package (defaults to the directory name)")
273
+ .option("--name <display>", "Display name")
274
+ .option("--ui <vue|plain>", `Native UI ${JSON.parse(readFileSync(join(templatesRoot(), "native-go-vue/frontend/package.json"), "utf8")).dependencies["@synopackageland/ui"]} by default (vue); plain uses ui/app.js`)
275
+ .option("--with <caps>", `Capabilities (comma-separated): ${CAPABILITY_NAMES.join(",")}`)
276
+ .option("--from <compose.yaml>", "compose: import an existing Compose; omit to create an example")
277
+ .option("--shared-folder <select|create>", "Select or create a shared folder in the install wizard")
278
+ .option("--shared-folder-name <name>", "Name of the shared folder to create")
279
+ .option("--shared-folder-key <key>", "Settings key, defaults to shared")
280
+ .option("--shared-folder-permission <ro|rw>", "Package account permission, defaults to rw")
281
+ .option("--min-dsm <version>", `Minimum DSM version (native templates and binary default to ${DEFAULT_MIN_DSM}, compose to ${CONTAINER_MIN_DSM})`)
282
+ .argument("[dir]", "Target directory", ".")
283
+ .addHelpText("after", "\nNative projects: native-go, native-python, native-node, native-rust and binary use dsm-package.yaml schema 2.\nCompose projects: compose uses synology-app.yaml; --from imports compose.yaml.\nQuick start: synopkgland init -t native-go --with files,jobs,login my-app\n cd my-app && synopkgland build\n")
284
284
  .action((dir, options, command) => {
285
285
  if (command.parent?.opts().project)
286
- throw failure("CLI_OPTION_INVALID", "--project 不適用於 init。");
286
+ throw failure("CLI_OPTION_INVALID", "--project does not apply to init.");
287
287
  if (!options.template)
288
- throw failure("INIT_TEMPLATE_REQUIRED", `請指定 -t, --template <name>。可選模板:${listTemplates().join(" | ")}`);
288
+ throw failure("INIT_TEMPLATE_REQUIRED", `Specify -t, --template <name>. Available templates: ${listTemplates().join(" | ")}`);
289
289
  if (!isTemplateName(options.template))
290
- throw failure("INIT_TEMPLATE_UNKNOWN", `未知模板:${options.template}`);
290
+ throw failure("INIT_TEMPLATE_UNKNOWN", `Unknown template: ${options.template}`);
291
291
  const targetDir = resolve(dir);
292
292
  initFromTemplate(options.template, targetDir, {
293
293
  packageId: options.package,
@@ -307,30 +307,30 @@ export async function runCli(argv) {
307
307
  permission: options.sharedFolderPermission,
308
308
  },
309
309
  });
310
- stdout(`已建立 ${options.template} 專案於 ${targetDir}`);
311
- // --install-links packs a local SDK like the released package; a symlink would load the checkout's own node_modules.
312
- if (options.template === "native-go" || options.template === "native-rust")
313
- stdout(`下一步:\n cd ${dir}\n (cd backend && ${options.template === "native-go" ? "go test ./..." : "cargo test"})\n${existsSync(join(targetDir, "frontend", "package.json")) ? ` (cd frontend && npm install${process.env.SYNOPKGLAND_SDK_DIR ? " --install-links" : ""})\n` : ""} synopkgland build\n說明見 README.md。`);
310
+ stdout(`Created the ${options.template} project at ${targetDir}`);
311
+ const next = initNextSteps(options.template, dir, existsSync(join(targetDir, "frontend", "package.json")), Boolean(process.env.SYNOPKGLAND_SDK_DIR));
312
+ if (next)
313
+ stdout(next);
314
314
  });
315
315
  program
316
316
  .command("add <caps...>")
317
- .description(`加入能力(${CAPABILITY_NAMES.join("、")})或 portal 與 SDK-owned overlay;不覆寫既有應用檔案`)
317
+ .description(`Add capabilities (${CAPABILITY_NAMES.join(", ")}) or a portal and SDK-owned overlay; never overwrite existing app files`)
318
318
  .action((caps, _options, command) => {
319
319
  const result = addCapabilities(rootOf(command), caps);
320
320
  for (const note of result.notes ?? [])
321
- console.error(`[注意] ${note}`);
322
- stdout(`已新增:${result.added.join(", ") || "無"};已存在:${result.unchanged.join(", ") || "無"}`);
321
+ console.error(`[note] ${note}`);
322
+ stdout(`Added: ${result.added.join(", ") || "none"}; already present: ${result.unchanged.join(", ") || "none"}`);
323
323
  });
324
324
  program
325
325
  .command("dev")
326
- .description("啟動本機 Compose")
326
+ .description("Start the local Compose")
327
327
  .action(async (_options, command) => {
328
328
  const project = requireProject(rootOf(command));
329
329
  const validation = validateProject(project);
330
330
  if (!validation.ok)
331
331
  throw failure(validation.errors[0].code, formatErrors(validation.errors));
332
332
  const handle = await runDev({ project });
333
- stdout("Compose 已就緒。按 Ctrl+C 結束。");
333
+ stdout("Compose is ready. Press Ctrl+C to stop.");
334
334
  const shutdown = async () => {
335
335
  await handle.close();
336
336
  process.exit(0);
@@ -341,26 +341,26 @@ export async function runCli(argv) {
341
341
  });
342
342
  program
343
343
  .command("test")
344
- .description("驗證契約並等待 Compose 就緒")
344
+ .description("Validate the contract and wait for Compose to be ready")
345
345
  .action(async (_options, command) => {
346
346
  const result = await testProject({
347
347
  project: requireProject(rootOf(command)),
348
348
  });
349
349
  if (!result.ok)
350
- throw failure(result.errors[0] ?? "TEST_FAILED", `測試失敗:${result.errors.join(", ")}`);
351
- stdout("測試通過。");
350
+ throw failure(result.errors[0] ?? "TEST_FAILED", `Test failed: ${result.errors.join(", ")}`);
351
+ stdout("Tests passed.");
352
352
  });
353
353
  program
354
354
  .command("deploy")
355
- .description("建置 SPK 並安裝到 DSM")
356
- .option("--json", "輸出單一 JSON 物件")
355
+ .description("Build the SPK and install it into DSM")
356
+ .option("--json", "Print a single JSON object")
357
357
  .option("--host <name>", "NAS profile")
358
358
  .option("--arch <family>", NATIVE_ARCH_FAMILIES.join(" | "))
359
359
  .option("--dev", "Development Package Identity")
360
- .option("--share <slot=name>", "Compose 共用資料夾", (value, previous) => collectKv(value, previous, "--share"))
361
- .option("--run-as <slot=name>", "Compose DSM 使用者", (value, previous) => collectKv(value, previous, "--run-as"))
362
- .option("--param <NAME=value>", "安裝參數(Compose)或安裝精靈值(native-go/binary)", (value, previous) => collectKv(value, previous, "--param"))
363
- .option("--dry-run", "只輸出本機部署計畫,不連接 NAS")
360
+ .option("--share <slot=name>", "Compose shared folder", (value, previous) => collectKv(value, previous, "--share"))
361
+ .option("--run-as <slot=name>", "Compose DSM user", (value, previous) => collectKv(value, previous, "--run-as"))
362
+ .option("--param <NAME=value>", "Install parameter (Compose) or install wizard value (native-go/binary)", (value, previous) => collectKv(value, previous, "--param"))
363
+ .option("--dry-run", "Print the local deploy plan only, without connecting to the NAS")
364
364
  .action(async (options, command) => {
365
365
  if (options.arch !== undefined &&
366
366
  !isNativeArchFamily(String(options.arch)))
@@ -371,7 +371,7 @@ export async function runCli(argv) {
371
371
  });
372
372
  if (plan.kind !== "container" &&
373
373
  (options.share || options.runAs))
374
- throw failure("CLI_OPTION_INVALID", "native-go/binary 不接受 --share/--run-as;安裝精靈值請用 --param。");
374
+ throw failure("CLI_OPTION_INVALID", "native-go/binary do not accept --share/--run-as; pass install wizard values with --param.");
375
375
  if (options.dryRun) {
376
376
  emit("deploy", plan, JSON.stringify(plan, null, 2));
377
377
  return;
@@ -380,7 +380,7 @@ export async function runCli(argv) {
380
380
  const host = hostOf(options.host);
381
381
  const output = await deployStandalone(plan, host, { params: options.param });
382
382
  const status = await standaloneInfo({ root: plan.root, host });
383
- emit("deploy", { ...plan, output, status }, `已部署 ${plan.packageId}: ${output}\n${formatProjectInfo(status)}${plan.notes.length ? "\n" + plan.notes.join("\n") : ""}`);
383
+ emit("deploy", { ...plan, output, status }, `Deployed ${plan.packageId}: ${output}\n${formatProjectInfo(status)}${plan.notes.length ? "\n" + plan.notes.join("\n") : ""}`);
384
384
  return;
385
385
  }
386
386
  const project = requireProject(rootOf(command));
@@ -400,15 +400,15 @@ export async function runCli(argv) {
400
400
  paramValues: options.param,
401
401
  });
402
402
  if (!result.ok)
403
- throw failure(result.code ?? "DEPLOY_FAILED", result.message ?? "部署失敗。");
403
+ throw failure(result.code ?? "DEPLOY_FAILED", result.message ?? "Deployment failed.");
404
404
  for (const note of result.notes ?? [])
405
- console.error(`[注意] ${note}`);
406
- emit("deploy", result, `已部署 ${result.packageId}${result.openUrl ? "\nOpen URL:" + result.openUrl : ""}`);
405
+ console.error(`[note] ${note}`);
406
+ emit("deploy", result, `Deployed ${result.packageId}${result.openUrl ? "\nOpen URL: " + result.openUrl : ""}`);
407
407
  });
408
408
  program
409
409
  .command("status")
410
- .description("DSM 套件狀態、Open URL 與能力就緒狀態")
411
- .option("--json", "輸出單一 JSON 物件")
410
+ .description("DSM package status, Open URL and capability readiness")
411
+ .option("--json", "Print a single JSON object")
412
412
  .option("--host <name>", "NAS profile")
413
413
  .option("--dev", "Development Package Identity")
414
414
  .action(async (options, command) => {
@@ -426,7 +426,7 @@ export async function runCli(argv) {
426
426
  });
427
427
  program
428
428
  .command("logs")
429
- .description("狀態診斷:DSM 套件狀態與遮罩後的診斷資訊;不串流應用程式日誌")
429
+ .description("Status diagnosis: DSM package status and masked diagnostics; does not stream application logs")
430
430
  .requiredOption("--host <name>", "NAS profile")
431
431
  .option("--dev", "Development Package Identity")
432
432
  .action(async (options, command) => {
@@ -445,32 +445,32 @@ export async function runCli(argv) {
445
445
  dev: options.dev,
446
446
  });
447
447
  if (!result.ok)
448
- throw failure("NOT_INSTALLED", "套件尚未安裝。先執行 synopkgland deploy --host。");
448
+ throw failure("NOT_INSTALLED", "The package is not installed. Run synopkgland deploy --host first.");
449
449
  for (const line of result.lines)
450
450
  stdout(line);
451
451
  if (result.diagnostics)
452
452
  stdout(JSON.stringify(result.diagnostics, null, 2));
453
453
  });
454
- const skill = program.command("skill").description("內建 Agent Skill Pack");
454
+ const skill = program.command("skill").description("Bundled Agent Skill Pack");
455
455
  skill.command("path").action(() => stdout(resolveSkillsRoot()));
456
456
  skill.command("list").action(() => stdout(formatSkillList()));
457
457
  skill.command("show <id>").action((id) => stdout(readSkill(id)));
458
458
  program
459
459
  .command("build")
460
- .description("產生 SPK:synology-app.yaml 或 dsm-package.yaml")
461
- .option("--json", "輸出單一 JSON 物件")
462
- .option("-o, --output <path>", "輸出 SPK,搭配單一 --arch")
463
- .option("--out-dir <dir>", "輸出目錄,原生專案預設 dist/")
464
- .option("--arch <families>", `架構,可用逗號分隔或重複指定:${NATIVE_ARCH_FAMILIES.join(",")}(armv7 用於 native-go 與 binary,不用於 Container Manager App);native layout 另支援 armv8`, (value, previous) => [...(previous ?? []), value])
465
- .option("--min-dsm <version>", "最低 DSM 版本,省略時使用 manifest 值")
466
- .option("--jobs <n>", "同時建置的架構數,預設 1(依序)", Number)
467
- .option("--verify-reproducible [when]", "從乾淨狀態再建置一次並比對;when:changed(預設,依賴指紋未變時略過)或 always")
460
+ .description("Build an SPK from synology-app.yaml or dsm-package.yaml")
461
+ .option("--json", "Print a single JSON object")
462
+ .option("-o, --output <path>", "Output SPK; use with a single --arch")
463
+ .option("--out-dir <dir>", "Output directory; native projects default to dist/")
464
+ .option("--arch <families>", `Architectures, comma-separated or repeated: ${NATIVE_ARCH_FAMILIES.join(",")} (armv7 is for native-go and binary, not for Container Manager Apps); native layout also supports armv8`, (value, previous) => [...(previous ?? []), value])
465
+ .option("--min-dsm <version>", "Minimum DSM version; uses the manifest value when omitted")
466
+ .option("--jobs <n>", "Number of architectures to build in parallel, default 1 (sequential)", Number)
467
+ .option("--verify-reproducible [when]", "Build again from a clean state and compare; when: changed (default, skips when the dependency fingerprint is unchanged) or always")
468
468
  .action(async (options, command) => {
469
469
  const root = rootOf(command);
470
470
  const standalone = standaloneRoot(root);
471
471
  const verify = options.verifyReproducible === true ? "changed" : options.verifyReproducible;
472
472
  if (verify !== undefined && verify !== "changed" && verify !== "always")
473
- throw failure("CLI_OPTION_INVALID", `--verify-reproducible 只接受 changed 或 always,收到 ${verify}。`);
473
+ throw failure("CLI_OPTION_INVALID", `--verify-reproducible accepts only changed or always, got ${verify}.`);
474
474
  let arches;
475
475
  try {
476
476
  arches =
@@ -501,9 +501,9 @@ export async function runCli(argv) {
501
501
  ? build({ output: undefined, outDir })
502
502
  : build({ output: join(outDir, basename(outputs[0])), outDir: undefined }),
503
503
  });
504
- const lines = [...outputs.map((output) => `已產生 ${output}`), ...(reproducible ? reproducibleLines(reproducible) : [])];
504
+ const lines = [...outputs.map((output) => `Built ${output}`), ...(reproducible ? reproducibleLines(reproducible) : [])];
505
505
  if (reproducible && !reproducible.identical)
506
- throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "找出 payload 中每次建置都不同的檔案(時間戳、隨機路徑、未鎖定的依賴),修正後重試。"), { report: { outputs, reproducible } });
506
+ throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "Find the payload files that differ between builds (timestamps, random paths, unpinned dependencies) and retry after fixing them."), { report: { outputs, reproducible } });
507
507
  emit("build", { outputs, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
508
508
  });
509
509
  if (json)
@@ -532,7 +532,7 @@ export async function runCli(argv) {
532
532
  ok: false,
533
533
  code,
534
534
  message,
535
- hint: detail.hint ?? getErrorCode(code)?.suggestion ?? "修正設定後重試;使用 --help 查看指令用法。",
535
+ hint: detail.hint ?? getErrorCode(code)?.suggestion ?? "Fix the configuration and retry; use --help to see command usage.",
536
536
  ...(detail.report ? { data: detail.report } : {}),
537
537
  }));
538
538
  }