@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
@@ -320,17 +320,17 @@ function collectPermanentRouteErrors(contract, parsed, composeFile) {
320
320
  const basePath = `services.${serviceName}`;
321
321
  if ("build" in service && service.build !== undefined) {
322
322
  const path = `${basePath}.build`;
323
- errors.push(makeError("COMPOSE_BUILD_FORBIDDEN", composeSpanForPath(compose, composeFile, path), `service「${serviceName}」宣告 build:;D0 route 不支援 install-time build。`, "請移除 build:,改用已存在的 image(最好使用 image@sha256:... 固定 digest),再重新執行 check。"));
323
+ errors.push(makeError("COMPOSE_BUILD_FORBIDDEN", composeSpanForPath(compose, composeFile, path), `service "${serviceName}" declares build:; the D0 route does not support install-time build.`, "Remove build: and use an existing image (preferably pinned with image@sha256:...), then run check again."));
324
324
  }
325
325
  if (dsmUserParams.length > 0 && typeof service.user === "string" && service.user.trim() !== "") {
326
326
  const path = `${basePath}.user`;
327
- errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", composeSpanForPath(compose, composeFile, path), `service「${serviceName}」已定義 user:「${service.user.trim()}」,與 contract 宣告的 dsm_user 衝突。`, "請從 Compose 移除 user 欄位,由 generator 依 dsm_user 自動注入 user: \"${NAME_UID}:${NAME_GID}\" 與 group_add。"));
327
+ errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", composeSpanForPath(compose, composeFile, path), `service "${serviceName}" defines user: "${service.user.trim()}", which conflicts with the dsm_user declared in the contract.`, "Remove the user field from Compose; the generator injects user: \"${NAME_UID}:${NAME_GID}\" and group_add from dsm_user."));
328
328
  }
329
329
  errors.push(...validateSocketMounts(compose, service, composeFile, `${basePath}.volumes`));
330
330
  }
331
331
  }
332
332
  for (const finding of scanComposePrivilege(compose)) {
333
- errors.push(makeError(finding.code, composeSpanForPath(compose, composeFile, finding.path), `service「${finding.serviceName}」使用 ${finding.flag};D0 route 永久拒收高權限 capability,即使 contract 已宣告也不會放行。`, "請移除 privileged、host network/PID/IPC、devices 或 cap_add,改用 package-only static-zero-config Compose。"));
333
+ errors.push(makeError(finding.code, composeSpanForPath(compose, composeFile, finding.path), `service "${finding.serviceName}" uses ${finding.flag}; the D0 route permanently rejects high-privilege capabilities, even when the contract declares them.`, "Remove privileged, host network/PID/IPC, devices or cap_add, and use a package-only static-zero-config Compose."));
334
334
  }
335
335
  return errors;
336
336
  }
@@ -398,13 +398,13 @@ function collectDynamicRouteError(contract, parsed, composeFile) {
398
398
  ? unsupportedInterpolation
399
399
  : undefined;
400
400
  const interpolationSuggestion = undeclaredName
401
- ? `請在 synology-app.yaml 的 install 宣告 ${undeclaredName},type 為 string 或 boolean;或從 Compose 移除該引用。`
402
- : "請只使用 ${NAME} 形式,並在 synology-app.yaml 的 install 宣告對應的 string 或 boolean parameter。";
403
- return makeError("COMPOSE_DYNAMIC_ROUTE_UNSUPPORTED", composeSpanForPath(compose, composeFile, path), "Compose dynamic route 含有不在 D1/D2 白名單內的輸入(" +
401
+ ? `Declare ${undeclaredName} in the synology-app.yaml install with type string or boolean, or remove the reference from Compose.`
402
+ : "Use only the ${NAME} form and declare the matching string or boolean parameter in the synology-app.yaml install.";
403
+ return makeError("COMPOSE_DYNAMIC_ROUTE_UNSUPPORTED", composeSpanForPath(compose, composeFile, path), "The Compose dynamic route contains an input outside the D1/D2 allowlist (" +
404
404
  reasonText +
405
- ");這條 route 會拒收,不會靜默改用其他執行方式。", undeclaredName
405
+ "); this route is rejected and does not silently fall back to another execution mode.", undeclaredName
406
406
  ? interpolationSuggestion
407
- : `${interpolationSuggestion} secret/config 因 DSM worker 不讀 docker-compose.override.yml 而拒收,請移除後重新執行 check。`);
407
+ : `${interpolationSuggestion} secret/config are rejected because the DSM worker does not read docker-compose.override.yml; remove them and run check again.`);
408
408
  }
409
409
  function parsedComposeForAnalysis(compose) {
410
410
  const source = composeSources.get(compose) ?? "";
@@ -563,7 +563,7 @@ export function validateComposeSecurity(compose, composeFile, options) {
563
563
  const dependsOn = normalizeDependsOn(service.depends_on);
564
564
  for (const dependency of dependsOn) {
565
565
  if (!compose.services?.[dependency]) {
566
- errors.push(makeError("COMPOSE_SERVICE_NOT_FOUND", composeSpanForPath(compose, composeFile, `${basePath}.depends_on`), `Compose 引用了不存在的 service「${dependency}」。`, "請修正 depends_on,或補上對應的 service 定義。"));
566
+ errors.push(makeError("COMPOSE_SERVICE_NOT_FOUND", composeSpanForPath(compose, composeFile, `${basePath}.depends_on`), `Compose references a service that does not exist: "${dependency}".`, "Fix depends_on, or add the matching service definition."));
567
567
  }
568
568
  }
569
569
  }
@@ -592,8 +592,8 @@ export function validateComposeNumericCommandArgs(compose, composeFile) {
592
592
  if (numericStrings.length === 0) {
593
593
  continue;
594
594
  }
595
- const quotedValues = numericStrings.map((item) => `「${item}」`).join("、");
596
- errors.push(makeError("COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.${field}`), `${field} 陣列含純數字字串 ${quotedValues}。此 Compose 會走 DSM Docker Project resource worker;worker 可能在 YAML round-trip 時移除引號並將它轉成整數,導致 project acquire 失敗。`, `請把整個命令包成單一字串,例如 ${field}: ["sh", "-c", "httpd -f -p 8080 -h /srv"],或與 flag 併寫成 "-p8080"。`));
595
+ const quotedValues = numericStrings.map((item) => `"${item}"`).join(", ");
596
+ errors.push(makeError("COMPOSE_NUMERIC_COMMAND_ARG_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.${field}`), `The ${field} array contains the pure numeric string ${quotedValues}. This Compose goes through the DSM Docker Project resource worker; the worker may drop the quotes during a YAML round-trip and turn it into an integer, so project acquire fails.`, `Wrap the whole command in one string, for example ${field}: ["sh", "-c", "httpd -f -p 8080 -h /srv"], or join it with the flag as "-p8080".`));
597
597
  }
598
598
  }
599
599
  return errors;
@@ -616,7 +616,7 @@ function validateSocketMounts(compose, service, composeFile, basePath) {
616
616
  for (const mount of volumes) {
617
617
  const source = mount.split(":")[0]?.trim() ?? "";
618
618
  if (isForbiddenSocketMount(source)) {
619
- errors.push(makeError("COMPOSE_SOCKET_MOUNT_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.volumes`), "Managed Profile 不允許掛載 Docker 或 Container Manager socket。", "請移除 docker.sock/container-manager socket 的 volume mount。"));
619
+ errors.push(makeError("COMPOSE_SOCKET_MOUNT_FORBIDDEN", composeSpanForPath(compose, composeFile, `${basePath}.volumes`), "A Managed Profile does not allow mounting the Docker or Container Manager socket.", "Remove the docker.sock/container-manager socket volume mount."));
620
620
  }
621
621
  }
622
622
  return errors;
@@ -753,29 +753,29 @@ function classifyVolumeSource(compose, mount, namedVolumes, context, composeFile
753
753
  return null;
754
754
  }
755
755
  if (context.installParamNames.has(varName)) {
756
- return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, `參數「${varName}」不能作為 host bind source。`, "請改用 ${SYNOPKG_PKGVAR}、${SYNO_APP_SECRETS_DIR}、type 為 shared_folder 的 install 參數,或 type 為 homes 的平台解析槽。");
756
+ return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, `The parameter "${varName}" cannot be a host bind source.`, "Use ${SYNOPKG_PKGVAR}, ${SYNO_APP_SECRETS_DIR}, an install parameter with type shared_folder, or the platform-resolved type homes slot.");
757
757
  }
758
- return makeError("COMPOSE_BIND_SOURCE_UNDECLARED", span, `volume source 引用了未宣告的參數「${varName}」。`, `請在 synology-app.yaml 的 install 宣告 ${varName},且 type 為 shared_folder 或 homes。`);
758
+ return makeError("COMPOSE_BIND_SOURCE_UNDECLARED", span, `The volume source references an undeclared parameter "${varName}".`, `Declare ${varName} in the synology-app.yaml install with type shared_folder or homes.`);
759
759
  }
760
760
  if (isProjectRelativePath(source, projectRoot)) {
761
761
  return null;
762
762
  }
763
763
  if (isLiteralHostPath(source)) {
764
- return makeError("COMPOSE_HOST_BIND_FORBIDDEN", span, "禁止將 /volume*、絕對路徑或開發者機器路徑寫成 Compose volume source。", "請改用 install 參數 type: shared_folder 或 homes,並以 ${參數名} 作為 bind source。");
764
+ return makeError("COMPOSE_HOST_BIND_FORBIDDEN", span, "Writing /volume*, an absolute path or a developer machine path as a Compose volume source is forbidden.", "Use an install parameter with type: shared_folder or homes and take ${PARAM} as the bind source.");
765
765
  }
766
766
  if (mount.type === "tmpfs") {
767
767
  return null;
768
768
  }
769
769
  if (mount.type === "bind") {
770
- return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, "禁止使用 type: bind 掛載非平台允許的 host source。", "請改用 ${SYNOPKG_PKGVAR}、${SYNO_APP_SECRETS_DIR} 或契約中的 ${shared_folder 參數}。");
770
+ return makeError("COMPOSE_BIND_SOURCE_FORBIDDEN", span, "Mounting a host source the platform does not allow with type: bind is forbidden.", "Use ${SYNOPKG_PKGVAR}, ${SYNO_APP_SECRETS_DIR} or a ${shared_folder parameter} from the contract.");
771
771
  }
772
772
  if (!namedVolumes.has(source)) {
773
- return makeError("COMPOSE_NAMED_VOLUME_UNDECLARED", span, `短語法 named volume「${source}」未在頂層 volumes 宣告,無法判斷它是 Docker volume 還是 Share 名。`, `請在頂層 volumes 宣告 ${source},或改用 \${參數名} 作為 shared_folder bind source。`);
773
+ return makeError("COMPOSE_NAMED_VOLUME_UNDECLARED", span, `The short-syntax named volume "${source}" is not declared in the top-level volumes, so it is unclear whether it is a Docker volume or a Share name.`, `Declare ${source} in the top-level volumes, or use \${PARAM} as the shared_folder bind source.`);
774
774
  }
775
775
  return null;
776
776
  }
777
777
  function invalidBindSuffix(span) {
778
- return makeError("COMPOSE_BIND_SUFFIX_INVALID", span, "volume source 的路徑後綴不合法。", "後綴只能是 / 加上一段或多段 [A-Za-z0-9._-],不可含 ..、${ 或絕對路徑。");
778
+ return makeError("COMPOSE_BIND_SUFFIX_INVALID", span, "The path suffix of the volume source is invalid.", "The suffix can only be / followed by one or more [A-Za-z0-9._-] segments, and must not contain .., ${ or an absolute path.");
779
779
  }
780
780
  function isValidatedBindSuffix(suffix) {
781
781
  if (suffix === "") {
@@ -853,18 +853,18 @@ export function validateComposeRunAs(compose, composeFile, context) {
853
853
  const user = raw.trim();
854
854
  const span = composeSpanForPath(compose, composeFile, `services.${serviceName}.user`);
855
855
  if (context.hasDsmUser) {
856
- errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", span, `service「${serviceName}」已定義 user:「${user}」,與 contract 宣告的 dsm_user 衝突。`, "請從 Compose 移除 user 欄位,由 generator 依 dsm_user 自動注入 user: \"${NAME_UID}:${NAME_GID}\" 與 group_add。"));
856
+ errors.push(makeError("COMPOSE_USER_CONFLICTS_DSM_USER", span, `service "${serviceName}" defines user: "${user}", which conflicts with the dsm_user declared in the contract.`, "Remove the user field from Compose; the generator injects user: \"${NAME_UID}:${NAME_GID}\" and group_add from dsm_user."));
857
857
  continue;
858
858
  }
859
859
  if (user.includes("${")) {
860
860
  continue;
861
861
  }
862
862
  if (context.hasSharedFolder && ROOT_USERS.has(user)) {
863
- errors.push(makeError("COMPOSE_USER_ROOT_FORBIDDEN", span, "有 Shared Folder 時容器不可用 uid 0/root 寫入 Share。", "請宣告 dsm_user,並以 ${NAME_UID}:${NAME_GID} 當 Compose user。", "warning"));
863
+ errors.push(makeError("COMPOSE_USER_ROOT_FORBIDDEN", span, "With a Shared Folder, a container must not write to the Share as uid 0/root.", "Declare dsm_user and use ${NAME_UID}:${NAME_GID} as the Compose user.", "warning"));
864
864
  continue;
865
865
  }
866
866
  if (NUMERIC_USER.test(user)) {
867
- errors.push(makeError("COMPOSE_USER_NUMERIC_FORBIDDEN", span, "契約不准寫死數字 uid。", "請改用 install dsm_user 參數的 ${NAME_UID}:${NAME_GID}。"));
867
+ errors.push(makeError("COMPOSE_USER_NUMERIC_FORBIDDEN", span, "The contract forbids hard-coded numeric uids.", "Use ${NAME_UID}:${NAME_GID} from the install dsm_user parameter."));
868
868
  }
869
869
  }
870
870
  return errors;
@@ -873,14 +873,14 @@ export function validateComposeRunAs(compose, composeFile, context) {
873
873
  export function validateGatewayPortLoopback(adminport, compose, composeFile) {
874
874
  return listPortExposures(compose)
875
875
  .filter((exposure) => exposure.hostPort === adminport && !exposure.loopbackOnly)
876
- .map((exposure) => makeError("COMPOSE_GATEWAY_PORT_EXPOSED", composeSpanForPath(compose, composeFile, `services.${exposure.service}.ports`), `${exposure.service} 的 port ${exposure.entry} 綁在所有網路介面上,不經 DSM 驗證就能直接連到容器。`, `請改成只綁 loopback:\"127.0.0.1:${adminport}:${exposure.containerPort ?? adminport}\",或在 synology-app.yaml 的 integration.web.access 明確選 public。`));
876
+ .map((exposure) => makeError("COMPOSE_GATEWAY_PORT_EXPOSED", composeSpanForPath(compose, composeFile, `services.${exposure.service}.ports`), `The port ${exposure.entry} of ${exposure.service} binds to every network interface, so the container is reachable without DSM verification.`, `Bind it to loopback only: \"127.0.0.1:${adminport}:${exposure.containerPort ?? adminport}\", or choose public explicitly in integration.web.access in synology-app.yaml.`));
877
877
  }
878
878
  export function validateAdminPortMapping(adminport, compose, composeFile) {
879
879
  const published = collectPublishedTcpPorts(compose);
880
880
  const services = published.get(adminport) ?? [];
881
881
  if (services.length !== 1) {
882
882
  return [
883
- makeError("CONTRACT_ADMINPORT_UNMAPPED", composeSpanForPath(compose, composeFile, `adminport:${adminport}`), `adminport ${adminport} 必須對應 Compose 中恰好一個 published TCP port。`, "請在 compose.yaml 發佈與 adminport 相同的 host port,並確認只有一個 service 使用它。"),
883
+ makeError("CONTRACT_ADMINPORT_UNMAPPED", composeSpanForPath(compose, composeFile, `adminport:${adminport}`), `adminport ${adminport} must map to exactly one published TCP port in Compose.`, "Publish the same host port as adminport in compose.yaml and make sure only one service uses it."),
884
884
  ];
885
885
  }
886
886
  return [];
@@ -9,33 +9,33 @@ export const PRIVILEGE_FLAGS = [
9
9
  const FLAG_ERRORS = {
10
10
  privileged: {
11
11
  code: "COMPOSE_PRIVILEGED_FORBIDDEN",
12
- message: "Compose 使用 privileged 容器,契約未宣告 privilege.privileged。",
13
- suggestion: "請在 synology-app.yaml 加上 privilege.privileged: true,或移除 privileged: true。",
12
+ message: "Compose uses a privileged container but the contract does not declare privilege.privileged.",
13
+ suggestion: "Add privilege.privileged: true to synology-app.yaml, or remove privileged: true.",
14
14
  },
15
15
  host_network: {
16
16
  code: "COMPOSE_HOST_NETWORK_FORBIDDEN",
17
- message: "Compose 使用 host network,契約未宣告 privilege.host_network。",
18
- suggestion: "請在 synology-app.yaml 加上 privilege.host_network: true,或改用受控網路模式。",
17
+ message: "Compose uses host network but the contract does not declare privilege.host_network.",
18
+ suggestion: "Add privilege.host_network: true to synology-app.yaml, or use a controlled network mode.",
19
19
  },
20
20
  host_pid: {
21
21
  code: "COMPOSE_HOST_PID_FORBIDDEN",
22
- message: "Compose 使用 host PID,契約未宣告 privilege.host_pid。",
23
- suggestion: "請在 synology-app.yaml 加上 privilege.host_pid: true,或移除 pid: host。",
22
+ message: "Compose uses host PID but the contract does not declare privilege.host_pid.",
23
+ suggestion: "Add privilege.host_pid: true to synology-app.yaml, or remove pid: host.",
24
24
  },
25
25
  host_ipc: {
26
26
  code: "COMPOSE_HOST_IPC_FORBIDDEN",
27
- message: "Compose 使用 host IPC,契約未宣告 privilege.host_ipc。",
28
- suggestion: "請在 synology-app.yaml 加上 privilege.host_ipc: true,或移除 ipc: host。",
27
+ message: "Compose uses host IPC but the contract does not declare privilege.host_ipc.",
28
+ suggestion: "Add privilege.host_ipc: true to synology-app.yaml, or remove ipc: host.",
29
29
  },
30
30
  devices: {
31
31
  code: "COMPOSE_DEVICES_FORBIDDEN",
32
- message: "Compose 掛載 devices,契約未宣告 privilege.devices。",
33
- suggestion: "請在 synology-app.yaml 加上 privilege.devices: true,或移除 devices。",
32
+ message: "Compose mounts devices but the contract does not declare privilege.devices.",
33
+ suggestion: "Add privilege.devices: true to synology-app.yaml, or remove devices.",
34
34
  },
35
35
  cap_add: {
36
36
  code: "COMPOSE_CAP_ADD_FORBIDDEN",
37
- message: "Compose 使用額外 cap_add,契約未宣告 privilege.cap_add。",
38
- suggestion: "請在 synology-app.yaml 加上 privilege.cap_add: true,或移除 cap_add。",
37
+ message: "Compose uses extra cap_add but the contract does not declare privilege.cap_add.",
38
+ suggestion: "Add privilege.cap_add: true to synology-app.yaml, or remove cap_add.",
39
39
  },
40
40
  };
41
41
  export function declaredPrivilegeFlags(contract) {
@@ -28,12 +28,12 @@ export function validateContractI18n(parsed, contractFile) {
28
28
  const errors = [];
29
29
  const invalid = (why) => {
30
30
  const pos = getValuePosition(doc, ["i18n"]) ?? { line: 1, column: 1 };
31
- errors.push(makeError("CONTRACT_I18N_INVALID", contractSpan(contractFile, pos.line, pos.column, "i18n"), `i18n ${why}`, "i18n 只接受 info;info 以 zh-TW、ja 等支援語言為 key,值為單行、不含 \" 或 \\ 的 displayname / description(英文寫在頂層)。"));
31
+ errors.push(makeError("CONTRACT_I18N_INVALID", contractSpan(contractFile, pos.line, pos.column, "i18n"), `i18n ${why}`, "i18n accepts only info; info is keyed by a supported language such as zh-TW or ja with single-line displayname / description values without \" or \\ (English goes at the top level)."));
32
32
  return undefined;
33
33
  };
34
- const text = (value, field) => plain(value) ? value : invalid(`${field} 必須是單行、非空且不含 " 或 \\ 的文字。`);
34
+ const text = (value, field) => plain(value) ? value : invalid(`${field} must be single-line, non-empty text without " or \\.`);
35
35
  if (!isRecord(contract.i18n) || Object.keys(contract.i18n).some((key) => key !== "info")) {
36
- return invalid("只接受 info(各語言的 displayname 與 description)。") ?? errors;
36
+ return invalid("Only info is accepted (per-language displayname and description).") ?? errors;
37
37
  }
38
38
  const info = readInfoTranslations(contract.i18n.info, { invalid: (why) => invalid(`.info ${why}`), text });
39
39
  if (info && Object.keys(info).length > 0) {
@@ -11,11 +11,11 @@ export function validateContractIcon(parsed, contractFile) {
11
11
  if (contract.icon === undefined || !contractPath)
12
12
  return [];
13
13
  const pos = getValuePosition(doc, ["icon"]) ?? { line: 1, column: 1 };
14
- const invalid = (message) => [makeError("CONTRACT_ICON_INVALID", contractSpan(contractFile, pos.line, pos.column, "icon"), `icon:${message}`, "icon 填專案內的正方形 PNG(至少 256×256),或含 icon_256.png 的 icon_<size>.png 目錄;省略則使用 SDK 預設圖示。")];
14
+ const invalid = (message) => [makeError("CONTRACT_ICON_INVALID", contractSpan(contractFile, pos.line, pos.column, "icon"), `icon: ${message}`, "Set icon to a square PNG inside the project (at least 256x256), or a directory of icon_<size>.png including icon_256.png; omit it to use the SDK default icon.")];
15
15
  const root = dirname(contractPath);
16
16
  const rel = typeof contract.icon === "string" && contract.icon.trim() !== "" ? relative(root, resolve(root, contract.icon)) : undefined;
17
17
  if (rel === undefined || isAbsolute(String(contract.icon)) || rel.startsWith("..") || isAbsolute(rel)) {
18
- return invalid(`「${String(contract.icon)}」必須是位於專案目錄內的相對路徑。`);
18
+ return invalid(`"${String(contract.icon)}" must be a relative path inside the project directory.`);
19
19
  }
20
20
  try {
21
21
  renderPackageIcons(resolve(root, rel));
@@ -39,12 +39,12 @@ export function validatePosixAllSharesSources(compose, composeFile, declaredPara
39
39
  const path = `services.${serviceName}.volumes.${index}`;
40
40
  if (declaredParameters.has(name)) {
41
41
  if (suffix !== "") {
42
- errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_SUFFIX_FORBIDDEN", composeSpan(composeFile, 1, 1, path), `POSIX all-shares 參數「${name}」只能作為完整 volume source,不能附加子路徑。`, `請將 ${source} 改成 \${${name}},並保留 Compose 的 container target;Share 名稱由 package-only preinst 在安裝時展開。`));
42
+ errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_SUFFIX_FORBIDDEN", composeSpan(composeFile, 1, 1, path), `The POSIX all-shares parameter "${name}" can only be the complete volume source, with no subpath.`, `Change ${source} to \${${name}} and keep the Compose container target; the package-only preinst expands the Share names at install time.`));
43
43
  }
44
44
  continue;
45
45
  }
46
46
  if (RESERVED_MARKER_NAMES.has(name)) {
47
- errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_UNDECLARED", composeSpan(composeFile, 1, 1, path), `Compose 使用了未宣告的 POSIX all-shares marker「${name}」。`, `請在 synology-app.yaml 宣告 install.${name}.type: ${POSIX_ALL_SHARES_TYPE},並讓 wizard/CLI 取得安裝當下的 Share 清單。`));
47
+ errors.push(makeError("COMPOSE_POSIX_ALL_SHARES_UNDECLARED", composeSpan(composeFile, 1, 1, path), `Compose uses an undeclared POSIX all-shares marker "${name}".`, `Declare install.${name}.type: ${POSIX_ALL_SHARES_TYPE} in synology-app.yaml and let the wizard/CLI take the Share list at install time.`));
48
48
  }
49
49
  }
50
50
  }
@@ -20,14 +20,14 @@ function mapSchemaError(error, contractFile, parsed) {
20
20
  const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".") || "schema";
21
21
  const pos = getValuePosition(parsed.doc, path.split(".").filter(Boolean)) ?? { line: 1, column: 1 };
22
22
  if (error.keyword === "const" && path === "schema") {
23
- return makeError("CONTRACT_SCHEMA_UNSUPPORTED", contractSpan(contractFile, pos.line, pos.column, "schema"), "不支援的 App Contract schema 版本。", "請將 schema 設為整數 1。");
23
+ return makeError("CONTRACT_SCHEMA_UNSUPPORTED", contractSpan(contractFile, pos.line, pos.column, "schema"), "Unsupported App Contract schema version.", "Set schema to the integer 1.");
24
24
  }
25
25
  if (error.keyword === "required") {
26
26
  const missing = String(error.params.missingProperty);
27
27
  const missingPos = getValuePosition(parsed.doc, [missing]) ?? { line: 1, column: 1 };
28
- return makeError("CONTRACT_MISSING_FIELD", contractSpan(contractFile, missingPos.line, missingPos.column, missing), `缺少必填欄位「${missing}」。`, `請在 synology-app.yaml 補上 ${missing}。`);
28
+ return makeError("CONTRACT_MISSING_FIELD", contractSpan(contractFile, missingPos.line, missingPos.column, missing), `Missing required field "${missing}".`, `Add ${missing} to synology-app.yaml.`);
29
29
  }
30
- return makeError("CONTRACT_SCHEMA_INVALID", contractSpan(contractFile, pos.line, pos.column, path), `App Contract 不符合 JSON Schema:${error.message ?? error.keyword}。`, "請修正 synology-app.yaml 的欄位格式。");
30
+ return makeError("CONTRACT_SCHEMA_INVALID", contractSpan(contractFile, pos.line, pos.column, path), `App Contract does not match the JSON Schema: ${error.message ?? error.keyword}.`, "Fix the field formats in synology-app.yaml.");
31
31
  }
32
32
  export function validateContractSchema(parsed, contractFile) {
33
33
  const validate = getValidator();
@@ -37,18 +37,18 @@ export function validateContract(parsed, contractFile) {
37
37
  // does not.
38
38
  if (typeof contract.package === "string" && contract.package.length > DSM_PACKAGE_IDENTITY_MAX) {
39
39
  const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
40
- errors.push(makeError("CONTRACT_PACKAGE_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `Package Identity 超過 DSM 上限 ${DSM_PACKAGE_IDENTITY_MAX} 字元(目前 ${contract.package.length})。`, "請縮短 package,否則 Package Center 會以「Invalid file format」拒絕安裝。"));
40
+ errors.push(makeError("CONTRACT_PACKAGE_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `The Package Identity exceeds the DSM limit of ${DSM_PACKAGE_IDENTITY_MAX} characters (currently ${contract.package.length}).`, "Shorten package, or Package Center rejects the install with \"Invalid file format\"."));
41
41
  }
42
42
  // Formal Package Identity length limit (28 characters to reserve 4 characters for .dev suffix)
43
43
  if (typeof contract.package === "string" &&
44
44
  contract.package.length > FORMAL_PACKAGE_IDENTITY_MAX_LENGTH) {
45
45
  const pos = getValuePosition(doc, ["package"]) ?? { line: 1, column: 1 };
46
- errors.push(makeError("PACKAGE_IDENTITY_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `Package Identity 長度 ${contract.package.length} 超過正式身分上限 ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} 字元(DSM 32 字元上限,.dev 需保留 4 字元)。`, `請將 package 縮短至 ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} 字元以內,以利 --dev 部署(上限 ${PACKAGE_IDENTITY_MAX_LENGTH} 字元)。`));
46
+ errors.push(makeError("PACKAGE_IDENTITY_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, "package"), `The Package Identity length ${contract.package.length} exceeds the formal identity limit of ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} characters (the DSM limit is 32, and .dev needs 4 reserved).`, `Shorten package to ${FORMAL_PACKAGE_IDENTITY_MAX_LENGTH} characters or fewer for --dev deploys (limit ${PACKAGE_IDENTITY_MAX_LENGTH} characters).`));
47
47
  }
48
48
  if (contract.adminport !== undefined) {
49
49
  if (typeof contract.adminport !== "number" || !Number.isInteger(contract.adminport)) {
50
50
  const pos = getValuePosition(doc, ["adminport"]) ?? { line: 1, column: 1 };
51
- errors.push(makeError("CONTRACT_ADMINPORT_INVALID", contractSpan(contractFile, pos.line, pos.column, "adminport"), "adminport 必須是整數。", "請提供與 Compose published port 對應的數字 adminport。"));
51
+ errors.push(makeError("CONTRACT_ADMINPORT_INVALID", contractSpan(contractFile, pos.line, pos.column, "adminport"), "adminport must be an integer.", "Provide a numeric adminport that matches the Compose published port."));
52
52
  }
53
53
  }
54
54
  errors.push(...validateInstallParameters(parsed, contractFile));
@@ -85,11 +85,11 @@ function validateWebLaunch(parsed, contractFile) {
85
85
  }
86
86
  const pos = getValuePosition(doc, ["integration", "web", "launch"]) ?? { line: 1, column: 1 };
87
87
  if (!isWebLaunchMode(web.launch)) {
88
- errors.push(makeError("CONTRACT_LAUNCH_INVALID", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "integration.web.launch 只能是 dsm-window 或 new-page。", "請改為 dsm-window(DSM 視窗,預設)或 new-page(新分頁),Headless App 請省略此欄位。"));
88
+ errors.push(makeError("CONTRACT_LAUNCH_INVALID", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "integration.web.launch can only be dsm-window or new-page.", "Use dsm-window (DSM window, the default) or new-page (new tab); omit this field for a headless App."));
89
89
  return errors;
90
90
  }
91
91
  if (contract.adminport === undefined) {
92
- errors.push(makeError("CONTRACT_LAUNCH_REQUIRES_WEB", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "Headless App 不需要 Launch Mode。", "請省略 integration.web.launch,或補上對應 Compose published port 的 adminport。"));
92
+ errors.push(makeError("CONTRACT_LAUNCH_REQUIRES_WEB", contractSpan(contractFile, pos.line, pos.column, "integration.web.launch"), "A headless App does not need a Launch Mode.", "Remove integration.web.launch, or add the adminport that matches the Compose published port."));
93
93
  }
94
94
  return errors;
95
95
  }
@@ -105,12 +105,12 @@ function validateWebAccess(parsed, contractFile) {
105
105
  };
106
106
  if (contract.adminport === undefined) {
107
107
  return [
108
- makeError("CONTRACT_ACCESS_REQUIRES_WEB", at(web.access !== undefined ? "access" : "groups"), "Headless App 沒有網頁入口,不需要 access 或 groups。", "請省略 integration.web.access/groups,或補上對應 Compose published port 的 adminport。"),
108
+ makeError("CONTRACT_ACCESS_REQUIRES_WEB", at(web.access !== undefined ? "access" : "groups"), "A headless App has no web entry, so it does not need access or groups.", "Remove integration.web.access/groups, or add the adminport that matches the Compose published port."),
109
109
  ];
110
110
  }
111
111
  if (web.access !== undefined && !isWebAccessMode(web.access)) {
112
112
  return [
113
- makeError("CONTRACT_ACCESS_INVALID", at("access"), "integration.web.access 只能是 dsm-users、dsm-admins、dsm-groups 或 public。", "請改為 dsm-users(已登入的 DSM 使用者,預設)、dsm-admins、dsm-groups(需搭配 groups),或明確選 public 以保留匿名 Web Station 入口。"),
113
+ makeError("CONTRACT_ACCESS_INVALID", at("access"), "integration.web.access can only be dsm-users, dsm-admins, dsm-groups or public.", "Use dsm-users (signed-in DSM users, the default), dsm-admins, dsm-groups (with groups), or public to keep an anonymous Web Station entry."),
114
114
  ];
115
115
  }
116
116
  const groups = web.groups;
@@ -119,7 +119,7 @@ function validateWebAccess(parsed, contractFile) {
119
119
  groups.every((g) => typeof g === "string" && DSM_GROUP_NAME.test(g));
120
120
  if (web.access === "dsm-groups" ? !groupsValid : groups !== undefined) {
121
121
  return [
122
- makeError("CONTRACT_ACCESS_GROUPS_INVALID", at("groups"), "integration.web.groups 必須搭配 access: dsm-groups,且是 DSM 本機群組或完整 DOMAIN\\group/group@domain 名稱(網域最長 64、群組最長 128 字元)的非空清單。", "請改用 access: dsm-groups 並列出群組名稱,或移除 groups。"),
122
+ makeError("CONTRACT_ACCESS_GROUPS_INVALID", at("groups"), "integration.web.groups needs access: dsm-groups and must be a non-empty list of DSM local groups or full DOMAIN\\group/group@domain names (domain up to 64, group up to 128 characters).", "Use access: dsm-groups and list the group names, or remove groups."),
123
123
  ];
124
124
  }
125
125
  return [];
@@ -133,17 +133,17 @@ function validateWebPortal(parsed, contractFile) {
133
133
  const pos = getValuePosition(doc, ["integration", "web", "portal"]) ?? { line: 1, column: 1 };
134
134
  // A Compose App has no root executor, and only its purge can stop nginx serving the alias after uninstall.
135
135
  return [
136
- makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "Compose App 沒有 executor,不能宣告 Application Portal:解除安裝後 DSM 不會 reload nginx,別名要 root 才能清除。", "請移除 integration.web.portal;需要登入入口別名時,由管理員在 DSM 手動設定反向代理。"),
136
+ makeError("CONTRACT_PORTAL_REQUIRES_EXECUTOR", contractSpan(contractFile, pos.line, pos.column, "integration.web.portal"), "A Compose App has no executor and cannot declare an Application Portal: after uninstall DSM does not reload nginx, and only root can remove the alias.", "Remove integration.web.portal; for a sign-in alias, ask an administrator to configure a reverse proxy in DSM."),
137
137
  ];
138
138
  }
139
139
  function validateDeveloperString(value, contractFile, path, pos) {
140
140
  const errors = [];
141
141
  const violations = stringViolations(value);
142
142
  if (violations.hasControlChars) {
143
- errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "合約字串含有不允許的控制字元。", "請移除控制字元後再驗證。"));
143
+ errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "The contract string contains a disallowed control character.", "Remove the control character and validate again."));
144
144
  }
145
145
  if (violations.hasInterpolation) {
146
- errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "合約字串不可包含 ${ 插值語法。", "請改為固定字面值,安裝參數請使用 install 區塊宣告。"));
146
+ errors.push(makeError("CONTRACT_INVALID_STRING", contractSpan(contractFile, pos.line, pos.column, path), "A contract string must not contain ${ interpolation.", "Use a fixed literal value and declare install parameters in the install block."));
147
147
  }
148
148
  return errors;
149
149
  }
@@ -157,10 +157,10 @@ function validateInstallParameters(parsed, contractFile) {
157
157
  const pos = getValuePosition(doc, ["install", name, "default"]) ?? { line: 1, column: 1 };
158
158
  const violations = stringViolations(param.default);
159
159
  if (violations.tooLong) {
160
- errors.push(makeError("INSTALL_STRING_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter「${name}」的預設值超過 256 字元。`, "請縮短字串預設值。"));
160
+ errors.push(makeError("INSTALL_STRING_TOO_LONG", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `The Install Parameter "${name}" default exceeds 256 characters.`, "Shorten the string default."));
161
161
  }
162
162
  if (violations.hasControlChars || violations.hasInterpolation) {
163
- errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter「${name}」的字串值不合法。`, "請移除控制字元與 ${ 插值語法。"));
163
+ errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `The Install Parameter "${name}" string value is invalid.`, "Remove control characters and ${ interpolation."));
164
164
  }
165
165
  }
166
166
  // A45/A46: both types are in the schema enum but neither has a working
@@ -181,7 +181,7 @@ function validateInstallParameters(parsed, contractFile) {
181
181
  // script generation -- turning those red would cost real coverage for no
182
182
  // extra safety. Nothing unshippable can reach a NAS or the community repo,
183
183
  // because both gates refuse; the author still learns at validate time.
184
- errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `${basePath}.type`), `Install Parameter「${name}」的 type: ${type} 目前沒有可用的安裝路徑,經 Package Center 安裝會以 DSM broken/150 結束;deploy 與 publish 會拒絕。`, entry?.suggestion ?? "請改用其他 install parameter type。", "warning"));
184
+ errors.push(makeError(code, contractSpan(contractFile, pos.line, pos.column, `${basePath}.type`), `Install Parameter "${name}" with type: ${type} has no usable install path today: a Package Center install ends in DSM broken/150, and deploy and publish refuse it.`, entry?.suggestion ?? "Use another install parameter type.", "warning"));
185
185
  // Deliberately no `continue`: the type-specific checks below still apply.
186
186
  // Skipping them once silently dropped CONTRACT_HOMES_PATH_FORBIDDEN.
187
187
  }
@@ -195,7 +195,7 @@ function validateInstallParameters(parsed, contractFile) {
195
195
  const pos = getValuePosition(doc, ["install", name, field]) ??
196
196
  getValuePosition(doc, ["install", name]) ??
197
197
  { line: 1, column: 1 };
198
- errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `homes Install Parameter「${name}」不得在契約欄位「${field}」指定 NAS 實體路徑或預設值。`, `請保留 install.${name}.type: homes,Compose 只引用 \${${name}};由套件 在安裝時依這台 NAS 的 User Home 設定解析路徑。`));
198
+ errors.push(makeError("CONTRACT_HOMES_PATH_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `The homes Install Parameter "${name}" must not set a NAS physical path or default in the contract field "${field}".`, `Keep install.${name}.type: homes and reference only \${${name}} in Compose; the package resolves the path from this NAS's User Home setting at install time.`));
199
199
  }
200
200
  if (isPosixAllSharesType(type)) {
201
201
  const configuredValue = Object.entries(param).find(([key, value]) => key !== "type" && (key === "default" || value !== undefined));
@@ -204,7 +204,7 @@ function validateInstallParameters(parsed, contractFile) {
204
204
  const pos = getValuePosition(doc, ["install", name, field]) ??
205
205
  getValuePosition(doc, ["install", name]) ??
206
206
  { line: 1, column: 1 };
207
- errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `POSIX all-shares Install Parameter「${name}」不得指定 default 或 NAS 實體路徑;Share 清單只在安裝時由 CLI 透過 DSM API 取得快照。`, `請只保留 install.${name}.type: posix_all_shares,Compose 使用 \${${name}} 作為完整 volume source。`));
207
+ errors.push(makeError("CONTRACT_POSIX_ALL_SHARES_VALUE_FORBIDDEN", contractSpan(contractFile, pos.line, pos.column, `${basePath}.${field}`), `The POSIX all-shares Install Parameter "${name}" must not set a default or NAS physical path; the Share list is snapshotted by the CLI through the DSM API only at install time.`, `Keep only install.${name}.type: posix_all_shares and use \${${name}} in Compose as the complete volume source.`));
208
208
  }
209
209
  }
210
210
  }
@@ -1,3 +1,4 @@
1
+ import { type PackageIcons } from "../build-spk/dsm-icons.js";
1
2
  import type { AppContract } from "../types.js";
2
3
  /** DSM Web Station portal alias max length (error 1311 above this). */
3
4
  export declare const WEB_STATION_ALIAS_MAX = 20;
@@ -44,4 +45,4 @@ export declare function cookieStripHeaders(contract: AppContract): Array<{
44
45
  export declare function webStationServiceName(alias: string): string;
45
46
  export declare function webStationServiceRoot(alias: string): string;
46
47
  export declare function renderWebStationResource(contract: AppContract): string;
47
- export declare function renderDsmWindowConfig(contract: AppContract): string;
48
+ export declare function renderDsmWindowConfig(contract: AppContract, icons: PackageIcons): string;
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { uiTitleFields } from "../build-spk/ui-texts.js";
3
+ import { windowIconTemplate } from "../build-spk/dsm-icons.js";
3
4
  import { contractAppTitle } from "./i18n.js";
4
5
  /** DSM Web Station portal alias max length (error 1311 above this). */
5
6
  export const WEB_STATION_ALIAS_MAX = 20;
@@ -139,7 +140,7 @@ export function renderWebStationResource(contract) {
139
140
  }
140
141
  return `${JSON.stringify({ webservice: { services: [service], portals: [portal] } }, null, 2)}\n`;
141
142
  }
142
- export function renderDsmWindowConfig(contract) {
143
+ export function renderDsmWindowConfig(contract, icons) {
143
144
  const packageId = String(contract.package);
144
145
  return `${JSON.stringify({
145
146
  ".url": {
@@ -147,7 +148,7 @@ export function renderDsmWindowConfig(contract) {
147
148
  type: "url",
148
149
  allUsers: true,
149
150
  ...uiTitleFields(contractAppTitle(contract)),
150
- icon: "images/app_{0}.png",
151
+ icon: windowIconTemplate(icons),
151
152
  url: trustedAppEntryPath(contract),
152
153
  },
153
154
  },
@@ -25,11 +25,11 @@ export async function deployProject(options) {
25
25
  return {
26
26
  ok: false,
27
27
  code: first.code,
28
- message: `Install Parameter「${first.name}」的 type: ${first.type} 目前沒有可用的安裝路徑,` +
29
- "Package Center 安裝會以 DSM broken/150 結束,因此 deploy 拒絕部署。" +
28
+ message: `The Install Parameter "${first.name}" with type: ${first.type} has no usable install path today, ` +
29
+ "a Package Center install ends in DSM broken/150, so deploy refuses to deploy." +
30
30
  (first.type === "homes"
31
- ? "若需要管理員選定的資料夾,請改用 type: shared_folder。"
32
- : "請改用 type: string,或讓 App 自行從 ${SYNO_APP_SECRETS_DIR} 讀檔。"),
31
+ ? "If you need an administrator-chosen folder, use type: shared_folder."
32
+ : "Use type: string, or have the App read the file from ${SYNO_APP_SECRETS_DIR}."),
33
33
  };
34
34
  }
35
35
  const validation = validateProject(options.project);
@@ -41,7 +41,7 @@ export async function deployProject(options) {
41
41
  return {
42
42
  ok: false,
43
43
  code: "PACKAGE_IDENTITY_TOO_LONG",
44
- message: `最終 Package Identity「${identity.packageId}」長度 ${identity.packageId.length} 超過 DSM 上限 ${PACKAGE_IDENTITY_MAX_LENGTH} 字元。`,
44
+ message: `The final Package Identity "${identity.packageId}" length ${identity.packageId.length} exceeds the DSM limit of ${PACKAGE_IDENTITY_MAX_LENGTH} characters.`,
45
45
  };
46
46
  }
47
47
  const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
@@ -66,7 +66,7 @@ export async function deployProject(options) {
66
66
  return {
67
67
  ok: false,
68
68
  code: POSIX_ALL_SHARES_LOOKUP_FAILED,
69
- message: `無法用管理員 session 列出這台 NAS 的 Shared Folder,無法填入 POSIX all-shares 參數「${name}」。`,
69
+ message: `The Shared Folders of this NAS could not be listed with an administrator session, so the POSIX all-shares parameter "${name}" cannot be filled.`,
70
70
  };
71
71
  }
72
72
  paramValues[name] = names.join(",");
@@ -171,7 +171,7 @@ export async function deployProject(options) {
171
171
  return {
172
172
  ok: false,
173
173
  code: "NATIVE_ARCH_UNDETERMINED",
174
- message: "無法從這台 NAS 判定 CPU 架構。請用 --arch x86_64 或 --arch aarch64 明確指定。",
174
+ message: "The CPU architecture could not be determined from this NAS. Specify --arch x86_64 or --arch aarch64 explicitly.",
175
175
  };
176
176
  }
177
177
  }
@@ -179,7 +179,7 @@ export async function deployProject(options) {
179
179
  return {
180
180
  ok: false,
181
181
  code: "CONTAINER_ARCH_UNSUPPORTED",
182
- message: `Container Manager App 只支援 ${CONTAINER_ARCH_FAMILIES.join("、")};DSM 不為 ${archFamily} 提供 Container Manager。`,
182
+ message: `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${archFamily}.`,
183
183
  };
184
184
  }
185
185
  const build = await buildSpk({
@@ -19,7 +19,7 @@ export async function resolveHomesExtraValues(input) {
19
19
  return {
20
20
  ok: false,
21
21
  code: "HOMES_NOT_ENABLED",
22
- message: "這台 NAS 未啟用 DSM User Home,或 DSM API 無法解析 user home 路徑;無法安裝需要 homes 的 App。請在 DSM 啟用 User Home 後重試。",
22
+ message: "This NAS has DSM User Home disabled, or the DSM API cannot resolve the user home path; an App that needs homes cannot be installed. Enable User Home in DSM and retry.",
23
23
  };
24
24
  }
25
25
  return {
@@ -1,7 +1,7 @@
1
1
  export const PACKAGE_IDENTITY_MAX_LENGTH = 32;
2
2
  export const FORMAL_PACKAGE_IDENTITY_MAX_LENGTH = 28;
3
3
  const DEV_SUFFIX = ".dev";
4
- const DEV_DISPLAY_SUFFIX = "(開發中)";
4
+ const DEV_DISPLAY_SUFFIX = " (Dev)";
5
5
  export function developmentPackageIdentity(packageIdentity) {
6
6
  return `${packageIdentity}${DEV_SUFFIX}`;
7
7
  }
@@ -1,4 +1,4 @@
1
- const USAGE = "synopkgland deploy --param delegate_share=<shared folder>,或 --param delegate_home=true";
1
+ const USAGE = "synopkgland deploy --param delegate_share=<shared folder>, or --param delegate_home=true";
2
2
  /** Mirrors the preinst draft check in build-spk/delegate-scope.ts. */
3
3
  function validShareName(name) {
4
4
  return (name !== "" && name !== "." && !name.includes("..") &&
@@ -17,7 +17,7 @@ export function resolveInstallParameterExtraValues(input) {
17
17
  return {
18
18
  ok: false,
19
19
  code: "DEPLOY_PARAM_UNDECLARED",
20
- message: `--param 提供了未宣告的 Install Parameter「${name}」。只接受 synology-app.yaml install 中宣告的 string/boolean 名稱。`,
20
+ message: `--param provided an undeclared Install Parameter "${name}". Only string/boolean names declared in synology-app.yaml install are accepted.`,
21
21
  };
22
22
  }
23
23
  if (param.type === "boolean") {
@@ -25,7 +25,7 @@ export function resolveInstallParameterExtraValues(input) {
25
25
  return {
26
26
  ok: false,
27
27
  code: "DEPLOY_PARAM_TYPE_INVALID",
28
- message: `--param ${name}=... 必須是 boolean 的 true 或 false;DSM multiselect 會以這兩個字串交給 Installation/install。`,
28
+ message: `--param ${name}=... must be the boolean true or false; DSM multiselect passes these two strings to Installation/install.`,
29
29
  };
30
30
  }
31
31
  extraValues[name] = raw;
@@ -35,7 +35,7 @@ export function resolveInstallParameterExtraValues(input) {
35
35
  return {
36
36
  ok: false,
37
37
  code: "DEPLOY_PARAM_TYPE_INVALID",
38
- message: `--param ${name}=... 對應的契約型別是 ${String(param.type ?? "未指定")};deploy --param 只接受 string/boolean。`,
38
+ message: `--param ${name}=... maps to contract type ${String(param.type ?? "unspecified")}; deploy --param accepts only string/boolean.`,
39
39
  };
40
40
  }
41
41
  const violations = stringViolations(raw);
@@ -43,7 +43,7 @@ export function resolveInstallParameterExtraValues(input) {
43
43
  return {
44
44
  ok: false,
45
45
  code: "DEPLOY_PARAM_TYPE_INVALID",
46
- message: `--param ${name}=... 不符合 string Install Parameter 約束(不可空白、控制字元、\${ 插值或超過 256 字元)。`,
46
+ message: `--param ${name}=... does not meet the string Install Parameter rules (no blanks, control characters, \${ interpolation or more than 256 characters).`,
47
47
  };
48
48
  }
49
49
  extraValues[name] = raw;
@@ -3,7 +3,7 @@ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
3
3
  import type { NasHost } from "../host/types.js";
4
4
  export interface DeploymentPlan {
5
5
  root: string;
6
- kind: "native-go" | "native-rust" | "binary" | "native-layout" | "container";
6
+ kind: "native-go" | "native-python" | "native-node" | "native-rust" | "binary" | "native-layout" | "container";
7
7
  packageId: string;
8
8
  version: string;
9
9
  arch?: NativeArchFamily;
@@ -32,11 +32,11 @@ export async function runPortPreflight(input) {
32
32
  if (!conflict) {
33
33
  return { ok: true };
34
34
  }
35
- const occupants = conflict.occupants.map(formatPortOccupant).join("、");
35
+ const occupants = conflict.occupants.map(formatPortOccupant).join(", ");
36
36
  return {
37
37
  ok: false,
38
38
  code: PORT_ALREADY_IN_USE,
39
- message: `Published port ${conflict.port} 已被 ${occupants} 佔用(Compose service:${conflict.services.join("、")})。` +
40
- "請改用未被其他服務使用的 host port;同一 Package Identity 的既有容器會在升級/重裝時放行。",
39
+ message: `Published port ${conflict.port} is already taken by ${occupants} (Compose services: ${conflict.services.join(", ")}).` +
40
+ "Use a host port no other service uses; existing containers of the same Package Identity are allowed during upgrade/reinstall.",
41
41
  };
42
42
  }