@synopackageland/cli 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +20 -9
  2. package/dist/bin.js +1 -2
  3. package/dist/build-spk/generator.d.ts +0 -3
  4. package/dist/build-spk/generator.js +135 -287
  5. package/dist/build-spk/native-lifecycle.js +0 -3
  6. package/dist/build-spk/native-payload.js +0 -1
  7. package/dist/cli.js +171 -151
  8. package/dist/compose/analyze.d.ts +1 -2
  9. package/dist/compose/analyze.js +5 -48
  10. package/dist/contract/unsupported-types.d.ts +10 -0
  11. package/dist/contract/unsupported-types.js +49 -0
  12. package/dist/contract/validate.js +25 -2
  13. package/dist/contract/web-launch.js +0 -2
  14. package/dist/deploy/deploy.d.ts +1 -4
  15. package/dist/deploy/deploy.js +57 -156
  16. package/dist/deploy/homes-values.js +1 -1
  17. package/dist/deploy/plan.d.ts +18 -0
  18. package/dist/deploy/plan.js +54 -0
  19. package/dist/deploy/port-preflight.d.ts +21 -0
  20. package/dist/deploy/port-preflight.js +42 -0
  21. package/dist/deploy/spk-version.d.ts +2 -14
  22. package/dist/deploy/spk-version.js +25 -111
  23. package/dist/dev.d.ts +1 -3
  24. package/dist/dev.js +14 -28
  25. package/dist/error-registry.js +810 -857
  26. package/dist/host/dsm-client.js +23 -263
  27. package/dist/host/dsm-port-profile.d.ts +47 -0
  28. package/dist/host/dsm-port-profile.js +206 -0
  29. package/dist/host/port-info-lookup.d.ts +9 -0
  30. package/dist/host/port-info-lookup.js +87 -0
  31. package/dist/host/port-info.d.ts +33 -0
  32. package/dist/host/port-info.js +66 -0
  33. package/dist/host/types.d.ts +6 -78
  34. package/dist/index.d.ts +0 -9
  35. package/dist/index.js +0 -6
  36. package/dist/info.d.ts +0 -1
  37. package/dist/info.js +3 -10
  38. package/dist/init.d.ts +11 -1
  39. package/dist/init.js +112 -4
  40. package/dist/log.js +2 -3
  41. package/dist/native/validate.js +0 -1
  42. package/dist/standalone/binary.d.ts +9 -0
  43. package/dist/standalone/binary.js +67 -0
  44. package/dist/standalone/build.d.ts +25 -0
  45. package/dist/standalone/build.js +108 -0
  46. package/dist/standalone/dsm-window.d.ts +26 -0
  47. package/dist/standalone/dsm-window.js +251 -0
  48. package/dist/standalone/helper.d.ts +31 -0
  49. package/dist/standalone/helper.js +104 -0
  50. package/dist/standalone/i18n.d.ts +28 -0
  51. package/dist/standalone/i18n.js +161 -0
  52. package/dist/standalone/inspect-spk.d.ts +21 -0
  53. package/dist/standalone/inspect-spk.js +160 -0
  54. package/dist/standalone/manifest.d.ts +71 -0
  55. package/dist/standalone/manifest.js +276 -0
  56. package/dist/standalone/root-messages.d.ts +7 -0
  57. package/dist/standalone/root-messages.js +250 -0
  58. package/dist/standalone/root-ops.d.ts +42 -0
  59. package/dist/standalone/root-ops.js +101 -0
  60. package/dist/standalone/root-window.d.ts +35 -0
  61. package/dist/standalone/root-window.js +290 -0
  62. package/dist/standalone/spk.d.ts +22 -0
  63. package/dist/standalone/spk.js +205 -0
  64. package/dist/templates/binary/synology-app.yaml +7 -0
  65. package/dist/templates/hello-files/compose.yaml +0 -1
  66. package/dist/templates/hello-web/compose.yaml +13 -10
  67. package/dist/templates/hello-web/html/app.js +1 -23
  68. package/dist/templates/hello-web/html/index.html +1 -3
  69. package/dist/templates/hello-web-page/compose.yaml +13 -10
  70. package/dist/templates/hello-web-page/html/app.js +1 -23
  71. package/dist/templates/hello-web-page/html/index.html +1 -3
  72. package/dist/templates/native-go/README.md +268 -0
  73. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  74. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  75. package/dist/templates/native-go/backend/go.mod +3 -0
  76. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  77. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  78. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  79. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  80. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  81. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  82. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  83. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  84. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  85. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  86. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  87. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  88. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  89. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  90. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  91. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  92. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  93. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  94. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  95. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  96. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  97. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  98. package/dist/templates/native-go/dsm-package.yaml +58 -0
  99. package/dist/templates/native-go/gitignore +1 -0
  100. package/dist/templates/native-go/i18n/glossary.json +662 -0
  101. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  104. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  105. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  106. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  107. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  108. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  109. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  110. package/dist/templates/native-go/ui/app.css +35 -0
  111. package/dist/templates/native-go/ui/app.js +94 -0
  112. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  113. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  114. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  115. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  116. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  117. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  118. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  119. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  120. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  121. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  122. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  123. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  124. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  125. package/dist/templates/native-go-vue/README.md +110 -0
  126. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  127. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  128. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  129. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  130. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  131. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  132. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  133. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  134. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  135. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  136. package/dist/templates/native-go-vue/gitignore +2 -0
  137. package/dist/types.d.ts +1 -1
  138. package/dist/validate.js +11 -7
  139. package/package.json +3 -7
  140. package/schema/synology-app.schema.json +36 -37
  141. package/skills/SKILL.md +4 -48
  142. package/skills/reference/create.md +11 -83
  143. package/skills/reference/deploy.md +7 -78
  144. package/skills/reference/diagnose.md +4 -101
  145. package/skills/reference/files.md +5 -41
  146. package/skills/reference/identity.md +3 -29
  147. package/skills/reference/wrap.md +0 -2
  148. package/templates/binary/synology-app.yaml +7 -0
  149. package/templates/hello-files/compose.yaml +0 -1
  150. package/templates/hello-web/compose.yaml +13 -10
  151. package/templates/hello-web/html/app.js +1 -23
  152. package/templates/hello-web/html/index.html +1 -3
  153. package/templates/hello-web-page/compose.yaml +13 -10
  154. package/templates/hello-web-page/html/app.js +1 -23
  155. package/templates/hello-web-page/html/index.html +1 -3
  156. package/templates/native-go/README.md +268 -0
  157. package/templates/native-go/backend/cmd/app/main.go +49 -0
  158. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  159. package/templates/native-go/backend/go.mod +3 -0
  160. package/templates/native-go/backend/internal/api/errors.go +33 -0
  161. package/templates/native-go/backend/internal/api/registry.go +94 -0
  162. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  163. package/templates/native-go/backend/internal/api/server.go +155 -0
  164. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  165. package/templates/native-go/backend/internal/app/routes.go +127 -0
  166. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  167. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  168. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  169. package/templates/native-go/backend/internal/helper/control.go +219 -0
  170. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  171. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  172. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  173. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  174. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  175. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  176. package/templates/native-go/backend/internal/helper/status.go +271 -0
  177. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  178. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  179. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  180. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  181. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  182. package/templates/native-go/dsm-package.yaml +58 -0
  183. package/templates/native-go/gitignore +1 -0
  184. package/templates/native-go/i18n/glossary.json +662 -0
  185. package/templates/native-go/i18n/messages/de.json +28 -0
  186. package/templates/native-go/i18n/messages/en.json +28 -0
  187. package/templates/native-go/i18n/messages/es.json +28 -0
  188. package/templates/native-go/i18n/messages/fr.json +28 -0
  189. package/templates/native-go/i18n/messages/it.json +28 -0
  190. package/templates/native-go/i18n/messages/ja.json +28 -0
  191. package/templates/native-go/i18n/messages/ko.json +28 -0
  192. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  193. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  194. package/templates/native-go/ui/app.css +35 -0
  195. package/templates/native-go/ui/app.js +94 -0
  196. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  197. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  198. package/templates/native-go-better-auth/node/app.mjs +72 -0
  199. package/templates/native-go-better-auth/node/build.mjs +2 -0
  200. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  201. package/templates/native-go-better-auth/node/package.json +1 -0
  202. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  203. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  204. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  205. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  206. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  207. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  208. package/templates/native-go-root/root/whoami.sh +6 -0
  209. package/templates/native-go-vue/README.md +110 -0
  210. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  211. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  212. package/templates/native-go-vue/frontend/package.json +6 -0
  213. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  214. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  215. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  216. package/templates/native-go-vue/frontend/src/style.css +14 -0
  217. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  218. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  219. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  220. package/templates/native-go-vue/gitignore +2 -0
  221. package/dist/attestation/app-package.d.ts +0 -86
  222. package/dist/attestation/app-package.js +0 -206
  223. package/dist/attestation/install-on-host.d.ts +0 -14
  224. package/dist/attestation/install-on-host.js +0 -52
  225. package/dist/attestation/keyring.d.ts +0 -21
  226. package/dist/attestation/keyring.js +0 -168
  227. package/dist/attestation/platform-service.d.ts +0 -70
  228. package/dist/attestation/platform-service.js +0 -250
  229. package/dist/build-spk/broker-glue.d.ts +0 -16
  230. package/dist/build-spk/broker-glue.js +0 -40
  231. package/dist/deploy/catalog-publish.d.ts +0 -3
  232. package/dist/deploy/catalog-publish.js +0 -7
  233. package/dist/doctor.d.ts +0 -22
  234. package/dist/doctor.js +0 -721
  235. package/dist/error-collector.d.ts +0 -2
  236. package/dist/error-collector.js +0 -46
  237. package/dist/host/broker-paths.d.ts +0 -1
  238. package/dist/host/broker-paths.js +0 -1
  239. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  240. package/dist/host/broker-upgrade-preflight.js +0 -156
  241. package/dist/host/digests.d.ts +0 -5
  242. package/dist/host/digests.js +0 -63
  243. package/dist/host/fake-nas.d.ts +0 -41
  244. package/dist/host/fake-nas.js +0 -174
  245. package/dist/mock-broker/bootstrap.d.ts +0 -2
  246. package/dist/mock-broker/bootstrap.js +0 -8
  247. package/dist/mock-broker/files.d.ts +0 -60
  248. package/dist/mock-broker/files.js +0 -408
  249. package/dist/mock-broker/oidc.d.ts +0 -31
  250. package/dist/mock-broker/oidc.js +0 -161
  251. package/dist/mock-broker/path.d.ts +0 -9
  252. package/dist/mock-broker/path.js +0 -50
  253. package/dist/mock-broker/server.d.ts +0 -19
  254. package/dist/mock-broker/server.js +0 -350
  255. package/dist/publish/build-ledger.d.ts +0 -22
  256. package/dist/publish/build-ledger.js +0 -26
  257. package/dist/publish/community-path.d.ts +0 -6
  258. package/dist/publish/community-path.js +0 -41
  259. package/dist/publish/curated-release.d.ts +0 -78
  260. package/dist/publish/curated-release.js +0 -883
  261. package/dist/publish/file-build-ledger.d.ts +0 -18
  262. package/dist/publish/file-build-ledger.js +0 -268
  263. package/dist/publish/github-pr.d.ts +0 -25
  264. package/dist/publish/github-pr.js +0 -85
  265. package/dist/publish/github-repo.d.ts +0 -8
  266. package/dist/publish/github-repo.js +0 -21
  267. package/dist/publish/platform-service-release.d.ts +0 -60
  268. package/dist/publish/platform-service-release.js +0 -333
  269. package/dist/publish/preflight.d.ts +0 -24
  270. package/dist/publish/preflight.js +0 -138
  271. package/dist/publish/publish.d.ts +0 -27
  272. package/dist/publish/publish.js +0 -136
  273. package/dist/publish/sync-definition.d.ts +0 -4
  274. package/dist/publish/sync-definition.js +0 -48
  275. package/dist/release-bin.d.ts +0 -2
  276. package/dist/release-bin.js +0 -23
  277. package/dist/templates/hello-web/html/sdk.js +0 -68
  278. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  279. package/dist/templates/native/synology-app.yaml +0 -7
  280. package/dist/test-host.d.ts +0 -15
  281. package/dist/test-host.js +0 -37
  282. package/templates/hello-web/html/sdk.js +0 -68
  283. package/templates/hello-web-page/html/sdk.js +0 -68
  284. package/templates/native/synology-app.yaml +0 -7
  285. /package/dist/templates/{native → binary}/native.yaml +0 -0
  286. /package/templates/{native → binary}/native.yaml +0 -0
@@ -0,0 +1,49 @@
1
+ import { collectInstallParameters } from "./load.js";
2
+ /**
3
+ * Install parameter types that the schema still accepts but that have no working
4
+ * install path on the live package-only route.
5
+ *
6
+ * Measured on vt-nas 2026-08-27: both end in DSM broken/150 with no Container
7
+ * Manager project, while a no-parameter control package installed fine on the
8
+ * same NAS. `secret` renders its password field and fails; `homes` renders no
9
+ * input at all. `validate` reports these as warnings so build-spk keeps working
10
+ * for fixtures that declare them incidentally -- the refusal belongs at the two
11
+ * gates that actually put an App somewhere: deploy and publish.
12
+ */
13
+ const UNSUPPORTED_INSTALL_TYPES = new Map([
14
+ ["secret", "CONTRACT_SECRET_UNSUPPORTED"],
15
+ ["homes", "CONTRACT_HOMES_UNSUPPORTED"],
16
+ ]);
17
+ /** Stable gate code for an install type that only the CLI can materialize. */
18
+ export const POSIX_ALL_SHARES_LISTING_UNSUPPORTED_CODE = "CONTRACT_POSIX_ALL_SHARES_LISTING_UNSUPPORTED";
19
+ /**
20
+ * Install parameter types that work through `synopkgland deploy` but cannot
21
+ * honor their declared Share scope through Package Center or a curated
22
+ * listing. Keep this separate from UNSUPPORTED_INSTALL_TYPES: deploy must
23
+ * continue to resolve these values from the NAS administrator session.
24
+ */
25
+ const LISTING_UNSUPPORTED_INSTALL_TYPES = new Map([
26
+ ["posix_all_shares", POSIX_ALL_SHARES_LISTING_UNSUPPORTED_CODE],
27
+ ]);
28
+ export function findUnsupportedInstallTypes(contract) {
29
+ const found = [];
30
+ for (const { name, param } of collectInstallParameters(contract)) {
31
+ const type = typeof param.type === "string" ? param.type : "";
32
+ const code = UNSUPPORTED_INSTALL_TYPES.get(type);
33
+ if (code) {
34
+ found.push({ name, type, code });
35
+ }
36
+ }
37
+ return found;
38
+ }
39
+ export function findListingUnsupportedInstallTypes(contract) {
40
+ const found = [];
41
+ for (const { name, param } of collectInstallParameters(contract)) {
42
+ const type = typeof param.type === "string" ? param.type : "";
43
+ const code = LISTING_UNSUPPORTED_INSTALL_TYPES.get(type);
44
+ if (code) {
45
+ found.push({ name, type, code });
46
+ }
47
+ }
48
+ return found;
49
+ }
@@ -6,6 +6,7 @@ import { validateContractSchema } from "./schema-validate.js";
6
6
  import { isWebLaunchMode, readWebIntegration } from "./web-launch.js";
7
7
  import { loadCompose } from "../compose/analyze.js";
8
8
  import { collectPosixAllSharesParameters, isPosixAllSharesType, validatePosixAllSharesSources, } from "./posix-all-shares.js";
9
+ import { getErrorCode } from "../error-registry.js";
9
10
  import { FORMAL_PACKAGE_IDENTITY_MAX_LENGTH, PACKAGE_IDENTITY_MAX_LENGTH, } from "../deploy/identity-shared.js";
10
11
  /** DSM 7.4 Package Center rejects a longer package identity outright. */
11
12
  const DSM_PACKAGE_IDENTITY_MAX = 32;
@@ -113,6 +114,28 @@ function validateInstallParameters(parsed, contractFile) {
113
114
  errors.push(makeError("INSTALL_STRING_INVALID", contractSpan(contractFile, pos.line, pos.column, `${basePath}.default`), `Install Parameter「${name}」的字串值不合法。`, "請移除控制字元與 ${ 插值語法。"));
114
115
  }
115
116
  }
117
+ // A45/A46: both types are in the schema enum but neither has a working
118
+ // install path on the live package-only route -- measured on vt-nas as DSM
119
+ // broken/150 with no Container Manager project, against a no-parameter
120
+ // control that installed fine. Refusing here is what makes the failure
121
+ // honest: `homes` used to validate, build a valid SPK, and only break at
122
+ // install time, telling the author nothing.
123
+ if (type === "secret" || type === "homes") {
124
+ const pos = getValuePosition(doc, ["install", name, "type"]) ??
125
+ getValuePosition(doc, ["install", name]) ??
126
+ { line: 1, column: 1 };
127
+ const code = type === "secret" ? "CONTRACT_SECRET_UNSUPPORTED" : "CONTRACT_HOMES_UNSUPPORTED";
128
+ const entry = getErrorCode(code);
129
+ // A warning here, a hard refusal in deploy/publish. Making validate itself
130
+ // fail would also fail build-spk, and several existing fixtures declare
131
+ // these types only incidentally while testing wizard copy, permissions and
132
+ // script generation -- turning those red would cost real coverage for no
133
+ // extra safety. Nothing unshippable can reach a NAS or the community repo,
134
+ // because both gates refuse; the author still learns at validate time.
135
+ 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"));
136
+ // Deliberately no `continue`: the type-specific checks below still apply.
137
+ // Skipping them once silently dropped CONTRACT_HOMES_PATH_FORBIDDEN.
138
+ }
116
139
  if (type === "homes") {
117
140
  const configuredPath = Object.entries(param).find(([key, value]) => key !== "type" &&
118
141
  (key === "default" || (typeof value === "string" && isPhysicalHostPath(value))));
@@ -123,7 +146,7 @@ function validateInstallParameters(parsed, contractFile) {
123
146
  const pos = getValuePosition(doc, ["install", name, field]) ??
124
147
  getValuePosition(doc, ["install", name]) ??
125
148
  { line: 1, column: 1 };
126
- 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}};由 glue/Broker 在安裝時依這台 NAS 的 User Home 設定解析路徑。`));
149
+ 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 設定解析路徑。`));
127
150
  }
128
151
  if (isPosixAllSharesType(type)) {
129
152
  const configuredValue = Object.entries(param).find(([key, value]) => key !== "type" && (key === "default" || value !== undefined));
@@ -132,7 +155,7 @@ function validateInstallParameters(parsed, contractFile) {
132
155
  const pos = getValuePosition(doc, ["install", name, field]) ??
133
156
  getValuePosition(doc, ["install", name]) ??
134
157
  { line: 1, column: 1 };
135
- 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 清單只在安裝時由 Broker 透過 DSM API snapshot。`, `請只保留 install.${name}.type: posix_all_shares,Compose 使用 \${${name}} 作為完整 volume source。`));
158
+ 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。`));
136
159
  }
137
160
  }
138
161
  }
@@ -81,8 +81,6 @@ export function renderWebStationResource(contract) {
81
81
  const alias = webStationAlias(String(contract.package));
82
82
  const launch = resolvedWebLaunch(contract);
83
83
  // DSM synowebapi crashes on type=reverse_proxy. Package acquire of reverse_proxy
84
- // also creates a preserve portal that Broker cannot rebind (error 1300).
85
- // Static + alias matches Broker; cookie strip / proxy_pass live in nginx user.conf.
86
84
  const service = {
87
85
  service: webStationServiceName(alias),
88
86
  display_name: String(contract.displayname ?? contract.package),
@@ -2,16 +2,13 @@ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
2
  import type { ProjectRoot } from "../discovery.js";
3
3
  import type { NasHost } from "../host/types.js";
4
4
  import { resolveDeploySpkVersion } from "./spk-version.js";
5
- import type { PublishSpkResult } from "@synopackageland/package-source/publish-spk";
6
5
  export interface DeployProjectOptions {
7
6
  project: ProjectRoot;
8
7
  /** native App only: which target to build. Omitted means "ask the NAS". */
9
8
  archFamily?: NativeArchFamily;
10
9
  host: NasHost;
11
10
  dev?: boolean;
12
- replaceCurated?: boolean;
13
11
  spkVersionResolver?: typeof resolveDeploySpkVersion;
14
- catalogPublish?: (spkPath: string) => Promise<PublishSpkResult>;
15
12
  shareValues?: Record<string, string>;
16
13
  runAsValues?: Record<string, string>;
17
14
  paramValues?: Record<string, string>;
@@ -23,7 +20,7 @@ export interface DeployProjectResult {
23
20
  packageId?: string;
24
21
  spkPath?: string;
25
22
  openUrl?: string;
26
- catalogVersion?: string;
23
+ version?: string;
27
24
  /** Non-blocking pre-flight remarks, e.g. a read-only Shared Folder grant. */
28
25
  notes?: string[];
29
26
  }
@@ -1,10 +1,9 @@
1
1
  import { buildSpk } from "../build-spk/generator.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { loadContract } from "../contract/load.js";
4
- import { digestProjectBundle, digestProjectContract } from "../host/digests.js";
4
+ import { findUnsupportedInstallTypes } from "../contract/unsupported-types.js";
5
5
  import { appOriginFromContract } from "../host/open-url.js";
6
6
  import { validateProject } from "../validate.js";
7
- import { publishBuiltSpk, packageSourceOrigin } from "./catalog-publish.js";
8
7
  import { resolveDeployIdentity, PACKAGE_IDENTITY_MAX_LENGTH } from "./identity.js";
9
8
  import { resolveDeploySpkVersion } from "./spk-version.js";
10
9
  import { resolveSharedFolderExtraValues, sharedFolderParameterNames } from "./share-values.js";
@@ -12,43 +11,26 @@ import { resolveDsmUserExtraValues, dsmUserParameterNames } from "./run-as-value
12
11
  import { runSharePermissionPreflight } from "./share-permission-preflight.js";
13
12
  import { homesParameterNames, resolveHomesExtraValues } from "./homes-values.js";
14
13
  import { resolveInstallParameterExtraValues } from "./param-values.js";
15
- import { CURATED_BUILD_OFFSET } from "@synopackageland/package-source/index-manifest";
16
- const CURATED_DEPLOY_REFUSED_MESSAGE = "此 NAS 已持有 Curated App Package Attestation。請改用 --dev,或使用 --replace-curated 明確降級。";
14
+ import { loadCompose } from "../compose/analyze.js";
15
+ import { runPortPreflight } from "./port-preflight.js";
17
16
  const POSIX_ALL_SHARES_LOOKUP_FAILED = "DEPLOY_POSIX_ALL_SHARES_LOOKUP_FAILED";
18
- function brokerCapabilitiesFromContract(project) {
19
- const contract = loadContract(project.contractPath).contract;
20
- const capabilities = ["identity"];
21
- const integration = contract.integration;
22
- if (integration?.files) {
23
- capabilities.push("files");
24
- }
25
- return capabilities;
26
- }
27
- function versionBuildIsCurated(version) {
28
- if (!version) {
29
- return false;
30
- }
31
- const match = /-(\d+)$/.exec(version);
32
- if (!match) {
33
- return false;
34
- }
35
- const build = Number(match[1]);
36
- return Number.isSafeInteger(build) && build >= CURATED_BUILD_OFFSET;
37
- }
38
- async function detectCuratedInstall(host, packageId) {
39
- let brokerRecord = false;
40
- try {
41
- brokerRecord = (await host.getAttestationInfo(packageId)).attestationKind === "app-package";
42
- }
43
- catch {
44
- // requestAttestation below remains the authoritative error path when the Broker info probe is unavailable.
45
- }
46
- return {
47
- brokerRecord,
48
- installedVersion: await host.getInstalledPackageVersion?.(packageId),
49
- };
50
- }
51
17
  export async function deployProject(options) {
18
+ const contract = loadContract(options.project.contractPath).contract;
19
+ // A45/A46: refuse before anything is built or uploaded. validate only warns,
20
+ // so this is the gate that keeps an uninstallable App off a NAS.
21
+ const unsupported = findUnsupportedInstallTypes(contract);
22
+ if (unsupported.length > 0) {
23
+ const first = unsupported[0];
24
+ return {
25
+ ok: false,
26
+ code: first.code,
27
+ message: `Install Parameter「${first.name}」的 type: ${first.type} 目前沒有可用的安裝路徑,` +
28
+ "Package Center 安裝會以 DSM broken/150 結束,因此 deploy 拒絕部署。" +
29
+ (first.type === "homes"
30
+ ? "若需要管理員選定的資料夾,請改用 type: shared_folder。"
31
+ : "請改用 type: string,或讓 App 自行從 ${SYNO_APP_SECRETS_DIR} 讀檔。"),
32
+ };
33
+ }
52
34
  const validation = validateProject(options.project);
53
35
  if (!validation.ok) {
54
36
  return { ok: false, code: "VALIDATION_FAILED", message: validation.errors.map((e) => e.code).join(", ") };
@@ -61,14 +43,8 @@ export async function deployProject(options) {
61
43
  message: `最終 Package Identity「${identity.packageId}」長度 ${identity.packageId.length} 超過 DSM 上限 ${PACKAGE_IDENTITY_MAX_LENGTH} 字元。`,
62
44
  };
63
45
  }
64
- const curatedInstall = identity.developmentInstance
65
- ? undefined
66
- : await detectCuratedInstall(options.host, identity.packageId);
67
- const hasCuratedInstall = curatedInstall?.brokerRecord === true || versionBuildIsCurated(curatedInstall?.installedVersion);
68
- if (hasCuratedInstall && !options.replaceCurated) {
69
- return { ok: false, code: "CURATED_DEPLOY_REFUSED", message: CURATED_DEPLOY_REFUSED_MESSAGE };
70
- }
71
- const contract = loadContract(options.project.contractPath).contract;
46
+ const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
47
+ const explicitParamNames = new Set(Object.keys(options.paramValues ?? {}));
72
48
  const paramValues = { ...(options.paramValues ?? {}) };
73
49
  const posixAllSharesNames = Object.entries(contract.install ?? {})
74
50
  .filter(([, parameter]) => parameter?.type === "posix_all_shares")
@@ -109,18 +85,16 @@ export async function deployProject(options) {
109
85
  return { ok: false, code: homesResolved.code, message: homesResolved.message };
110
86
  }
111
87
  const shareResolved = await resolveSharedFolderExtraValues({
112
- requiredKeys: sharedFolderParameterNames(contract),
88
+ requiredKeys: sharedFolderParameterNames(contract).filter((key) => !packageInstalled || options.shareValues?.[key] !== undefined),
113
89
  provided: options.shareValues ?? {},
114
- existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
115
90
  lookup: async (logicalName) => options.host.resolveSharePath?.(logicalName),
116
91
  });
117
92
  if (!shareResolved.ok) {
118
93
  return { ok: false, code: shareResolved.code, message: shareResolved.message };
119
94
  }
120
95
  const runAsResolved = resolveDsmUserExtraValues({
121
- requiredKeys: dsmUserParameterNames(contract),
96
+ requiredKeys: dsmUserParameterNames(contract).filter((key) => !packageInstalled || options.runAsValues?.[key] !== undefined),
122
97
  provided: options.runAsValues ?? {},
123
- existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
124
98
  });
125
99
  if (!runAsResolved.ok) {
126
100
  return { ok: false, code: runAsResolved.code, message: runAsResolved.message };
@@ -143,76 +117,45 @@ export async function deployProject(options) {
143
117
  if (!sharePermission.ok) {
144
118
  return { ok: false, code: sharePermission.code, message: sharePermission.message };
145
119
  }
120
+ const portPreflight = await runPortPreflight({
121
+ compose: loadCompose(options.project.composePath)?.compose,
122
+ packageIdentity: identity.packageId,
123
+ listPortInfo: options.host.listPortInfo
124
+ ? () => options.host.listPortInfo()
125
+ : undefined,
126
+ });
127
+ if (!portPreflight.ok) {
128
+ return { ok: false, code: portPreflight.code, message: portPreflight.message };
129
+ }
146
130
  const extraValues = {
147
131
  ...paramResolved.extraValues,
148
132
  ...homesResolved.extraValues,
149
133
  ...shareResolved.extraValues,
150
134
  ...runAsResolved.extraValues,
151
135
  };
152
- const contractDigest = digestProjectContract(options.project);
153
- const bundleDigest = digestProjectBundle(options.project);
154
- if (hasCuratedInstall && options.replaceCurated) {
155
- const installed = await options.host.isPackageInstalled(identity.packageId);
156
- if (installed) {
157
- console.warn("警告:--replace-curated 會移除 Curated 安裝,但會保留 App Data。");
158
- if (!options.host.uninstallPackage) {
159
- return {
160
- ok: false,
161
- code: "CATALOG_INSTALL_FAILED",
162
- message: "此 NAS Host 不支援保留 App Data 的 Curated replacement uninstall。",
163
- };
164
- }
165
- try {
166
- await options.host.uninstallPackage(identity.packageId);
136
+ // The upgrade wizard deliberately ignores auto-filled Share controls unless
137
+ // the administrator opted into a change. Keep the CLI's explicit upgrade
138
+ // flags equivalent to checking those controls.
139
+ if (packageInstalled) {
140
+ for (const name of sharedFolderParameterNames(contract)) {
141
+ if ((options.shareValues?.[name] ?? "").trim() !== "") {
142
+ extraValues[`CHANGE_${name}`] = "true";
167
143
  }
168
- catch (error) {
169
- return {
170
- ok: false,
171
- code: "CATALOG_INSTALL_FAILED",
172
- message: error instanceof Error ? error.message : String(error),
173
- };
144
+ }
145
+ for (const name of posixAllSharesNames) {
146
+ if (explicitParamNames.has(name) && (paramValues[name] ?? "").trim() !== "") {
147
+ extraValues[`CHANGE_${name}`] = "true";
174
148
  }
175
149
  }
176
150
  }
177
- let attestationResult;
178
- try {
179
- attestationResult = await options.host.requestAttestation({
180
- packageIdentity: identity.packageId,
181
- contractDigest,
182
- bundleDigest,
183
- capabilities: brokerCapabilitiesFromContract(options.project),
184
- allowCuratedDowngrade: options.replaceCurated,
185
- });
186
- }
187
- catch (error) {
188
- const code = error.code ?? "ATTESTATION_FAILED";
189
- return {
190
- ok: false,
191
- code,
192
- message: code === "CURATED_DEPLOY_REFUSED"
193
- ? CURATED_DEPLOY_REFUSED_MESSAGE
194
- : error instanceof Error
195
- ? error.message
196
- : String(error),
197
- };
198
- }
199
151
  const resolveSpkVersion = options.spkVersionResolver ?? resolveDeploySpkVersion;
200
152
  let spkVersion;
201
153
  try {
202
154
  spkVersion = await resolveSpkVersion(options.project, options.host, identity.packageId);
203
155
  }
204
156
  catch (error) {
205
- const code = error.code;
206
- if (code === "DEPLOY_PERSONAL_BUILD_EXHAUSTED" ||
207
- code === "PERSONAL_BUILD_EXHAUSTED" ||
208
- code === "DEPLOY_VERSION_NOT_ASCENDING") {
209
- return {
210
- ok: false,
211
- code: code === "DEPLOY_VERSION_NOT_ASCENDING" ? code : "DEPLOY_PERSONAL_BUILD_EXHAUSTED",
212
- message: error instanceof Error ? error.message : String(error),
213
- };
214
- }
215
- throw error;
157
+ return { ok: false, code: "DEPLOY_VERSION_INVALID",
158
+ message: error instanceof Error ? error.message : String(error) };
216
159
  }
217
160
  let archFamily = options.archFamily;
218
161
  if (archFamily === undefined && options.project.nativePath && existsSync(options.project.nativePath)) {
@@ -231,67 +174,25 @@ export async function deployProject(options) {
231
174
  displayname: identity.displayname,
232
175
  spkVersion,
233
176
  archFamily,
234
- embeddedAttestationToken: attestationResult.token,
235
177
  });
236
178
  if (!build.ok || !build.outputPath) {
237
179
  return { ok: false, code: "BUILD_SPK_FAILED", message: build.errors.join(", ") };
238
180
  }
239
- let catalogVersion = spkVersion;
240
- const publishToCatalog = options.catalogPublish ?? publishBuiltSpk;
241
- if (options.host.installationId === "live-nas" && !identity.developmentInstance) {
242
- try {
243
- const published = await publishToCatalog(build.outputPath);
244
- catalogVersion = published.version;
245
- }
246
- catch (error) {
247
- return {
248
- ok: false,
249
- code: "CATALOG_PUBLISH_FAILED",
250
- message: error instanceof Error ? error.message : String(error),
251
- };
252
- }
253
- }
254
181
  try {
255
- if (identity.developmentInstance) {
256
- if (!options.host.installFromLocalSpk) {
257
- return {
258
- ok: false,
259
- code: "LOCAL_SPK_INSTALL_UNSUPPORTED",
260
- message: "Development deploy requires installFromLocalSpk on the host adapter.",
261
- };
262
- }
263
- await options.host.installFromLocalSpk({
264
- packageId: identity.packageId,
265
- version: catalogVersion,
266
- spkPath: build.outputPath,
267
- extraValues,
268
- });
269
- }
270
- else {
271
- if (options.host.ensurePackageSource) {
272
- await options.host.ensurePackageSource(packageSourceOrigin());
273
- }
274
- await options.host.installFromCatalog({
275
- packageId: identity.packageId,
276
- version: catalogVersion,
277
- extraValues,
278
- });
279
- }
182
+ await options.host.installFromLocalSpk({
183
+ packageId: identity.packageId,
184
+ version: spkVersion,
185
+ spkPath: build.outputPath,
186
+ extraValues,
187
+ });
280
188
  }
281
189
  catch (error) {
282
- return {
283
- ok: false,
284
- code: "CATALOG_INSTALL_FAILED",
285
- message: error instanceof Error ? error.message : String(error),
286
- };
190
+ return { ok: false, code: "LOCAL_SPK_INSTALL_FAILED",
191
+ message: error instanceof Error ? error.message : String(error) };
287
192
  }
288
- const info = await options.host.getAttestationInfo(identity.packageId);
289
193
  return {
290
- ok: true,
291
- packageId: identity.packageId,
292
- spkPath: build.outputPath,
293
- openUrl: info.openUrl ?? appOriginFromContract(options.host.dsmOrigin, contract),
294
- catalogVersion,
295
- notes: sharePermission.notes.length > 0 ? sharePermission.notes : undefined,
194
+ ok: true, packageId: identity.packageId, spkPath: build.outputPath,
195
+ openUrl: appOriginFromContract(options.host.dsmOrigin, contract),
196
+ version: spkVersion, notes: sharePermission.notes,
296
197
  };
297
198
  }
@@ -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,或 Broker 無法解析 user home 路徑;無法安裝需要 homes 的 App。請在 DSM 啟用 User Home 後重試。",
22
+ message: "這台 NAS 未啟用 DSM User Home,或 DSM API 無法解析 user home 路徑;無法安裝需要 homes 的 App。請在 DSM 啟用 User Home 後重試。",
23
23
  };
24
24
  }
25
25
  return {
@@ -0,0 +1,18 @@
1
+ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
+ import type { NasHost } from "../host/types.js";
3
+ export interface DeploymentPlan {
4
+ root: string;
5
+ kind: "native-go" | "binary" | "container";
6
+ packageId: string;
7
+ version: string;
8
+ arch?: NativeArchFamily;
9
+ privilege: "none" | "root";
10
+ transport: "dsm-local-spk";
11
+ notes: string[];
12
+ }
13
+ /** Purely local planning: no credentials, network, build or installation. */
14
+ export declare function planDeployment(root: string, options?: {
15
+ arch?: NativeArchFamily;
16
+ dev?: boolean;
17
+ }): DeploymentPlan;
18
+ export declare function deployStandalone(plan: DeploymentPlan, host: NasHost): Promise<string>;
@@ -0,0 +1,54 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { findProjectRoot } from "../discovery.js";
4
+ import { loadContract } from "../contract/load.js";
5
+ import { validateProject } from "../validate.js";
6
+ import { findStandaloneRoot, loadStandaloneManifest } from "../standalone/manifest.js";
7
+ import { buildStandalone } from "../standalone/build.js";
8
+ import { resolveDeployIdentity } from "./identity.js";
9
+ import { nextDeployVersion } from "./spk-version.js";
10
+ /** Purely local planning: no credentials, network, build or installation. */
11
+ export function planDeployment(root, options = {}) {
12
+ const standalone = findStandaloneRoot(root);
13
+ if (standalone) {
14
+ const loaded = loadStandaloneManifest(standalone);
15
+ if (!loaded.ok)
16
+ throw new Error(loaded.errors.map(e => `${e.code}: ${e.message}`).join("\n"));
17
+ const m = loaded.manifest;
18
+ if (options.dev)
19
+ throw new Error('--dev is not supported for native-go; choose a distinct package name at init');
20
+ if (options.arch && !m.arches.includes(options.arch))
21
+ throw new Error(`Architecture not declared: ${options.arch}`);
22
+ return { root: standalone, kind: "native-go", packageId: m.package, version: m.spkVersion,
23
+ arch: options.arch, privilege: m.privilege, transport: "dsm-local-spk",
24
+ notes: m.privilege === "root" ? ['Approve root tasks in DSM after installation; changed task text requires fresh approval. Remove tasks before uninstall.'] : [] };
25
+ }
26
+ const project = findProjectRoot(root);
27
+ if (!project)
28
+ throw new Error('No dsm-package.yaml or synology-app.yaml found');
29
+ const validation = validateProject(project);
30
+ if (!validation.ok)
31
+ throw new Error(validation.errors.map(e => `${e.code}: ${e.message}`).join("\n"));
32
+ const contract = loadContract(project.contractPath).contract;
33
+ const identity = resolveDeployIdentity(project, { dev: options.dev });
34
+ return { root: project.root, kind: existsSync(join(project.root, 'native.yaml')) ? 'binary' : 'container',
35
+ packageId: identity.packageId, version: nextDeployVersion(String(contract.version)),
36
+ arch: options.arch, privilege: 'none', transport: 'dsm-local-spk',
37
+ notes: ['DSM resolves target architecture and installed version during deployment. Container bind access uses the configured process user.'] };
38
+ }
39
+ export async function deployStandalone(plan, host) {
40
+ if (plan.kind !== 'native-go')
41
+ throw new Error('Expected native-go deployment plan');
42
+ const arch = plan.arch ?? await host.getArchFamily?.();
43
+ if (!arch)
44
+ throw new Error('Cannot determine NAS architecture; specify --arch');
45
+ const installed = await host.getInstalledPackageVersion?.(plan.packageId);
46
+ if (installed && nextDeployVersion(plan.version, installed) !== plan.version) {
47
+ throw new Error(`Increase dsm-package.yaml version/build above installed ${installed}`);
48
+ }
49
+ const { outputs } = await buildStandalone({ root: plan.root, arches: [arch] });
50
+ if (outputs.length !== 1)
51
+ throw new Error('Expected one SPK');
52
+ await host.installFromLocalSpk({ packageId: plan.packageId, version: plan.version, spkPath: outputs[0] });
53
+ return outputs[0];
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { ComposeDocument } from "../types.js";
2
+ import { type PortInfoEntry } from "../host/port-info.js";
3
+ export declare const PORT_ALREADY_IN_USE = "PORT_ALREADY_IN_USE";
4
+ export interface PortPreflightInput {
5
+ compose?: ComposeDocument;
6
+ packageIdentity: string;
7
+ listPortInfo?: () => Promise<PortInfoEntry[] | undefined>;
8
+ }
9
+ export type PortPreflightResult = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ code: typeof PORT_ALREADY_IN_USE;
14
+ message: string;
15
+ };
16
+ /**
17
+ * Checks Compose's published TCP host ports against DSM's administrator-only
18
+ * PortInfo listing. Missing capability, transport failure, malformed data,
19
+ * and an empty listing all mean "cannot tell" and therefore pass through.
20
+ */
21
+ export declare function runPortPreflight(input: PortPreflightInput): Promise<PortPreflightResult>;
@@ -0,0 +1,42 @@
1
+ import { collectPublishedTcpPorts } from "../compose/analyze.js";
2
+ import { findPortConflicts, formatPortOccupant, } from "../host/port-info.js";
3
+ export const PORT_ALREADY_IN_USE = "PORT_ALREADY_IN_USE";
4
+ /**
5
+ * Checks Compose's published TCP host ports against DSM's administrator-only
6
+ * PortInfo listing. Missing capability, transport failure, malformed data,
7
+ * and an empty listing all mean "cannot tell" and therefore pass through.
8
+ */
9
+ export async function runPortPreflight(input) {
10
+ const publishedPorts = input.compose
11
+ ? collectPublishedTcpPorts(input.compose)
12
+ : new Map();
13
+ if (publishedPorts.size === 0 || !input.listPortInfo) {
14
+ return { ok: true };
15
+ }
16
+ let portInfo;
17
+ try {
18
+ portInfo = await input.listPortInfo();
19
+ }
20
+ catch {
21
+ portInfo = undefined;
22
+ }
23
+ if (!portInfo || portInfo.length === 0) {
24
+ return { ok: true };
25
+ }
26
+ const conflicts = findPortConflicts({
27
+ publishedPorts,
28
+ packageIdentity: input.packageIdentity,
29
+ portInfo,
30
+ });
31
+ const conflict = conflicts[0];
32
+ if (!conflict) {
33
+ return { ok: true };
34
+ }
35
+ const occupants = conflict.occupants.map(formatPortOccupant).join("、");
36
+ return {
37
+ ok: false,
38
+ code: PORT_ALREADY_IN_USE,
39
+ message: `Published port ${conflict.port} 已被 ${occupants} 佔用(Compose service:${conflict.services.join("、")})。` +
40
+ "請改用未被其他服務使用的 host port;同一 Package Identity 的既有容器會在升級/重裝時放行。",
41
+ };
42
+ }
@@ -1,17 +1,5 @@
1
1
  import type { ProjectRoot } from "../discovery.js";
2
2
  import type { NasHost } from "../host/types.js";
3
- export declare class DeployPersonalBuildExhaustedError extends Error {
4
- readonly code = "DEPLOY_PERSONAL_BUILD_EXHAUSTED";
5
- constructor();
6
- }
7
- export declare class DeployVersionNotAscendingError extends Error {
8
- readonly code = "DEPLOY_VERSION_NOT_ASCENDING";
9
- constructor(contractVersion: string, proposedVersion: string, existingVersion: string);
10
- }
11
- export interface DeployVersionCandidates {
12
- installed?: string;
13
- feed?: string;
14
- }
15
- export declare function resolvePersonalDeploySpkVersion(contractVersion: string, candidates?: DeployVersionCandidates): string;
3
+ /** Increment the installed build only for the same application version. */
4
+ export declare function nextDeployVersion(version: string, installed?: string): string;
16
5
  export declare function resolveDeploySpkVersion(project: ProjectRoot, host: NasHost, packageId: string): Promise<string>;
17
- export declare function publishDeploySpk(spkPath: string): Promise<string>;