@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,87 @@
1
+ import { dsmFetch } from "./dsm-http.js";
2
+ const PORT_INFO_API = "SYNO.Core.Service.PortInfo";
3
+ function asRecord(value) {
4
+ if (value && typeof value === "object" && !Array.isArray(value)) {
5
+ return value;
6
+ }
7
+ return undefined;
8
+ }
9
+ function validPortRange(value) {
10
+ const match = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(value.trim());
11
+ if (!match) {
12
+ return false;
13
+ }
14
+ const start = Number(match[1]);
15
+ const end = Number(match[2] ?? match[1]);
16
+ return (Number.isSafeInteger(start) &&
17
+ Number.isSafeInteger(end) &&
18
+ start >= 1 &&
19
+ start <= end &&
20
+ end <= 65535);
21
+ }
22
+ /**
23
+ * Parses the native DSM response, not a synoagentcli operation envelope.
24
+ * `undefined` means "cannot tell" and deliberately allows deployment to
25
+ * continue. An empty `port_info` list has the same fail-open meaning.
26
+ */
27
+ export function parsePortInfo(payload) {
28
+ const outer = asRecord(payload);
29
+ const data = asRecord(outer?.data);
30
+ const items = data?.port_info;
31
+ if (outer?.success !== true || !Array.isArray(items) || items.length === 0) {
32
+ return undefined;
33
+ }
34
+ const entries = [];
35
+ for (const item of items) {
36
+ const record = asRecord(item);
37
+ if (!record) {
38
+ return undefined;
39
+ }
40
+ const desc = record?.desc;
41
+ const dstPort = record?.dst_port;
42
+ const name = record?.name;
43
+ const portId = record?.port_id;
44
+ const protocol = record?.protocol;
45
+ if (typeof desc !== "string" ||
46
+ typeof name !== "string" ||
47
+ typeof portId !== "string" ||
48
+ typeof protocol !== "string" ||
49
+ !Array.isArray(dstPort) ||
50
+ dstPort.length === 0 ||
51
+ !dstPort.every((value) => typeof value === "string" && validPortRange(value))) {
52
+ return undefined;
53
+ }
54
+ const srcPort = record.src_port;
55
+ if (srcPort !== undefined &&
56
+ srcPort !== null &&
57
+ (!Array.isArray(srcPort) || !srcPort.every((value) => typeof value === "string"))) {
58
+ return undefined;
59
+ }
60
+ entries.push({
61
+ desc,
62
+ dst_port: [...dstPort],
63
+ name,
64
+ port_id: portId,
65
+ protocol,
66
+ ...(srcPort === undefined ? {} : { src_port: srcPort === null ? null : [...srcPort] }),
67
+ });
68
+ }
69
+ return entries;
70
+ }
71
+ export async function fetchPortInfo(dsmOrigin, cookieHeader, request = dsmFetch) {
72
+ try {
73
+ const query = new URLSearchParams({
74
+ api: PORT_INFO_API,
75
+ version: "1",
76
+ method: "load",
77
+ });
78
+ const response = await request(`${dsmOrigin.replace(/\/$/, "")}/webapi/entry.cgi?${query.toString()}`, { headers: { cookie: cookieHeader } });
79
+ if (!response.ok) {
80
+ return undefined;
81
+ }
82
+ return parsePortInfo(await response.json());
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
@@ -0,0 +1,33 @@
1
+ /** One row returned by SYNO.Core.Service.PortInfo/load. */
2
+ export interface PortInfoEntry {
3
+ desc: string;
4
+ dst_port: readonly string[];
5
+ name: string;
6
+ port_id: string;
7
+ protocol: string;
8
+ src_port?: readonly string[] | null;
9
+ }
10
+ export type DsmPortInfoEntry = PortInfoEntry;
11
+ export type PortInfoRow = PortInfoEntry;
12
+ export interface PortConflict {
13
+ port: number;
14
+ services: readonly string[];
15
+ occupants: readonly PortInfoEntry[];
16
+ }
17
+ /**
18
+ * Returns the Docker container name represented by a DSM port row, when the
19
+ * row has Docker's known `port_id` or `desc` shape. Other DSM services are
20
+ * intentionally returned as undefined so they remain conflicts.
21
+ */
22
+ export declare function dockerContainerName(entry: PortInfoEntry): string | undefined;
23
+ export declare function formatPortOccupant(entry: PortInfoEntry): string;
24
+ /**
25
+ * Finds occupied published ports, ignoring only containers that are one of
26
+ * the Compose containers for this exact package identity. A same-identity
27
+ * occupant is the existing install being upgraded or reinstalled.
28
+ */
29
+ export declare function findPortConflicts(input: {
30
+ publishedPorts: ReadonlyMap<number, readonly string[]>;
31
+ packageIdentity: string;
32
+ portInfo: readonly PortInfoEntry[];
33
+ }): PortConflict[];
@@ -0,0 +1,66 @@
1
+ import { composeProjectName } from "../build-spk/resource-worker.js";
2
+ function parsePortRange(value) {
3
+ const match = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(value.trim());
4
+ if (!match) {
5
+ return undefined;
6
+ }
7
+ const start = Number(match[1]);
8
+ const end = Number(match[2] ?? match[1]);
9
+ if (!Number.isSafeInteger(start) ||
10
+ !Number.isSafeInteger(end) ||
11
+ start < 1 ||
12
+ end > 65535 ||
13
+ start > end) {
14
+ return undefined;
15
+ }
16
+ return { start, end };
17
+ }
18
+ function escapeRegExp(value) {
19
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ }
21
+ function entryCoversPort(entry, port) {
22
+ return entry.dst_port.some((value) => {
23
+ const range = parsePortRange(value);
24
+ return range !== undefined && range.start <= port && port <= range.end;
25
+ });
26
+ }
27
+ /**
28
+ * Returns the Docker container name represented by a DSM port row, when the
29
+ * row has Docker's known `port_id` or `desc` shape. Other DSM services are
30
+ * intentionally returned as undefined so they remain conflicts.
31
+ */
32
+ export function dockerContainerName(entry) {
33
+ const portIdMatch = /^(.*)_tcp$/i.exec(entry.port_id.trim());
34
+ if (portIdMatch?.[1]) {
35
+ return portIdMatch[1];
36
+ }
37
+ const descMatch = /^Docker\s+(.+)$/i.exec(entry.desc.trim());
38
+ return descMatch?.[1]?.trim() || undefined;
39
+ }
40
+ export function formatPortOccupant(entry) {
41
+ return dockerContainerName(entry) ?? (entry.desc.trim() || entry.port_id.trim() || entry.name.trim());
42
+ }
43
+ /**
44
+ * Finds occupied published ports, ignoring only containers that are one of
45
+ * the Compose containers for this exact package identity. A same-identity
46
+ * occupant is the existing install being upgraded or reinstalled.
47
+ */
48
+ export function findPortConflicts(input) {
49
+ const conflicts = [];
50
+ const projectName = composeProjectName(input.packageIdentity);
51
+ for (const [port, services] of input.publishedPorts) {
52
+ // Compose names a container <project>-<service>-<replica>. Match the
53
+ // replica as a number rather than assuming -1: a scaled service would
54
+ // otherwise look like a foreign occupant and refuse this package's own
55
+ // redeploy. The service name is still matched exactly, so a different
56
+ // package whose project name merely shares a prefix stays a conflict.
57
+ const ownContainer = (containerName) => services.some((service) => new RegExp(`^${escapeRegExp(`${projectName}-${service}-`)}\\d+$`).test(containerName));
58
+ const occupants = input.portInfo.filter((entry) => entry.protocol.trim().toLowerCase() === "tcp" &&
59
+ entryCoversPort(entry, port) &&
60
+ !ownContainer(dockerContainerName(entry) ?? ""));
61
+ if (occupants.length > 0) {
62
+ conflicts.push({ port, services, occupants });
63
+ }
64
+ }
65
+ return conflicts;
66
+ }
@@ -1,76 +1,20 @@
1
1
  import type { SharePermissionListing } from "../deploy/share-permission-preflight.js";
2
2
  import type { InstalledPackageState } from "./catalog-install.js";
3
- export type AttestationKind = "local" | "app-package" | "none";
4
- export type PackageRoute = "package-only" | "broker-installer" | "unknown";
5
- export type OidcOriginKind = "public-ca" | "quickconnect" | "lan-ip" | "self-signed" | "private-ca" | "http";
6
- export interface HostOidcOriginEvaluation {
7
- ready: boolean;
8
- kind: OidcOriginKind;
9
- hostname?: string;
10
- issuerUrl?: string;
11
- setupHint: string;
12
- }
13
- export interface HostOidcCompatSettings {
14
- enabled: boolean;
15
- origin: HostOidcOriginEvaluation;
16
- }
3
+ import type { PortInfoEntry } from "./port-info.js";
4
+ import type { DsmPortProfileInventory } from "./dsm-port-profile.js";
17
5
  export interface DeployIdentity {
18
6
  packageId: string;
19
7
  displayname: string;
20
8
  developmentInstance: boolean;
21
9
  }
22
- export interface AttestationIssueResult {
23
- kind: "local";
24
- attestationDigest: string;
25
- token?: string;
26
- }
27
- export interface ResidueItem {
28
- kind: "port_profile_sc" | "cm_project_config";
29
- path: string;
30
- name: string;
31
- reason: string;
32
- }
33
- export interface DsmResidueInventory {
34
- items: ResidueItem[];
35
- unreadable: string[];
36
- }
37
- export interface HostAttestationInfo {
38
- packageIdentity: string;
39
- attestationKind: AttestationKind;
40
- developmentInstance: boolean;
41
- openUrl?: string;
42
- readiness?: string;
43
- projectName?: string;
44
- }
45
- export interface HostInstallRecord {
46
- packageId: string;
47
- version?: string;
48
- source?: "package-center" | "local-spk";
49
- extraValues?: Record<string, string>;
50
- }
51
10
  export interface NasHost {
52
11
  readonly dsmOrigin: string;
53
- readonly installationId: string;
54
- requestAttestation(input: {
55
- packageIdentity: string;
56
- contractDigest: string;
57
- bundleDigest: string;
58
- capabilities: string[];
59
- allowCuratedDowngrade?: boolean;
60
- }): Promise<AttestationIssueResult>;
61
- installFromCatalog(input: {
62
- packageId: string;
63
- version?: string;
64
- extraValues?: Record<string, string>;
65
- }): Promise<void>;
66
- installFromLocalSpk?(input: {
12
+ installFromLocalSpk(input: {
67
13
  packageId: string;
68
14
  version: string;
69
15
  spkPath: string;
70
16
  extraValues?: Record<string, string>;
71
17
  }): Promise<void>;
72
- /** Stop and normally uninstall a formal package while preserving App Data. */
73
- uninstallPackage?(packageIdentity: string): Promise<void>;
74
18
  /**
75
19
  * CPU family of this NAS, for picking a native App's target. Returns
76
20
  * undefined when the DSM answer does not map to a family we ship, so the
@@ -88,31 +32,15 @@ export interface NasHost {
88
32
  listSharePermissions?(logicalName: string): Promise<SharePermissionListing | undefined>;
89
33
  /** Group names a DSM user belongs to, for resolving share permission. */
90
34
  listUserGroups?(userName: string): Promise<string[] | undefined>;
35
+ /** Read SYNO.Core.Service.PortInfo/load using an administrator session. */
36
+ listPortInfo?(): Promise<PortInfoEntry[] | undefined>;
91
37
  /** Resolve the NAS-specific user-home root when DSM User Home is enabled. */
92
38
  resolveHomesPath?(): Promise<string | undefined>;
93
- getInstalledProjectEnv?(packageIdentity: string): Promise<Record<string, string> | undefined>;
94
- ensurePackageSource?(origin: string): Promise<void>;
95
39
  getInstalledPackageVersion?(packageIdentity: string): Promise<string | undefined>;
96
40
  getInstalledPackageState?(packageIdentity: string): Promise<InstalledPackageState | undefined>;
97
- getAttestationInfo(packageIdentity: string): Promise<HostAttestationInfo>;
98
41
  isPackageInstalled(packageIdentity: string): Promise<boolean>;
99
42
  fetchContainerLogs(packageIdentity: string): Promise<string[]>;
100
- getDsmDiagnostics?(packageIdentity?: string): Promise<{
101
- dsmResidue?: DsmResidueInventory;
102
- packageRoute?: PackageRoute;
103
- isPackageOnly?: boolean;
104
- [key: string]: unknown;
105
- } | undefined>;
106
- /** Read the optional administrator OIDC compatibility settings endpoint. */
107
- getOidcCompatSettings?(): Promise<HostOidcCompatSettings | undefined>;
108
- smokeRequireLogin(input: {
109
- packageIdentity: string;
110
- cookieHeader: string;
111
- }): Promise<{
112
- ok: boolean;
113
- username?: string;
114
- }>;
115
- markCurated?(packageIdentity: string): void;
43
+ getDsmPortProfileInventory?(): Promise<DsmPortProfileInventory | undefined>;
116
44
  }
117
45
  export interface HostCredentials {
118
46
  dsmOrigin: string;
package/dist/index.d.ts CHANGED
@@ -6,14 +6,5 @@ export { initFromTemplate, isTemplateName } from "./init.js";
6
6
  export { runDev } from "./dev.js";
7
7
  export { testProject } from "./test-command.js";
8
8
  export { buildSpk } from "./build-spk/generator.js";
9
- export { publishProject, resolveCommunityAppsRoot } from "./publish/publish.js";
10
- export { communityAppsGitHubRepo, communityAppsGitHubUrl, DEFAULT_GITHUB_OWNER, } from "./publish/github-repo.js";
11
- export { InMemoryBuildLedger } from "./publish/build-ledger.js";
12
- export type { BuildLedger } from "./publish/build-ledger.js";
13
- export { createCuratedAttestationSignerFromEnv, isPublicOciDigest, releaseCurated, runCuratedRelease, runCuratedReleaseCli, digestReleaseBundle, validateCuratedCompose, } from "./publish/curated-release.js";
14
- export type { CuratedAttestationSignInput, CuratedAttestationSigner, CuratedComposeGateResult, CuratedReleaseSignInput, CuratedReleaseOptions, CuratedReleaseResult, } from "./publish/curated-release.js";
15
- export { canonicalAppPackageClaims, canonicalCuratedReleaseClaims, digestAppPackageClaims, digestCuratedReleaseClaims, normalizeCapabilities, assertPackageVersionBinding, signCuratedAppPackageAttestationToken, signCuratedReleaseAttestationToken, } from "./attestation/app-package.js";
16
- export type { AppPackageAttestationClaims, AppPackageSigningKey, CuratedAppPackageAttestationInput, CuratedReleaseAttestationClaims, CuratedReleaseAttestationInput, } from "./attestation/app-package.js";
17
- export { startMockBroker, FAKE_USER } from "./mock-broker/server.js";
18
9
  export { RecordingComposeRunner } from "./compose/runner.js";
19
10
  export type { ValidationError, ValidationResult } from "./types.js";
package/dist/index.js CHANGED
@@ -5,10 +5,4 @@ export { initFromTemplate, isTemplateName } from "./init.js";
5
5
  export { runDev } from "./dev.js";
6
6
  export { testProject } from "./test-command.js";
7
7
  export { buildSpk } from "./build-spk/generator.js";
8
- export { publishProject, resolveCommunityAppsRoot } from "./publish/publish.js";
9
- export { communityAppsGitHubRepo, communityAppsGitHubUrl, DEFAULT_GITHUB_OWNER, } from "./publish/github-repo.js";
10
- export { InMemoryBuildLedger } from "./publish/build-ledger.js";
11
- export { createCuratedAttestationSignerFromEnv, isPublicOciDigest, releaseCurated, runCuratedRelease, runCuratedReleaseCli, digestReleaseBundle, validateCuratedCompose, } from "./publish/curated-release.js";
12
- export { canonicalAppPackageClaims, canonicalCuratedReleaseClaims, digestAppPackageClaims, digestCuratedReleaseClaims, normalizeCapabilities, assertPackageVersionBinding, signCuratedAppPackageAttestationToken, signCuratedReleaseAttestationToken, } from "./attestation/app-package.js";
13
- export { startMockBroker, FAKE_USER } from "./mock-broker/server.js";
14
8
  export { RecordingComposeRunner } from "./compose/runner.js";
package/dist/info.d.ts CHANGED
@@ -8,7 +8,6 @@ export interface ProjectInfoOptions {
8
8
  export declare function projectInfo(options: ProjectInfoOptions): Promise<ProjectInfoResult>;
9
9
  export interface ProjectInfoResult {
10
10
  packageIdentity: string;
11
- attestationKind: "local" | "app-package" | "none";
12
11
  developmentInstance: boolean;
13
12
  openUrl?: string;
14
13
  readiness?: string;
package/dist/info.js CHANGED
@@ -3,27 +3,20 @@ import { appOriginFromContract } from "./host/open-url.js";
3
3
  import { resolveDeployIdentity } from "./deploy/identity.js";
4
4
  export async function projectInfo(options) {
5
5
  const identity = resolveDeployIdentity(options.project, { dev: options.dev });
6
- const remote = await options.host.getAttestationInfo(identity.packageId);
6
+ const remote = await options.host.getInstalledPackageState?.(identity.packageId);
7
7
  const installed = await options.host.isPackageInstalled(identity.packageId);
8
8
  const contract = loadContract(options.project.contractPath).contract;
9
9
  return {
10
10
  packageIdentity: identity.packageId,
11
- attestationKind: remote.attestationKind,
12
11
  developmentInstance: identity.developmentInstance,
13
- openUrl: remote.openUrl ?? appOriginFromContract(options.host.dsmOrigin, contract),
14
- readiness: remote.readiness ?? (installed ? "UNKNOWN" : "NOT_INSTALLED"),
12
+ openUrl: appOriginFromContract(options.host.dsmOrigin, contract),
13
+ readiness: remote?.status ?? (installed ? "UNKNOWN" : "NOT_INSTALLED"),
15
14
  installed,
16
15
  };
17
16
  }
18
17
  export function formatProjectInfo(info) {
19
- const attestationLabel = info.attestationKind === "app-package"
20
- ? "Curated(App Package Attestation)"
21
- : info.attestationKind === "local"
22
- ? "Local(Local Attestation)"
23
- : "無";
24
18
  return [
25
19
  `Package Identity:${info.packageIdentity}`,
26
- `Attestation:${attestationLabel}`,
27
20
  `開發用 Instance:${info.developmentInstance ? "是" : "否"}`,
28
21
  `已安裝:${info.installed ? "是" : "否"}`,
29
22
  info.openUrl ? `Open URL:${info.openUrl}` : "Open URL:(未設定)",
package/dist/init.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import type { TemplateName } from "./types.js";
2
2
  export declare function isTemplateName(value: string): value is TemplateName;
3
3
  export declare function listTemplates(): TemplateName[];
4
- export declare function initFromTemplate(template: TemplateName, targetDir: string): void;
4
+ export interface InitOptions {
5
+ /** Standalone templates: INFO.package (default: from the directory name). */
6
+ packageId?: string;
7
+ /** Standalone templates: display name (default: from the directory name). */
8
+ displayName?: string;
9
+ /** native-go: "root" adds administrator-approved root operations (default "none"). */
10
+ privilege?: "none" | "root";
11
+ ui?: "plain" | "vue";
12
+ integration?: "go" | "better-auth";
13
+ }
14
+ export declare function initFromTemplate(template: TemplateName, targetDir: string, options?: InitOptions): void;
package/dist/init.js CHANGED
@@ -1,12 +1,13 @@
1
- import { mkdirSync, cpSync, existsSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  const TEMPLATE_NAMES = [
5
5
  "hello-web",
6
6
  "hello-web-page",
7
7
  "hello-files",
8
8
  "compose-import",
9
- "native",
9
+ "binary",
10
+ "native-go",
10
11
  ];
11
12
  export function isTemplateName(value) {
12
13
  return TEMPLATE_NAMES.includes(value);
@@ -22,11 +23,118 @@ function templatesRoot() {
22
23
  }
23
24
  return join(moduleDir, "..", "templates");
24
25
  }
25
- export function initFromTemplate(template, targetDir) {
26
+ /**
27
+ * native-go --privilege root: files laid over the native-go template, and the
28
+ * files they replace.
29
+ */
30
+ const ROOT_OVERLAY = "native-go-root";
31
+ const ROOT_REPLACED = ["backend/internal/app/root_none.go"];
32
+ /** Templates whose identity is filled in at init (placeholders in these files). */
33
+ const SUBSTITUTED = {
34
+ "native-go": ["dsm-package.yaml", "README.md"],
35
+ };
36
+ const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
37
+ /** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
38
+ const RENAMED = { gitignore: ".gitignore" };
39
+ function words(dir) {
40
+ return basename(dir).split(/[^A-Za-z0-9]+/).filter(Boolean);
41
+ }
42
+ function defaultPackageId(dir) {
43
+ const id = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
44
+ return PACKAGE_ID.test(id) ? id : "MyPackage";
45
+ }
46
+ function defaultDisplayName(dir) {
47
+ const name = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
48
+ return name || "My Package";
49
+ }
50
+ function templateFiles(root, dir = root) {
51
+ return readdirSync(dir).flatMap((name) => {
52
+ const path = join(dir, name);
53
+ return statSync(path).isDirectory() ? templateFiles(root, path) : [relative(root, path)];
54
+ });
55
+ }
56
+ function targetName(file) {
57
+ return RENAMED[file] ?? file;
58
+ }
59
+ export function initFromTemplate(template, targetDir, options = {}) {
26
60
  const source = join(templatesRoot(), template);
27
61
  if (!existsSync(source)) {
28
62
  throw new Error(`找不到模板:${template}`);
29
63
  }
64
+ const substitutions = substitutionsFor(template, targetDir, options);
65
+ const privilege = options.privilege ?? (options.integration ? "root" : "none");
66
+ const overlays = [
67
+ ...(privilege === "root" ? [ROOT_OVERLAY] : []),
68
+ ...(options.ui === "vue" ? ["native-go-vue"] : []),
69
+ ...(options.integration ? ["native-go-integration"] : []),
70
+ ...(options.integration === "better-auth" ? ["native-go-better-auth"] : []),
71
+ ].map(name => join(templatesRoot(), name));
72
+ const files = [...templateFiles(source), ...overlays.flatMap(root => templateFiles(root))];
73
+ const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
74
+ if (collisions.length > 0) {
75
+ throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
76
+ }
30
77
  mkdirSync(targetDir, { recursive: true });
31
78
  cpSync(source, targetDir, { recursive: true });
79
+ for (const overlay of overlays)
80
+ cpSync(overlay, targetDir, { recursive: true });
81
+ if (privilege === "root")
82
+ ROOT_REPLACED.forEach(file => rmSync(join(targetDir, file)));
83
+ for (const [from, to] of Object.entries(RENAMED)) {
84
+ if (files.includes(from)) {
85
+ renameSync(join(targetDir, from), join(targetDir, to));
86
+ }
87
+ }
88
+ for (const file of new Set([...(SUBSTITUTED[template] ?? []), ...overlays.flatMap(root => templateFiles(root))])) {
89
+ const path = join(targetDir, targetName(file));
90
+ let text = readFileSync(path, "utf8");
91
+ for (const [token, value] of Object.entries(substitutions)) {
92
+ text = text.replaceAll(token, value);
93
+ }
94
+ writeFileSync(path, text);
95
+ }
96
+ if (options.ui === "vue") {
97
+ const manifest = join(targetDir, "dsm-package.yaml");
98
+ writeFileSync(manifest, readFileSync(manifest, "utf8").replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}"));
99
+ rmSync(join(targetDir, "ui"), { recursive: true });
100
+ }
101
+ if (options.integration === "better-auth")
102
+ rmSync(join(targetDir, "backend/internal/app/identity_none.go"));
103
+ if (options.integration) {
104
+ const mod = join(targetDir, "backend/go.mod");
105
+ writeFileSync(mod, readFileSync(mod, "utf8").replace("go 1.24", "go 1.25"));
106
+ const manifest = join(targetDir, "dsm-package.yaml");
107
+ writeFileSync(manifest, readFileSync(manifest, "utf8") + "\nintegration:\n protocol: 1\n workers: {}\n" + (options.integration === "better-auth" ? " identity: better-auth\n" : ""));
108
+ const routes = join(targetDir, "backend/internal/app/routes.go");
109
+ writeFileSync(routes, readFileSync(routes, "utf8").replace("return append(routes, cfg.rootRoutes()...)", "routes = append(routes, cfg.integrationRoutes()...)\n\treturn append(routes, cfg.rootRoutes()...)"));
110
+ }
111
+ }
112
+ function substitutionsFor(template, targetDir, options) {
113
+ if (options.privilege !== undefined && template !== "native-go") {
114
+ throw new Error("[INIT_PRIVILEGE_UNSUPPORTED] --privilege 只適用於 -t native-go。");
115
+ }
116
+ if ((options.ui !== undefined || options.integration !== undefined) && template !== "native-go")
117
+ throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui/--integration 只適用於 native-go。");
118
+ if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
119
+ throw new Error("[INIT_UI_INVALID] --ui 只接受 plain 或 vue。");
120
+ if (options.integration !== undefined && !["go", "better-auth"].includes(options.integration))
121
+ throw new Error("[INIT_INTEGRATION_INVALID] --integration 只接受 go 或 better-auth。");
122
+ if (options.integration && (options.ui !== "vue" || options.privilege === "none"))
123
+ throw new Error("[INIT_INTEGRATION_OPTIONS] integration 需要 --ui vue 與 root privilege(自動選用)。");
124
+ const privilege = options.privilege ?? (options.integration ? "root" : "none");
125
+ if (privilege !== "none" && privilege !== "root") {
126
+ throw new Error(`[INIT_PRIVILEGE_INVALID] --privilege 只接受 none 或 root,收到「${String(privilege)}」。`);
127
+ }
128
+ if (!SUBSTITUTED[template]) {
129
+ return {};
130
+ }
131
+ const packageId = options.packageId ?? defaultPackageId(targetDir);
132
+ if (!PACKAGE_ID.test(packageId)) {
133
+ throw new Error(`[INIT_PACKAGE_INVALID] package「${packageId}」必須以英文字母開頭,只含英數字(2–64 字),例如 PhotoTagger。`);
134
+ }
135
+ const displayName = options.displayName ?? defaultDisplayName(targetDir);
136
+ if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
137
+ throw new Error("[INIT_DISPLAY_NAME_INVALID] 顯示名稱必須是單行,且不含 \" 或 \\。");
138
+ }
139
+ return { __PACKAGE__: packageId, __DISPLAY_NAME__: displayName, __PRIVILEGE__: privilege };
32
140
  }
package/dist/log.js CHANGED
@@ -6,14 +6,13 @@ export async function followProjectLogs(options) {
6
6
  return { ok: false, lines: [], diagnostics: { code: "NOT_INSTALLED" } };
7
7
  }
8
8
  const lines = await options.host.fetchContainerLogs(identity.packageId);
9
- const info = await options.host.getAttestationInfo(identity.packageId);
9
+ const info = await options.host.getInstalledPackageState?.(identity.packageId);
10
10
  return {
11
11
  ok: true,
12
12
  lines,
13
13
  diagnostics: {
14
14
  packageIdentity: identity.packageId,
15
- attestationKind: info.attestationKind,
16
- readiness: info.readiness,
15
+ readiness: info?.status,
17
16
  redacted: true,
18
17
  },
19
18
  };
@@ -11,7 +11,6 @@ function nativeSpan(file, doc, pathSegments, pathString) {
11
11
  return { file, line: pos.line, column: pos.column, path: dotPath };
12
12
  }
13
13
  const HEX64_REGEX = /^[0-9a-fA-F]{64}$/;
14
- const FORBIDDEN_SERVICE_FIELDS = ["user", "command", "pre_start", "post_start", "shell"];
15
14
  export function validateNative(parsed, nativeFile, options = {}) {
16
15
  const errors = [];
17
16
  const profile = options.profile ?? "personal";
@@ -0,0 +1,9 @@
1
+ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
+ import type { StandaloneError } from "./manifest.js";
3
+ /**
4
+ * Checks a prebuilt CGI before it is packaged: a regular file inside the
5
+ * project, a 64-bit little-endian ELF for `arch`, and statically linked
6
+ * (no PT_INTERP), because DSM models ship different libc versions.
7
+ * Returns undefined when the file is acceptable.
8
+ */
9
+ export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily): StandaloneError | undefined;
@@ -0,0 +1,67 @@
1
+ import { closeSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
2
+ import { relative } from "node:path";
3
+ const ELF_MACHINE = { x86_64: 62, aarch64: 183 };
4
+ const PT_INTERP = 3;
5
+ const MAX_BINARY_BYTES = 256 * 1024 * 1024;
6
+ const HEADER_BYTES = 64 * 1024;
7
+ function fail(code, message) {
8
+ return { code, message };
9
+ }
10
+ /**
11
+ * Checks a prebuilt CGI before it is packaged: a regular file inside the
12
+ * project, a 64-bit little-endian ELF for `arch`, and statically linked
13
+ * (no PT_INTERP), because DSM models ship different libc versions.
14
+ * Returns undefined when the file is acceptable.
15
+ */
16
+ export function checkCgiBinary(projectRoot, file, arch) {
17
+ let stat;
18
+ try {
19
+ stat = lstatSync(file);
20
+ }
21
+ catch {
22
+ return fail("STANDALONE_BINARY_MISSING", `找不到 ${arch} 的 backend 執行檔:${file}`);
23
+ }
24
+ if (!stat.isFile()) {
25
+ return fail("STANDALONE_BINARY_NOT_REGULAR", `${file} 必須是一般檔案(不可是 symlink 或目錄)。`);
26
+ }
27
+ const rel = relative(realpathSync(projectRoot), realpathSync(file));
28
+ if (rel.startsWith("..")) {
29
+ return fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${file} 實際位於專案目錄之外。`);
30
+ }
31
+ if (stat.size > MAX_BINARY_BYTES) {
32
+ return fail("STANDALONE_BINARY_TOO_LARGE", `${file} 超過 256 MiB。`);
33
+ }
34
+ return checkElf(readHeader(file), file, arch);
35
+ }
36
+ function readHeader(file) {
37
+ const fd = openSync(file, "r");
38
+ try {
39
+ const buf = Buffer.alloc(HEADER_BYTES);
40
+ return buf.subarray(0, readSync(fd, buf, 0, HEADER_BYTES, 0));
41
+ }
42
+ finally {
43
+ closeSync(fd);
44
+ }
45
+ }
46
+ function checkElf(buf, file, arch) {
47
+ const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file} 不是 64-bit little-endian Linux ELF 執行檔。`);
48
+ if (buf.length < 64 || buf.toString("latin1", 0, 4) !== "\x7fELF" || buf[4] !== 2 || buf[5] !== 1) {
49
+ return notElf;
50
+ }
51
+ const machine = buf.readUInt16LE(18);
52
+ if (machine !== ELF_MACHINE[arch]) {
53
+ return fail("STANDALONE_BINARY_ARCH_MISMATCH", `${file} 的 ELF machine 是 ${machine},不是 ${arch}。`);
54
+ }
55
+ const phoff = Number(buf.readBigUInt64LE(32));
56
+ const phentsize = buf.readUInt16LE(54);
57
+ const phnum = buf.readUInt16LE(56);
58
+ if (phentsize < 4 || phoff + phentsize * phnum > buf.length) {
59
+ return notElf;
60
+ }
61
+ for (let i = 0; i < phnum; i++) {
62
+ if (buf.readUInt32LE(phoff + i * phentsize) === PT_INTERP) {
63
+ return fail("STANDALONE_BINARY_DYNAMIC", `${file} 是動態連結執行檔。請改用靜態連結(Go:CGO_ENABLED=0;Rust:*-unknown-linux-musl)。`);
64
+ }
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,25 @@
1
+ import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
+ export interface CommandResult {
3
+ status: number | null;
4
+ stderr: string;
5
+ /** The program itself was not found. */
6
+ missing?: boolean;
7
+ }
8
+ export type CommandRunner = (command: string, args: string[], options: {
9
+ cwd: string;
10
+ env: Record<string, string | undefined>;
11
+ }) => CommandResult;
12
+ export interface StandaloneBuildOptions {
13
+ root: string;
14
+ /** Defaults to every arch in the manifest. */
15
+ arches?: NativeArchFamily[];
16
+ /** Defaults to `<root>/dist`. */
17
+ outDir?: string;
18
+ run?: CommandRunner;
19
+ }
20
+ /** Argv only, never a shell; output streams to the terminal except stderr, which is also kept. */
21
+ export declare const runCommand: CommandRunner;
22
+ /** `build-spk` for a Standalone Package: build the UI, compile the CGI, pack one SPK per arch. */
23
+ export declare function buildStandalone(options: StandaloneBuildOptions): Promise<{
24
+ outputs: string[];
25
+ }>;