@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,161 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * UI languages a Standalone Package can ship, as BCP-47 tags (the catalog
5
+ * file names and the `Intl` locale), with the DSM language code each one
6
+ * answers to. Every other DSM language opens the app in English.
7
+ */
8
+ export const LOCALES = ["en", "zh-TW", "zh-CN", "ja", "ko", "de", "fr", "es", "it"];
9
+ export const DSM_LANGS = {
10
+ en: "enu",
11
+ "zh-TW": "cht",
12
+ "zh-CN": "chs",
13
+ ja: "jpn",
14
+ ko: "krn",
15
+ de: "ger",
16
+ fr: "fre",
17
+ es: "spn",
18
+ it: "ita",
19
+ };
20
+ const MESSAGE_KEY = /^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)*$/;
21
+ export function isLocale(value) {
22
+ return LOCALES.includes(value);
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function placeholders(text) {
28
+ return [...new Set([...text.matchAll(/\{(\w+)\}/g)].map((m) => m[1]))].sort().join(",");
29
+ }
30
+ function readCatalog(file, locale) {
31
+ const invalid = (why) => ({
32
+ error: { code: "STANDALONE_I18N_CATALOG_INVALID", message: `${locale}.json ${why}` },
33
+ });
34
+ let raw;
35
+ try {
36
+ raw = JSON.parse(readFileSync(file, "utf8"));
37
+ }
38
+ catch (error) {
39
+ return invalid(`不是合法的 JSON:${String(error)}`);
40
+ }
41
+ if (!isRecord(raw)) {
42
+ return invalid("必須是 { \"key\": \"text\" } 的 JSON 物件。");
43
+ }
44
+ for (const [key, text] of Object.entries(raw)) {
45
+ if (!MESSAGE_KEY.test(key) || typeof text !== "string") {
46
+ return invalid(`的「${key}」必須是點分隔的 key(例如 greeting.save)對應一段文字。`);
47
+ }
48
+ }
49
+ return { catalog: raw };
50
+ }
51
+ /** `<dir>/<locale>.json` for each shipped locale; en.json is required. */
52
+ export function loadCatalogs(dir) {
53
+ const errors = [];
54
+ const catalogs = {};
55
+ const files = existsSync(dir) ? readdirSync(dir).filter((name) => name.endsWith(".json")).sort() : [];
56
+ for (const name of files) {
57
+ const locale = name.slice(0, -".json".length);
58
+ if (!isLocale(locale)) {
59
+ errors.push({
60
+ code: "STANDALONE_I18N_LOCALE_UNSUPPORTED",
61
+ message: `${name}:只支援 ${LOCALES.join(", ")};其他 DSM 語言會顯示英文。`,
62
+ });
63
+ continue;
64
+ }
65
+ const read = readCatalog(join(dir, name), locale);
66
+ if ("error" in read) {
67
+ errors.push(read.error);
68
+ }
69
+ else {
70
+ catalogs[locale] = read.catalog;
71
+ }
72
+ }
73
+ const en = catalogs.en;
74
+ if (!en) {
75
+ if (!errors.some((e) => e.code === "STANDALONE_I18N_CATALOG_INVALID")) {
76
+ errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir} 缺少 en.json(所有語言的後備文字)。` });
77
+ }
78
+ return { ok: false, errors };
79
+ }
80
+ for (const [locale, catalog] of Object.entries(catalogs)) {
81
+ errors.push(...compareWithEnglish(locale, catalog, en));
82
+ }
83
+ return errors.length === 0 ? { ok: true, catalogs: catalogs } : { ok: false, errors };
84
+ }
85
+ function compareWithEnglish(locale, catalog, en) {
86
+ const missing = Object.keys(en).filter((key) => !(key in catalog));
87
+ const extra = Object.keys(catalog).filter((key) => !(key in en));
88
+ if (missing.length > 0 || extra.length > 0) {
89
+ const parts = [
90
+ missing.length > 0 ? `缺少 ${missing.join(", ")}` : "",
91
+ extra.length > 0 ? `多出 ${extra.join(", ")}` : "",
92
+ ].filter(Boolean);
93
+ return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json 與 en.json 的 key 不同:${parts.join(";")}。` }];
94
+ }
95
+ const drifted = Object.keys(en).filter((key) => placeholders(catalog[key]) !== placeholders(en[key]));
96
+ return drifted.length === 0
97
+ ? []
98
+ : [{
99
+ code: "STANDALONE_I18N_PLACEHOLDER_MISMATCH",
100
+ message: `${locale}.json 的 ${drifted.join(", ")} 與 en.json 的 {placeholder} 不一致。`,
101
+ }];
102
+ }
103
+ function readGlossary(file) {
104
+ const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}:${why}` });
105
+ let raw;
106
+ try {
107
+ raw = JSON.parse(readFileSync(file, "utf8"));
108
+ }
109
+ catch (error) {
110
+ return invalid(`不是合法的 JSON:${String(error)}`);
111
+ }
112
+ if (!isRecord(raw) || !Array.isArray(raw.terms)) {
113
+ return invalid("必須是 { \"terms\": [...] }。");
114
+ }
115
+ const terms = [];
116
+ for (const term of raw.terms) {
117
+ const ok = isRecord(term)
118
+ && typeof term.id === "string"
119
+ && isRecord(term.text)
120
+ && Object.entries(term.text).every(([locale, text]) => isLocale(locale) && typeof text === "string" && text !== "")
121
+ && (term.keys === undefined || (Array.isArray(term.keys) && term.keys.every((key) => typeof key === "string")));
122
+ if (!ok) {
123
+ return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"} 需要 id、以支援語言為 key 的 text,以及選填的 keys 清單。`);
124
+ }
125
+ terms.push({ id: term.id, text: term.text, keys: term.keys ?? [] });
126
+ }
127
+ return terms;
128
+ }
129
+ /** Case-insensitive: a term can start a sentence or sit inside one ("Backend status"). */
130
+ function containsTerm(message, term, locale) {
131
+ return message.toLocaleLowerCase(locale).includes(term.toLocaleLowerCase(locale));
132
+ }
133
+ /**
134
+ * Each glossary term lists the message keys that must use it: in every
135
+ * shipped locale the message has to contain that locale's official term.
136
+ */
137
+ export function checkGlossary(file, catalogs) {
138
+ const terms = readGlossary(file);
139
+ if (!Array.isArray(terms)) {
140
+ return [terms];
141
+ }
142
+ const errors = [];
143
+ for (const term of terms) {
144
+ for (const key of term.keys) {
145
+ if (!(key in catalogs.en)) {
146
+ errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id} 指向不存在的 key ${key}。` });
147
+ continue;
148
+ }
149
+ for (const [locale, catalog] of Object.entries(catalogs)) {
150
+ const official = term.text[locale];
151
+ if (official !== undefined && !containsTerm(catalog[key], official, locale)) {
152
+ errors.push({
153
+ code: "STANDALONE_I18N_GLOSSARY_MISMATCH",
154
+ message: `${locale}.json 的 ${key}「${catalog[key]}」沒有使用 Synology 用語「${official}」(glossary term ${term.id})。`,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return errors;
161
+ }
@@ -0,0 +1,21 @@
1
+ export type SpkKind = "native-go" | "binary" | "container";
2
+ export interface SpkInspection {
3
+ kind?: SpkKind;
4
+ package?: string;
5
+ version?: string;
6
+ arch?: string;
7
+ /** Whether the SPK carries a Synology package signature. */
8
+ signed: boolean;
9
+ /** Payload path of the package CGI, when present with mode 0755. */
10
+ cgi?: string;
11
+ payload: Array<{
12
+ path: string;
13
+ mode: string;
14
+ }>;
15
+ /** Anything that breaks the unsigned, package-account-only contract. */
16
+ findings: string[];
17
+ }
18
+ /** Reads an SPK without installing it and reports how it would run on DSM. */
19
+ export declare function inspectStandaloneSpk(spkPath: string): Promise<SpkInspection>;
20
+ /** Auto-detect the generated runtime; retain strict native-go inspection when requested. */
21
+ export declare function inspectSpk(spkPath: string, expectedKind?: SpkKind): Promise<SpkInspection>;
@@ -0,0 +1,160 @@
1
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import * as tar from "tar";
5
+ import { CGI_PAYLOAD_PATH } from "./dsm-window.js";
6
+ const ROOT_SCRIPT_HINTS = [/\bsudo\b/, /\bchown\s+(-\w+\s+)*root\b/, /chmod\s+[ugo]*\+s\b/, /chmod\s+[2467][0-7]{3}\b/];
7
+ /** Reads an SPK without installing it and reports how it would run on DSM. */
8
+ export async function inspectStandaloneSpk(spkPath) {
9
+ return inspectSpk(spkPath, "native-go");
10
+ }
11
+ /** Auto-detect the generated runtime; retain strict native-go inspection when requested. */
12
+ export async function inspectSpk(spkPath, expectedKind) {
13
+ const dir = mkdtempSync(join(tmpdir(), "syno-inspect-spk-"));
14
+ try {
15
+ await tar.x({ file: spkPath, cwd: dir });
16
+ const findings = [];
17
+ const payload = await listPayload(join(dir, "package.tgz"), findings);
18
+ const paths = new Set(payload.map(entry => entry.path));
19
+ const kind = expectedKind ?? (paths.has(CGI_PAYLOAD_PATH) ? "native-go"
20
+ : paths.has("native.yaml") ? "binary" : paths.has("compose.yaml") ? "container" : "native-go");
21
+ const identityPayload = [CGI_PAYLOAD_PATH, "integration/helper", "integration/inventory.json", "integration/workers.json", "integration/identity/app.mjs"].every(path => paths.has(path));
22
+ const info = readInfo(join(dir, "INFO"), findings, kind, identityPayload);
23
+ checkConf(dir, findings, kind);
24
+ checkScripts(dir, findings);
25
+ const cgiEntry = payload.find((entry) => entry.path === CGI_PAYLOAD_PATH);
26
+ if (kind === "native-go" && !cgiEntry) {
27
+ findings.push(`payload 沒有 ${CGI_PAYLOAD_PATH}`);
28
+ }
29
+ else if (cgiEntry && cgiEntry.mode !== "0755") {
30
+ findings.push(`${CGI_PAYLOAD_PATH} 的 mode 是 ${cgiEntry.mode},應為 0755`);
31
+ }
32
+ return {
33
+ kind,
34
+ package: info.package,
35
+ version: info.version,
36
+ arch: info.arch,
37
+ signed: existsSync(join(dir, "syno_signature.asc")),
38
+ cgi: cgiEntry?.mode === "0755" ? CGI_PAYLOAD_PATH : undefined,
39
+ payload,
40
+ findings,
41
+ };
42
+ }
43
+ finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ }
47
+ function readInfo(path, findings, kind, identityPayload) {
48
+ if (!existsSync(path)) {
49
+ findings.push("缺少 INFO");
50
+ return {};
51
+ }
52
+ const info = {};
53
+ for (const line of readFileSync(path, "utf8").split("\n")) {
54
+ const match = line.match(/^([a-z_]+)="(.*)"$/);
55
+ if (match) {
56
+ info[match[1]] = match[2];
57
+ }
58
+ }
59
+ for (const key of ["package", "version", "arch", "os_min_ver"]) {
60
+ if (!info[key]) {
61
+ findings.push(`INFO 缺少 ${key}`);
62
+ }
63
+ }
64
+ if (kind === "native-go" && (info.thirdparty !== "yes" || info.dsmuidir !== "ui")) {
65
+ findings.push('INFO 必須有 thirdparty="yes" 與 dsmuidir="ui",DSM 才會提供 /webman/3rdparty/<package>/');
66
+ }
67
+ const allowedDependency = kind === "container"
68
+ ? info.install_dep_packages?.split(":").every(dep => /^(WebStation|ContainerManager)([><=].*)?$/.test(dep))
69
+ : kind === "native-go" && identityPayload && info.install_dep_packages === "Node.js_v22";
70
+ if (info.install_dep_packages && !allowedDependency) {
71
+ findings.push(`INFO 依賴其他套件:${info.install_dep_packages}`);
72
+ }
73
+ return info;
74
+ }
75
+ function checkConf(dir, findings, kind) {
76
+ const confDir = join(dir, "conf");
77
+ const entries = existsSync(confDir) ? readdirSync(confDir) : [];
78
+ for (const name of entries) {
79
+ if (name !== "privilege" && !(kind === "container" && name === "resource")) {
80
+ findings.push(`conf/${name} 會向 DSM 註冊額外資源;Standalone Package 不產生它`);
81
+ }
82
+ }
83
+ if (kind === "container") {
84
+ try {
85
+ const resource = JSON.parse(readFileSync(join(confDir, "resource"), "utf8"));
86
+ if (!resource["docker-project"])
87
+ findings.push("container 缺少 docker-project resource");
88
+ for (const key of Object.keys(resource)) {
89
+ if (!["docker-project", "webservice"].includes(key))
90
+ findings.push(`未知 container resource: ${key}`);
91
+ }
92
+ }
93
+ catch {
94
+ findings.push("container 缺少有效的 conf/resource");
95
+ }
96
+ }
97
+ if (!entries.includes("privilege")) {
98
+ findings.push("缺少 conf/privilege(DSM 7 必須以 package 帳號執行)");
99
+ return;
100
+ }
101
+ let privilege;
102
+ try {
103
+ privilege = JSON.parse(readFileSync(join(confDir, "privilege"), "utf8"));
104
+ }
105
+ catch {
106
+ findings.push("conf/privilege 不是 JSON");
107
+ return;
108
+ }
109
+ const defaults = privilege.defaults;
110
+ if (defaults?.["run-as"] !== "package") {
111
+ findings.push('conf/privilege 的 defaults.run-as 必須是 "package"');
112
+ }
113
+ for (const key of ["tool", "ctrl-script", "executable", "join-groupname"]) {
114
+ if (key in privilege) {
115
+ findings.push(`conf/privilege 要求 ${key}(root 或額外群組權限)`);
116
+ }
117
+ }
118
+ }
119
+ function checkScripts(dir, findings) {
120
+ const scriptsDir = join(dir, "scripts");
121
+ if (!existsSync(join(scriptsDir, "start-stop-status"))) {
122
+ findings.push("缺少 scripts/start-stop-status");
123
+ }
124
+ for (const name of existsSync(scriptsDir) ? readdirSync(scriptsDir) : []) {
125
+ const body = readFileSync(join(scriptsDir, name), "utf8");
126
+ if (ROOT_SCRIPT_HINTS.some((pattern) => pattern.test(body))) {
127
+ findings.push(`scripts/${name} 嘗試取得 root 權限(sudo、chown root 或 setuid)`);
128
+ }
129
+ }
130
+ }
131
+ async function listPayload(packageTgz, findings) {
132
+ if (!existsSync(packageTgz)) {
133
+ findings.push("缺少 package.tgz");
134
+ return [];
135
+ }
136
+ const payload = [];
137
+ await tar.t({
138
+ file: packageTgz,
139
+ onReadEntry: (entry) => {
140
+ const path = entry.path.replace(/^\.\//, "");
141
+ const mode = (entry.mode ?? 0) & 0o7777;
142
+ if (entry.type === "SymbolicLink" || entry.type === "Link") {
143
+ findings.push(`payload 含連結:${path}`);
144
+ }
145
+ if (path.startsWith("/") || path.split("/").includes("..")) {
146
+ findings.push(`payload 路徑跳出套件目錄:${path}`);
147
+ }
148
+ if (mode & 0o6000) {
149
+ findings.push(`payload 含 setuid/setgid 檔案:${path}`);
150
+ }
151
+ if (mode & 0o002) {
152
+ findings.push(`payload 含所有人可寫的檔案:${path}`);
153
+ }
154
+ if (entry.type === "File") {
155
+ payload.push({ path, mode: mode.toString(8).padStart(4, "0") });
156
+ }
157
+ },
158
+ });
159
+ return payload;
160
+ }
@@ -0,0 +1,71 @@
1
+ import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
2
+ import { type Catalogs, type Locale } from "./i18n.js";
3
+ import { type RootOp } from "./root-ops.js";
4
+ import { type Integration } from "./helper.js";
5
+ /**
6
+ * `dsm-package.yaml`: a Standalone Package, i.e. an unsigned native SPK that
7
+ * runs as its own DSM package account with a CGI backend and a DSM window UI.
8
+ */
9
+ export declare const STANDALONE_MANIFEST = "dsm-package.yaml";
10
+ export type StandaloneBackend = {
11
+ kind: "go";
12
+ dir: string;
13
+ main: string;
14
+ } | {
15
+ kind: "binary";
16
+ files: Partial<Record<NativeArchFamily, string>>;
17
+ };
18
+ export interface InfoText {
19
+ displayname?: string;
20
+ description?: string;
21
+ }
22
+ export interface StandaloneI18n {
23
+ /** UI messages per locale, embedded into the DSM window. */
24
+ catalogs?: Catalogs;
25
+ /** INFO `displayname_<dsm lang>` / `description_<dsm lang>`. */
26
+ info: Partial<Record<Locale, InfoText>>;
27
+ }
28
+ export interface StandaloneManifest {
29
+ root: string;
30
+ package: string;
31
+ version: string;
32
+ build: number;
33
+ /** INFO version: `<version>-<build, 4 digits>`. */
34
+ spkVersion: string;
35
+ displayname: string;
36
+ description: string;
37
+ maintainer: string;
38
+ os_min_ver: string;
39
+ arches: NativeArchFamily[];
40
+ backend: StandaloneBackend;
41
+ ui: {
42
+ dir: string;
43
+ build?: {
44
+ cwd: string;
45
+ command: string[];
46
+ };
47
+ };
48
+ i18n?: StandaloneI18n;
49
+ /** `none`: the package account only. `root`: adds DSM-approved root tasks. */
50
+ privilege: "none" | "root";
51
+ /** `root/<name>.sh`, present exactly when privilege is root. */
52
+ rootOps?: RootOp[];
53
+ integration?: Integration;
54
+ }
55
+ export interface StandaloneError {
56
+ code: string;
57
+ message: string;
58
+ }
59
+ export type ManifestResult = {
60
+ ok: true;
61
+ manifest: StandaloneManifest;
62
+ } | {
63
+ ok: false;
64
+ errors: StandaloneError[];
65
+ };
66
+ export declare function loadStandaloneManifest(root: string): ManifestResult;
67
+ /**
68
+ * The nearest directory at or above `start` that holds dsm-package.yaml,
69
+ * unless a Managed Profile project (synology-app.yaml) is found first.
70
+ */
71
+ export declare function findStandaloneRoot(start: string): string | null;