@synopackageland/cli 0.3.10 → 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 (280) 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 +45 -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/spk-version.d.ts +2 -14
  20. package/dist/deploy/spk-version.js +25 -111
  21. package/dist/dev.d.ts +1 -3
  22. package/dist/dev.js +14 -28
  23. package/dist/error-registry.js +810 -862
  24. package/dist/host/dsm-client.js +12 -272
  25. package/dist/host/dsm-port-profile.d.ts +0 -1
  26. package/dist/host/dsm-port-profile.js +0 -2
  27. package/dist/host/types.d.ts +1 -83
  28. package/dist/index.d.ts +0 -9
  29. package/dist/index.js +0 -6
  30. package/dist/info.d.ts +0 -1
  31. package/dist/info.js +3 -10
  32. package/dist/init.d.ts +11 -1
  33. package/dist/init.js +112 -4
  34. package/dist/log.js +2 -3
  35. package/dist/native/validate.js +0 -1
  36. package/dist/standalone/binary.d.ts +9 -0
  37. package/dist/standalone/binary.js +67 -0
  38. package/dist/standalone/build.d.ts +25 -0
  39. package/dist/standalone/build.js +108 -0
  40. package/dist/standalone/dsm-window.d.ts +26 -0
  41. package/dist/standalone/dsm-window.js +251 -0
  42. package/dist/standalone/helper.d.ts +31 -0
  43. package/dist/standalone/helper.js +104 -0
  44. package/dist/standalone/i18n.d.ts +28 -0
  45. package/dist/standalone/i18n.js +161 -0
  46. package/dist/standalone/inspect-spk.d.ts +21 -0
  47. package/dist/standalone/inspect-spk.js +160 -0
  48. package/dist/standalone/manifest.d.ts +71 -0
  49. package/dist/standalone/manifest.js +276 -0
  50. package/dist/standalone/root-messages.d.ts +7 -0
  51. package/dist/standalone/root-messages.js +250 -0
  52. package/dist/standalone/root-ops.d.ts +42 -0
  53. package/dist/standalone/root-ops.js +101 -0
  54. package/dist/standalone/root-window.d.ts +35 -0
  55. package/dist/standalone/root-window.js +290 -0
  56. package/dist/standalone/spk.d.ts +22 -0
  57. package/dist/standalone/spk.js +205 -0
  58. package/dist/templates/binary/synology-app.yaml +7 -0
  59. package/dist/templates/hello-files/compose.yaml +0 -1
  60. package/dist/templates/hello-web/compose.yaml +13 -10
  61. package/dist/templates/hello-web/html/app.js +1 -23
  62. package/dist/templates/hello-web/html/index.html +1 -3
  63. package/dist/templates/hello-web-page/compose.yaml +13 -10
  64. package/dist/templates/hello-web-page/html/app.js +1 -23
  65. package/dist/templates/hello-web-page/html/index.html +1 -3
  66. package/dist/templates/native-go/README.md +268 -0
  67. package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
  68. package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
  69. package/dist/templates/native-go/backend/go.mod +3 -0
  70. package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
  71. package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
  72. package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
  73. package/dist/templates/native-go/backend/internal/api/server.go +155 -0
  74. package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
  75. package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
  76. package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
  77. package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
  78. package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  79. package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
  80. package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
  81. package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  82. package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  83. package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  84. package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
  85. package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  86. package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
  87. package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
  88. package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
  89. package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  90. package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  91. package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  92. package/dist/templates/native-go/dsm-package.yaml +58 -0
  93. package/dist/templates/native-go/gitignore +1 -0
  94. package/dist/templates/native-go/i18n/glossary.json +662 -0
  95. package/dist/templates/native-go/i18n/messages/de.json +28 -0
  96. package/dist/templates/native-go/i18n/messages/en.json +28 -0
  97. package/dist/templates/native-go/i18n/messages/es.json +28 -0
  98. package/dist/templates/native-go/i18n/messages/fr.json +28 -0
  99. package/dist/templates/native-go/i18n/messages/it.json +28 -0
  100. package/dist/templates/native-go/i18n/messages/ja.json +28 -0
  101. package/dist/templates/native-go/i18n/messages/ko.json +28 -0
  102. package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
  103. package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
  104. package/dist/templates/native-go/ui/app.css +35 -0
  105. package/dist/templates/native-go/ui/app.js +94 -0
  106. package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  107. package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  108. package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
  109. package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
  110. package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
  111. package/dist/templates/native-go-better-auth/node/package.json +1 -0
  112. package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  113. package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  114. package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  115. package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  116. package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
  117. package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  118. package/dist/templates/native-go-root/root/whoami.sh +6 -0
  119. package/dist/templates/native-go-vue/README.md +110 -0
  120. package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
  121. package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
  122. package/dist/templates/native-go-vue/frontend/package.json +6 -0
  123. package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
  124. package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
  125. package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
  126. package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
  127. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  128. package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
  129. package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
  130. package/dist/templates/native-go-vue/gitignore +2 -0
  131. package/dist/types.d.ts +1 -1
  132. package/dist/validate.js +11 -7
  133. package/package.json +3 -7
  134. package/schema/synology-app.schema.json +36 -37
  135. package/skills/SKILL.md +4 -48
  136. package/skills/reference/create.md +11 -83
  137. package/skills/reference/deploy.md +7 -78
  138. package/skills/reference/diagnose.md +4 -101
  139. package/skills/reference/files.md +5 -41
  140. package/skills/reference/identity.md +3 -29
  141. package/skills/reference/wrap.md +0 -2
  142. package/templates/binary/synology-app.yaml +7 -0
  143. package/templates/hello-files/compose.yaml +0 -1
  144. package/templates/hello-web/compose.yaml +13 -10
  145. package/templates/hello-web/html/app.js +1 -23
  146. package/templates/hello-web/html/index.html +1 -3
  147. package/templates/hello-web-page/compose.yaml +13 -10
  148. package/templates/hello-web-page/html/app.js +1 -23
  149. package/templates/hello-web-page/html/index.html +1 -3
  150. package/templates/native-go/README.md +268 -0
  151. package/templates/native-go/backend/cmd/app/main.go +49 -0
  152. package/templates/native-go/backend/cmd/helper/main.go +38 -0
  153. package/templates/native-go/backend/go.mod +3 -0
  154. package/templates/native-go/backend/internal/api/errors.go +33 -0
  155. package/templates/native-go/backend/internal/api/registry.go +94 -0
  156. package/templates/native-go/backend/internal/api/registry_test.go +329 -0
  157. package/templates/native-go/backend/internal/api/server.go +155 -0
  158. package/templates/native-go/backend/internal/app/root_none.go +9 -0
  159. package/templates/native-go/backend/internal/app/routes.go +127 -0
  160. package/templates/native-go/backend/internal/app/routes_test.go +167 -0
  161. package/templates/native-go/backend/internal/dsm/auth.go +118 -0
  162. package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
  163. package/templates/native-go/backend/internal/helper/control.go +219 -0
  164. package/templates/native-go/backend/internal/helper/control_test.go +242 -0
  165. package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
  166. package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
  167. package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
  168. package/templates/native-go/backend/internal/helper/process_other.go +5 -0
  169. package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
  170. package/templates/native-go/backend/internal/helper/status.go +271 -0
  171. package/templates/native-go/backend/internal/helper/status_test.go +205 -0
  172. package/templates/native-go/backend/internal/helper/supervision.go +84 -0
  173. package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
  174. package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
  175. package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
  176. package/templates/native-go/dsm-package.yaml +58 -0
  177. package/templates/native-go/gitignore +1 -0
  178. package/templates/native-go/i18n/glossary.json +662 -0
  179. package/templates/native-go/i18n/messages/de.json +28 -0
  180. package/templates/native-go/i18n/messages/en.json +28 -0
  181. package/templates/native-go/i18n/messages/es.json +28 -0
  182. package/templates/native-go/i18n/messages/fr.json +28 -0
  183. package/templates/native-go/i18n/messages/it.json +28 -0
  184. package/templates/native-go/i18n/messages/ja.json +28 -0
  185. package/templates/native-go/i18n/messages/ko.json +28 -0
  186. package/templates/native-go/i18n/messages/zh-CN.json +28 -0
  187. package/templates/native-go/i18n/messages/zh-TW.json +28 -0
  188. package/templates/native-go/ui/app.css +35 -0
  189. package/templates/native-go/ui/app.js +94 -0
  190. package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
  191. package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
  192. package/templates/native-go-better-auth/node/app.mjs +72 -0
  193. package/templates/native-go-better-auth/node/build.mjs +2 -0
  194. package/templates/native-go-better-auth/node/package-lock.json +862 -0
  195. package/templates/native-go-better-auth/node/package.json +1 -0
  196. package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
  197. package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
  198. package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
  199. package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
  200. package/templates/native-go-root/backend/internal/app/root.go +107 -0
  201. package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
  202. package/templates/native-go-root/root/whoami.sh +6 -0
  203. package/templates/native-go-vue/README.md +110 -0
  204. package/templates/native-go-vue/frontend/DESIGN.md +78 -0
  205. package/templates/native-go-vue/frontend/package-lock.json +2507 -0
  206. package/templates/native-go-vue/frontend/package.json +6 -0
  207. package/templates/native-go-vue/frontend/src/App.vue +132 -0
  208. package/templates/native-go-vue/frontend/src/context.ts +17 -0
  209. package/templates/native-go-vue/frontend/src/main.ts +26 -0
  210. package/templates/native-go-vue/frontend/src/style.css +14 -0
  211. package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
  212. package/templates/native-go-vue/frontend/tsconfig.json +1 -0
  213. package/templates/native-go-vue/frontend/vite.config.ts +8 -0
  214. package/templates/native-go-vue/gitignore +2 -0
  215. package/dist/attestation/app-package.d.ts +0 -86
  216. package/dist/attestation/app-package.js +0 -206
  217. package/dist/attestation/install-on-host.d.ts +0 -14
  218. package/dist/attestation/install-on-host.js +0 -52
  219. package/dist/attestation/keyring.d.ts +0 -21
  220. package/dist/attestation/keyring.js +0 -168
  221. package/dist/attestation/platform-service.d.ts +0 -70
  222. package/dist/attestation/platform-service.js +0 -250
  223. package/dist/build-spk/broker-glue.d.ts +0 -16
  224. package/dist/build-spk/broker-glue.js +0 -40
  225. package/dist/deploy/catalog-publish.d.ts +0 -3
  226. package/dist/deploy/catalog-publish.js +0 -7
  227. package/dist/doctor.d.ts +0 -22
  228. package/dist/doctor.js +0 -822
  229. package/dist/error-collector.d.ts +0 -2
  230. package/dist/error-collector.js +0 -46
  231. package/dist/host/broker-paths.d.ts +0 -1
  232. package/dist/host/broker-paths.js +0 -1
  233. package/dist/host/broker-upgrade-preflight.d.ts +0 -61
  234. package/dist/host/broker-upgrade-preflight.js +0 -156
  235. package/dist/host/digests.d.ts +0 -5
  236. package/dist/host/digests.js +0 -63
  237. package/dist/host/fake-nas.d.ts +0 -49
  238. package/dist/host/fake-nas.js +0 -193
  239. package/dist/mock-broker/bootstrap.d.ts +0 -2
  240. package/dist/mock-broker/bootstrap.js +0 -8
  241. package/dist/mock-broker/files.d.ts +0 -60
  242. package/dist/mock-broker/files.js +0 -408
  243. package/dist/mock-broker/oidc.d.ts +0 -31
  244. package/dist/mock-broker/oidc.js +0 -161
  245. package/dist/mock-broker/path.d.ts +0 -9
  246. package/dist/mock-broker/path.js +0 -50
  247. package/dist/mock-broker/server.d.ts +0 -19
  248. package/dist/mock-broker/server.js +0 -350
  249. package/dist/publish/build-ledger.d.ts +0 -22
  250. package/dist/publish/build-ledger.js +0 -26
  251. package/dist/publish/community-path.d.ts +0 -6
  252. package/dist/publish/community-path.js +0 -41
  253. package/dist/publish/curated-release.d.ts +0 -78
  254. package/dist/publish/curated-release.js +0 -883
  255. package/dist/publish/file-build-ledger.d.ts +0 -18
  256. package/dist/publish/file-build-ledger.js +0 -268
  257. package/dist/publish/github-pr.d.ts +0 -25
  258. package/dist/publish/github-pr.js +0 -85
  259. package/dist/publish/github-repo.d.ts +0 -8
  260. package/dist/publish/github-repo.js +0 -21
  261. package/dist/publish/platform-service-release.d.ts +0 -60
  262. package/dist/publish/platform-service-release.js +0 -333
  263. package/dist/publish/preflight.d.ts +0 -24
  264. package/dist/publish/preflight.js +0 -138
  265. package/dist/publish/publish.d.ts +0 -27
  266. package/dist/publish/publish.js +0 -136
  267. package/dist/publish/sync-definition.d.ts +0 -4
  268. package/dist/publish/sync-definition.js +0 -48
  269. package/dist/release-bin.d.ts +0 -2
  270. package/dist/release-bin.js +0 -23
  271. package/dist/templates/hello-web/html/sdk.js +0 -68
  272. package/dist/templates/hello-web-page/html/sdk.js +0 -68
  273. package/dist/templates/native/synology-app.yaml +0 -7
  274. package/dist/test-host.d.ts +0 -15
  275. package/dist/test-host.js +0 -37
  276. package/templates/hello-web/html/sdk.js +0 -68
  277. package/templates/hello-web-page/html/sdk.js +0 -68
  278. package/templates/native/synology-app.yaml +0 -7
  279. /package/dist/templates/{native → binary}/native.yaml +0 -0
  280. /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,45 +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
14
  import { loadCompose } from "../compose/analyze.js";
17
15
  import { runPortPreflight } from "./port-preflight.js";
18
- const CURATED_DEPLOY_REFUSED_MESSAGE = "此 NAS 已持有 Curated App Package Attestation。請改用 --dev,或使用 --replace-curated 明確降級。";
19
16
  const POSIX_ALL_SHARES_LOOKUP_FAILED = "DEPLOY_POSIX_ALL_SHARES_LOOKUP_FAILED";
20
- function brokerCapabilitiesFromContract(project) {
21
- const contract = loadContract(project.contractPath).contract;
22
- const capabilities = ["identity"];
23
- const integration = contract.integration;
24
- if (integration?.files) {
25
- capabilities.push("files");
26
- }
27
- return capabilities;
28
- }
29
- function versionBuildIsCurated(version) {
30
- if (!version) {
31
- return false;
32
- }
33
- const match = /-(\d+)$/.exec(version);
34
- if (!match) {
35
- return false;
36
- }
37
- const build = Number(match[1]);
38
- return Number.isSafeInteger(build) && build >= CURATED_BUILD_OFFSET;
39
- }
40
- async function detectCuratedInstall(host, packageId) {
41
- let brokerRecord = false;
42
- try {
43
- brokerRecord = (await host.getAttestationInfo(packageId)).attestationKind === "app-package";
44
- }
45
- catch {
46
- // requestAttestation below remains the authoritative error path when the Broker info probe is unavailable.
47
- }
48
- return {
49
- brokerRecord,
50
- installedVersion: await host.getInstalledPackageVersion?.(packageId),
51
- };
52
- }
53
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
+ }
54
34
  const validation = validateProject(options.project);
55
35
  if (!validation.ok) {
56
36
  return { ok: false, code: "VALIDATION_FAILED", message: validation.errors.map((e) => e.code).join(", ") };
@@ -63,14 +43,8 @@ export async function deployProject(options) {
63
43
  message: `最終 Package Identity「${identity.packageId}」長度 ${identity.packageId.length} 超過 DSM 上限 ${PACKAGE_IDENTITY_MAX_LENGTH} 字元。`,
64
44
  };
65
45
  }
66
- const curatedInstall = identity.developmentInstance
67
- ? undefined
68
- : await detectCuratedInstall(options.host, identity.packageId);
69
- const hasCuratedInstall = curatedInstall?.brokerRecord === true || versionBuildIsCurated(curatedInstall?.installedVersion);
70
- if (hasCuratedInstall && !options.replaceCurated) {
71
- return { ok: false, code: "CURATED_DEPLOY_REFUSED", message: CURATED_DEPLOY_REFUSED_MESSAGE };
72
- }
73
- 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 ?? {}));
74
48
  const paramValues = { ...(options.paramValues ?? {}) };
75
49
  const posixAllSharesNames = Object.entries(contract.install ?? {})
76
50
  .filter(([, parameter]) => parameter?.type === "posix_all_shares")
@@ -111,18 +85,16 @@ export async function deployProject(options) {
111
85
  return { ok: false, code: homesResolved.code, message: homesResolved.message };
112
86
  }
113
87
  const shareResolved = await resolveSharedFolderExtraValues({
114
- requiredKeys: sharedFolderParameterNames(contract),
88
+ requiredKeys: sharedFolderParameterNames(contract).filter((key) => !packageInstalled || options.shareValues?.[key] !== undefined),
115
89
  provided: options.shareValues ?? {},
116
- existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
117
90
  lookup: async (logicalName) => options.host.resolveSharePath?.(logicalName),
118
91
  });
119
92
  if (!shareResolved.ok) {
120
93
  return { ok: false, code: shareResolved.code, message: shareResolved.message };
121
94
  }
122
95
  const runAsResolved = resolveDsmUserExtraValues({
123
- requiredKeys: dsmUserParameterNames(contract),
96
+ requiredKeys: dsmUserParameterNames(contract).filter((key) => !packageInstalled || options.runAsValues?.[key] !== undefined),
124
97
  provided: options.runAsValues ?? {},
125
- existingEnv: (await options.host.getInstalledProjectEnv?.(identity.packageId)) ?? {},
126
98
  });
127
99
  if (!runAsResolved.ok) {
128
100
  return { ok: false, code: runAsResolved.code, message: runAsResolved.message };
@@ -161,70 +133,29 @@ export async function deployProject(options) {
161
133
  ...shareResolved.extraValues,
162
134
  ...runAsResolved.extraValues,
163
135
  };
164
- const contractDigest = digestProjectContract(options.project);
165
- const bundleDigest = digestProjectBundle(options.project);
166
- if (hasCuratedInstall && options.replaceCurated) {
167
- const installed = await options.host.isPackageInstalled(identity.packageId);
168
- if (installed) {
169
- console.warn("警告:--replace-curated 會移除 Curated 安裝,但會保留 App Data。");
170
- if (!options.host.uninstallPackage) {
171
- return {
172
- ok: false,
173
- code: "CATALOG_INSTALL_FAILED",
174
- message: "此 NAS Host 不支援保留 App Data 的 Curated replacement uninstall。",
175
- };
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";
176
143
  }
177
- try {
178
- await options.host.uninstallPackage(identity.packageId);
179
- }
180
- catch (error) {
181
- return {
182
- ok: false,
183
- code: "CATALOG_INSTALL_FAILED",
184
- message: error instanceof Error ? error.message : String(error),
185
- };
144
+ }
145
+ for (const name of posixAllSharesNames) {
146
+ if (explicitParamNames.has(name) && (paramValues[name] ?? "").trim() !== "") {
147
+ extraValues[`CHANGE_${name}`] = "true";
186
148
  }
187
149
  }
188
150
  }
189
- let attestationResult;
190
- try {
191
- attestationResult = await options.host.requestAttestation({
192
- packageIdentity: identity.packageId,
193
- contractDigest,
194
- bundleDigest,
195
- capabilities: brokerCapabilitiesFromContract(options.project),
196
- allowCuratedDowngrade: options.replaceCurated,
197
- });
198
- }
199
- catch (error) {
200
- const code = error.code ?? "ATTESTATION_FAILED";
201
- return {
202
- ok: false,
203
- code,
204
- message: code === "CURATED_DEPLOY_REFUSED"
205
- ? CURATED_DEPLOY_REFUSED_MESSAGE
206
- : error instanceof Error
207
- ? error.message
208
- : String(error),
209
- };
210
- }
211
151
  const resolveSpkVersion = options.spkVersionResolver ?? resolveDeploySpkVersion;
212
152
  let spkVersion;
213
153
  try {
214
154
  spkVersion = await resolveSpkVersion(options.project, options.host, identity.packageId);
215
155
  }
216
156
  catch (error) {
217
- const code = error.code;
218
- if (code === "DEPLOY_PERSONAL_BUILD_EXHAUSTED" ||
219
- code === "PERSONAL_BUILD_EXHAUSTED" ||
220
- code === "DEPLOY_VERSION_NOT_ASCENDING") {
221
- return {
222
- ok: false,
223
- code: code === "DEPLOY_VERSION_NOT_ASCENDING" ? code : "DEPLOY_PERSONAL_BUILD_EXHAUSTED",
224
- message: error instanceof Error ? error.message : String(error),
225
- };
226
- }
227
- throw error;
157
+ return { ok: false, code: "DEPLOY_VERSION_INVALID",
158
+ message: error instanceof Error ? error.message : String(error) };
228
159
  }
229
160
  let archFamily = options.archFamily;
230
161
  if (archFamily === undefined && options.project.nativePath && existsSync(options.project.nativePath)) {
@@ -243,67 +174,25 @@ export async function deployProject(options) {
243
174
  displayname: identity.displayname,
244
175
  spkVersion,
245
176
  archFamily,
246
- embeddedAttestationToken: attestationResult.token,
247
177
  });
248
178
  if (!build.ok || !build.outputPath) {
249
179
  return { ok: false, code: "BUILD_SPK_FAILED", message: build.errors.join(", ") };
250
180
  }
251
- let catalogVersion = spkVersion;
252
- const publishToCatalog = options.catalogPublish ?? publishBuiltSpk;
253
- if (options.host.installationId === "live-nas" && !identity.developmentInstance) {
254
- try {
255
- const published = await publishToCatalog(build.outputPath);
256
- catalogVersion = published.version;
257
- }
258
- catch (error) {
259
- return {
260
- ok: false,
261
- code: "CATALOG_PUBLISH_FAILED",
262
- message: error instanceof Error ? error.message : String(error),
263
- };
264
- }
265
- }
266
181
  try {
267
- if (identity.developmentInstance) {
268
- if (!options.host.installFromLocalSpk) {
269
- return {
270
- ok: false,
271
- code: "LOCAL_SPK_INSTALL_UNSUPPORTED",
272
- message: "Development deploy requires installFromLocalSpk on the host adapter.",
273
- };
274
- }
275
- await options.host.installFromLocalSpk({
276
- packageId: identity.packageId,
277
- version: catalogVersion,
278
- spkPath: build.outputPath,
279
- extraValues,
280
- });
281
- }
282
- else {
283
- if (options.host.ensurePackageSource) {
284
- await options.host.ensurePackageSource(packageSourceOrigin());
285
- }
286
- await options.host.installFromCatalog({
287
- packageId: identity.packageId,
288
- version: catalogVersion,
289
- extraValues,
290
- });
291
- }
182
+ await options.host.installFromLocalSpk({
183
+ packageId: identity.packageId,
184
+ version: spkVersion,
185
+ spkPath: build.outputPath,
186
+ extraValues,
187
+ });
292
188
  }
293
189
  catch (error) {
294
- return {
295
- ok: false,
296
- code: "CATALOG_INSTALL_FAILED",
297
- message: error instanceof Error ? error.message : String(error),
298
- };
190
+ return { ok: false, code: "LOCAL_SPK_INSTALL_FAILED",
191
+ message: error instanceof Error ? error.message : String(error) };
299
192
  }
300
- const info = await options.host.getAttestationInfo(identity.packageId);
301
193
  return {
302
- ok: true,
303
- packageId: identity.packageId,
304
- spkPath: build.outputPath,
305
- openUrl: info.openUrl ?? appOriginFromContract(options.host.dsmOrigin, contract),
306
- catalogVersion,
307
- 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,
308
197
  };
309
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
+ }
@@ -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>;