@synopackageland/cli 0.1.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 (170) hide show
  1. package/README.md +13 -0
  2. package/dist/attestation/app-package.d.ts +86 -0
  3. package/dist/attestation/app-package.js +199 -0
  4. package/dist/attestation/install-on-host.d.ts +14 -0
  5. package/dist/attestation/install-on-host.js +52 -0
  6. package/dist/attestation/keyring.d.ts +21 -0
  7. package/dist/attestation/keyring.js +168 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +14 -0
  10. package/dist/build-spk/dsm-arch.d.ts +4 -0
  11. package/dist/build-spk/dsm-arch.js +32 -0
  12. package/dist/build-spk/dsm-icons.d.ts +2 -0
  13. package/dist/build-spk/dsm-icons.js +67 -0
  14. package/dist/build-spk/generator.d.ts +43 -0
  15. package/dist/build-spk/generator.js +653 -0
  16. package/dist/build-spk/native-payload.d.ts +22 -0
  17. package/dist/build-spk/native-payload.js +654 -0
  18. package/dist/build-spk/reproducible-archive.d.ts +8 -0
  19. package/dist/build-spk/reproducible-archive.js +34 -0
  20. package/dist/cli.d.ts +1 -0
  21. package/dist/cli.js +345 -0
  22. package/dist/compose/analyze.d.ts +25 -0
  23. package/dist/compose/analyze.js +346 -0
  24. package/dist/compose/env.d.ts +7 -0
  25. package/dist/compose/env.js +36 -0
  26. package/dist/compose/privilege.d.ts +14 -0
  27. package/dist/compose/privilege.js +102 -0
  28. package/dist/compose/runner.d.ts +32 -0
  29. package/dist/compose/runner.js +84 -0
  30. package/dist/contract/load.d.ts +25 -0
  31. package/dist/contract/load.js +87 -0
  32. package/dist/contract/schema-validate.d.ts +3 -0
  33. package/dist/contract/schema-validate.js +50 -0
  34. package/dist/contract/validate.d.ts +3 -0
  35. package/dist/contract/validate.js +80 -0
  36. package/dist/contract/web-launch.d.ts +19 -0
  37. package/dist/contract/web-launch.js +94 -0
  38. package/dist/deploy/catalog-publish.d.ts +3 -0
  39. package/dist/deploy/catalog-publish.js +7 -0
  40. package/dist/deploy/deploy.d.ts +22 -0
  41. package/dist/deploy/deploy.js +136 -0
  42. package/dist/deploy/identity-shared.d.ts +3 -0
  43. package/dist/deploy/identity-shared.js +14 -0
  44. package/dist/deploy/identity.d.ts +7 -0
  45. package/dist/deploy/identity.js +20 -0
  46. package/dist/deploy/package-source.d.ts +7 -0
  47. package/dist/deploy/package-source.js +17 -0
  48. package/dist/deploy/run-as-values.d.ts +18 -0
  49. package/dist/deploy/run-as-values.js +48 -0
  50. package/dist/deploy/share-values.d.ts +20 -0
  51. package/dist/deploy/share-values.js +60 -0
  52. package/dist/deploy/spk-info.d.ts +12 -0
  53. package/dist/deploy/spk-info.js +41 -0
  54. package/dist/deploy/spk-version.d.ts +4 -0
  55. package/dist/deploy/spk-version.js +64 -0
  56. package/dist/dev.d.ts +13 -0
  57. package/dist/dev.js +31 -0
  58. package/dist/discovery.d.ts +8 -0
  59. package/dist/discovery.js +22 -0
  60. package/dist/errors.d.ts +6 -0
  61. package/dist/errors.js +18 -0
  62. package/dist/host/broker-paths.d.ts +1 -0
  63. package/dist/host/broker-paths.js +1 -0
  64. package/dist/host/catalog-install.d.ts +59 -0
  65. package/dist/host/catalog-install.js +326 -0
  66. package/dist/host/credentials.d.ts +2 -0
  67. package/dist/host/credentials.js +37 -0
  68. package/dist/host/digests.d.ts +5 -0
  69. package/dist/host/digests.js +55 -0
  70. package/dist/host/dsm-client.d.ts +6 -0
  71. package/dist/host/dsm-client.js +290 -0
  72. package/dist/host/dsm-http.d.ts +1 -0
  73. package/dist/host/dsm-http.js +87 -0
  74. package/dist/host/fake-nas.d.ts +16 -0
  75. package/dist/host/fake-nas.js +116 -0
  76. package/dist/host/open-url.d.ts +5 -0
  77. package/dist/host/open-url.js +13 -0
  78. package/dist/host/share-lookup.d.ts +3 -0
  79. package/dist/host/share-lookup.js +144 -0
  80. package/dist/host/types.d.ts +67 -0
  81. package/dist/host/types.js +1 -0
  82. package/dist/index.d.ts +19 -0
  83. package/dist/index.js +14 -0
  84. package/dist/info.d.ts +17 -0
  85. package/dist/info.js +34 -0
  86. package/dist/init.d.ts +4 -0
  87. package/dist/init.js +31 -0
  88. package/dist/inspect.d.ts +13 -0
  89. package/dist/inspect.js +102 -0
  90. package/dist/load-env.d.ts +1 -0
  91. package/dist/load-env.js +31 -0
  92. package/dist/log.d.ts +13 -0
  93. package/dist/log.js +20 -0
  94. package/dist/mock-broker/bootstrap.d.ts +2 -0
  95. package/dist/mock-broker/bootstrap.js +8 -0
  96. package/dist/mock-broker/files.d.ts +60 -0
  97. package/dist/mock-broker/files.js +408 -0
  98. package/dist/mock-broker/oidc.d.ts +31 -0
  99. package/dist/mock-broker/oidc.js +161 -0
  100. package/dist/mock-broker/path.d.ts +9 -0
  101. package/dist/mock-broker/path.js +50 -0
  102. package/dist/mock-broker/server.d.ts +19 -0
  103. package/dist/mock-broker/server.js +350 -0
  104. package/dist/publish/build-ledger.d.ts +22 -0
  105. package/dist/publish/build-ledger.js +26 -0
  106. package/dist/publish/community-path.d.ts +6 -0
  107. package/dist/publish/community-path.js +41 -0
  108. package/dist/publish/curated-release.d.ts +78 -0
  109. package/dist/publish/curated-release.js +693 -0
  110. package/dist/publish/file-build-ledger.d.ts +18 -0
  111. package/dist/publish/file-build-ledger.js +268 -0
  112. package/dist/publish/github-pr.d.ts +25 -0
  113. package/dist/publish/github-pr.js +85 -0
  114. package/dist/publish/github-repo.d.ts +8 -0
  115. package/dist/publish/github-repo.js +21 -0
  116. package/dist/publish/publish.d.ts +23 -0
  117. package/dist/publish/publish.js +128 -0
  118. package/dist/publish/sync-definition.d.ts +4 -0
  119. package/dist/publish/sync-definition.js +48 -0
  120. package/dist/skill.d.ts +10 -0
  121. package/dist/skill.js +50 -0
  122. package/dist/templates/compose-import/compose.yaml +11 -0
  123. package/dist/templates/compose-import/synology-app.yaml +15 -0
  124. package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  125. package/dist/templates/hello-files/compose.yaml +8 -0
  126. package/dist/templates/hello-files/synology-app.yaml +13 -0
  127. package/dist/templates/hello-web/compose.yaml +7 -0
  128. package/dist/templates/hello-web/html/app.js +23 -0
  129. package/dist/templates/hello-web/html/index.html +15 -0
  130. package/dist/templates/hello-web/html/sdk.js +68 -0
  131. package/dist/templates/hello-web/synology-app.yaml +13 -0
  132. package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  133. package/dist/templates/hello-web-page/compose.yaml +7 -0
  134. package/dist/templates/hello-web-page/html/app.js +23 -0
  135. package/dist/templates/hello-web-page/html/index.html +15 -0
  136. package/dist/templates/hello-web-page/html/sdk.js +68 -0
  137. package/dist/templates/hello-web-page/synology-app.yaml +13 -0
  138. package/dist/test-command.d.ts +13 -0
  139. package/dist/test-command.js +26 -0
  140. package/dist/test-host.d.ts +13 -0
  141. package/dist/test-host.js +24 -0
  142. package/dist/types.d.ts +59 -0
  143. package/dist/types.js +1 -0
  144. package/dist/validate.d.ts +8 -0
  145. package/dist/validate.js +103 -0
  146. package/package.json +38 -0
  147. package/schema/synology-app.schema.json +137 -0
  148. package/skills/SKILL.md +51 -0
  149. package/skills/reference/create.md +34 -0
  150. package/skills/reference/deploy.md +41 -0
  151. package/skills/reference/diagnose.md +42 -0
  152. package/skills/reference/files.md +37 -0
  153. package/skills/reference/identity.md +31 -0
  154. package/skills/reference/wrap.md +35 -0
  155. package/templates/compose-import/compose.yaml +11 -0
  156. package/templates/compose-import/synology-app.yaml +15 -0
  157. package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  158. package/templates/hello-files/compose.yaml +8 -0
  159. package/templates/hello-files/synology-app.yaml +13 -0
  160. package/templates/hello-web/compose.yaml +7 -0
  161. package/templates/hello-web/html/app.js +23 -0
  162. package/templates/hello-web/html/index.html +15 -0
  163. package/templates/hello-web/html/sdk.js +68 -0
  164. package/templates/hello-web/synology-app.yaml +13 -0
  165. package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  166. package/templates/hello-web-page/compose.yaml +7 -0
  167. package/templates/hello-web-page/html/app.js +23 -0
  168. package/templates/hello-web-page/html/index.html +15 -0
  169. package/templates/hello-web-page/html/sdk.js +68 -0
  170. package/templates/hello-web-page/synology-app.yaml +13 -0
@@ -0,0 +1,34 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import * as tar from "tar";
4
+ /** Every archive this CLI writes uses the same timestamp so builds are byte-identical. */
5
+ export const FIXED_MTIME = new Date(0);
6
+ /** Relative paths of every file under `root`, depth-first and sorted for a stable archive order. */
7
+ export function collectArchiveEntries(root) {
8
+ const entries = [];
9
+ const walk = (relativeDir) => {
10
+ const absoluteDir = relativeDir ? join(root, relativeDir) : root;
11
+ for (const entry of readdirSync(absoluteDir).sort()) {
12
+ const relativePath = relativeDir ? `${relativeDir}/${entry}` : entry;
13
+ const absolutePath = join(absoluteDir, entry);
14
+ if (statSync(absolutePath).isDirectory()) {
15
+ walk(relativePath);
16
+ }
17
+ else {
18
+ entries.push(relativePath);
19
+ }
20
+ }
21
+ };
22
+ walk("");
23
+ return entries.sort((a, b) => a.localeCompare(b));
24
+ }
25
+ /** Writes `sourceDir` to `outputPath` as a tar whose bytes depend only on the file contents. */
26
+ export async function createReproducibleTar(sourceDir, outputPath, options = {}) {
27
+ await tar.c({
28
+ file: outputPath,
29
+ cwd: sourceDir,
30
+ gzip: options.gzip ?? false,
31
+ portable: true,
32
+ mtime: FIXED_MTIME,
33
+ }, collectArchiveEntries(sourceDir));
34
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runCli(argv: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,345 @@
1
+ import { Command } from "commander";
2
+ import { resolve, dirname } from "node:path";
3
+ import { findProjectRoot } from "./discovery.js";
4
+ import { formatErrors } from "./errors.js";
5
+ import { formatInspectReport, inspectProject } from "./inspect.js";
6
+ import { initFromTemplate, isTemplateName, listTemplates } from "./init.js";
7
+ import { validateProject } from "./validate.js";
8
+ import { runDev } from "./dev.js";
9
+ import { testProject } from "./test-command.js";
10
+ import { buildSpk } from "./build-spk/generator.js";
11
+ import { deployProject } from "./deploy/deploy.js";
12
+ import { createNasHost } from "./host/dsm-client.js";
13
+ import { projectInfo, formatProjectInfo } from "./info.js";
14
+ import { followProjectLogs } from "./log.js";
15
+ import { testProjectOnHost } from "./test-host.js";
16
+ import { formatSkillList, readSkill, resolveSkillsRoot } from "./skill.js";
17
+ import { publishProject, resolveCommunityAppsRoot } from "./publish/publish.js";
18
+ import { createExecGitHubCli, createExecGitOps } from "./publish/github-pr.js";
19
+ import { communityAppsGitHubRepo } from "./publish/github-repo.js";
20
+ function collectKvOption(value, previous, flag, example) {
21
+ const separator = value.indexOf("=");
22
+ if (separator <= 0) {
23
+ throw new Error(`${flag} 格式應為 SLOT=邏輯名,例如 ${example}`);
24
+ }
25
+ return {
26
+ ...(previous ?? {}),
27
+ [value.slice(0, separator)]: value.slice(separator + 1),
28
+ };
29
+ }
30
+ function collectShareOption(value, previous) {
31
+ return collectKvOption(value, previous, "--share", "--share MEDIA_SHARE=photos");
32
+ }
33
+ function collectRunAsOption(value, previous) {
34
+ return collectKvOption(value, previous, "--run-as", "--run-as RUN_AS=alice");
35
+ }
36
+ function resolveProject(cwd, project) {
37
+ const start = project ? resolve(cwd, project) : cwd;
38
+ const found = findProjectRoot(start);
39
+ if (!found) {
40
+ console.error("找不到 synology-app.yaml。請在專案根目錄執行,或使用 --project 指定。");
41
+ process.exit(1);
42
+ }
43
+ return found;
44
+ }
45
+ export async function runCli(argv) {
46
+ const program = new Command();
47
+ program
48
+ .name("synopkgland")
49
+ .description("Syno Package Land Developer Kit CLI")
50
+ .option("--project <dir>", "App 專案根目錄");
51
+ program
52
+ .command("validate")
53
+ .description("驗證 App Contract 與 Compose")
54
+ .option("--curated", "使用 Curated listing profile(未宣告 privilege 為紅燈)")
55
+ .action((options, command) => {
56
+ const global = command.parent?.opts() ?? {};
57
+ const project = resolveProject(process.cwd(), global.project);
58
+ const result = validateProject(project, {
59
+ profile: options.curated ? "listing" : "personal",
60
+ });
61
+ if (result.warnings && result.warnings.length > 0) {
62
+ console.error(formatErrors(result.warnings));
63
+ }
64
+ if (!result.ok) {
65
+ console.error(formatErrors(result.errors));
66
+ process.exit(1);
67
+ }
68
+ console.log("驗證通過。");
69
+ });
70
+ program
71
+ .command("inspect")
72
+ .description("檢視 Compose 意圖與 Web entry 提示")
73
+ .action((_options, command) => {
74
+ const global = command.parent?.opts() ?? {};
75
+ const project = resolveProject(process.cwd(), global.project);
76
+ const report = inspectProject(project);
77
+ console.log(formatInspectReport(report));
78
+ });
79
+ program
80
+ .command("init")
81
+ .description("從模板建立 App 專案")
82
+ .requiredOption("-t, --template <name>", `模板:${listTemplates().join(" | ")}`)
83
+ .argument("[dir]", "目標目錄", ".")
84
+ .action((dir, options, command) => {
85
+ const global = command.parent?.opts() ?? {};
86
+ if (global.project) {
87
+ console.error("--project 不適用於 init。");
88
+ process.exit(1);
89
+ }
90
+ if (!isTemplateName(options.template)) {
91
+ console.error(`未知模板:${options.template}`);
92
+ process.exit(1);
93
+ }
94
+ const targetDir = resolve(process.cwd(), dir);
95
+ initFromTemplate(options.template, targetDir);
96
+ console.log(`已建立 ${options.template} 專案於 ${targetDir}`);
97
+ });
98
+ program
99
+ .command("dev")
100
+ .description("啟動本機 mock Broker 與 Compose")
101
+ .option("--files-root <dir>", "將本機目錄對應為 NAS Logical Path")
102
+ .action(async (options, command) => {
103
+ const global = command.parent?.opts() ?? {};
104
+ const project = resolveProject(process.cwd(), global.project);
105
+ const validation = validateProject(project);
106
+ if (!validation.ok) {
107
+ console.error(formatErrors(validation.errors));
108
+ process.exit(1);
109
+ }
110
+ try {
111
+ const handle = await runDev({
112
+ project,
113
+ filesRoot: options.filesRoot,
114
+ });
115
+ console.log(`Mock Broker:${handle.brokerBaseUrl}`);
116
+ console.log(`Bootstrap:${handle.bootstrapPath}`);
117
+ console.log("Compose 已就緒。按 Ctrl+C 結束。");
118
+ const shutdown = async () => {
119
+ await handle.close();
120
+ process.exit(0);
121
+ };
122
+ process.on("SIGINT", shutdown);
123
+ process.on("SIGTERM", shutdown);
124
+ await new Promise(() => { });
125
+ }
126
+ catch (error) {
127
+ console.error(error instanceof Error ? error.message : String(error));
128
+ process.exit(1);
129
+ }
130
+ });
131
+ program
132
+ .command("test")
133
+ .description("驗證契約並等待 Compose 就緒")
134
+ .option("--host <name>", "在已部署的 NAS 上執行 Open smoke")
135
+ .option("--dev", "檢查 Development Package Identity({package}.dev)")
136
+ .action(async (options, command) => {
137
+ const global = command.parent?.opts() ?? {};
138
+ const project = resolveProject(process.cwd(), global.project);
139
+ if (options.host) {
140
+ const host = createNasHost({ host: options.host });
141
+ if (!host) {
142
+ console.error("找不到 NAS 憑證。請設定 synoagentcli profile 或 DSM_URL/DSM_PASSWORD。");
143
+ process.exit(1);
144
+ }
145
+ try {
146
+ const result = await testProjectOnHost({ project, host, dev: options.dev });
147
+ if (!result.ok) {
148
+ console.error(result.message ?? "測試失敗。");
149
+ process.exit(1);
150
+ }
151
+ console.log("NAS smoke 測試通過。");
152
+ }
153
+ catch (error) {
154
+ console.error(error instanceof Error ? error.message : String(error));
155
+ process.exit(1);
156
+ }
157
+ return;
158
+ }
159
+ try {
160
+ const result = await testProject({ project });
161
+ if (!result.ok) {
162
+ console.error(`測試失敗:${result.errors.join(", ")}`);
163
+ process.exit(1);
164
+ }
165
+ console.log("測試通過。");
166
+ }
167
+ catch (error) {
168
+ console.error(error instanceof Error ? error.message : String(error));
169
+ process.exit(1);
170
+ }
171
+ });
172
+ program
173
+ .command("deploy")
174
+ .description("Inner Loop Deploy 到 NAS(產生 SPK、簽發 Local Attestation、安裝)")
175
+ .requiredOption("--host <name>", "synoagentcli profile 或 NAS 名稱")
176
+ .option("--dev", "部署到 Development Package Identity({package}.dev)")
177
+ .option("--replace-curated", "危險:將既有 Curated App Package Attestation 降級為 Local Attestation")
178
+ .option("--share <slot=name>", "此 NAS 上的 Shared Folder 邏輯名(契約 shared_folder 槽,可重複)", collectShareOption)
179
+ .option("--run-as <slot=name>", "此 NAS 上的 DSM 使用者邏輯名(契約 dsm_user 槽)", collectRunAsOption)
180
+ .action(async (options, command) => {
181
+ const global = command.parent?.opts() ?? {};
182
+ const project = resolveProject(process.cwd(), global.project);
183
+ const host = createNasHost({ host: options.host });
184
+ if (!host) {
185
+ console.error("找不到 NAS 憑證。請設定 synoagentcli profile 或 DSM_URL/DSM_PASSWORD。");
186
+ process.exit(1);
187
+ }
188
+ try {
189
+ const result = await deployProject({
190
+ project,
191
+ host,
192
+ dev: options.dev,
193
+ replaceCurated: options.replaceCurated,
194
+ shareValues: options.share,
195
+ runAsValues: options.runAs,
196
+ });
197
+ if (!result.ok) {
198
+ console.error(`[${result.code ?? "DEPLOY_FAILED"}] ${result.message ?? "部署失敗。"}`);
199
+ process.exit(1);
200
+ }
201
+ console.log(`已部署 ${result.packageId}`);
202
+ if (result.openUrl) {
203
+ console.log(`Open URL:${result.openUrl}`);
204
+ }
205
+ }
206
+ catch (error) {
207
+ console.error(error instanceof Error ? error.message : String(error));
208
+ process.exit(1);
209
+ }
210
+ });
211
+ program
212
+ .command("info")
213
+ .description("顯示 NAS 上的 attestation、Open URL 與 readiness")
214
+ .requiredOption("--host <name>", "synoagentcli profile 或 NAS 名稱")
215
+ .option("--dev", "查詢 Development Package Identity")
216
+ .action(async (options, command) => {
217
+ const global = command.parent?.opts() ?? {};
218
+ const project = resolveProject(process.cwd(), global.project);
219
+ const host = createNasHost({ host: options.host });
220
+ if (!host) {
221
+ console.error("找不到 NAS 憑證。請設定 synoagentcli profile 或 DSM_URL/DSM_PASSWORD。");
222
+ process.exit(1);
223
+ }
224
+ try {
225
+ const info = await projectInfo({ project, host, dev: options.dev });
226
+ console.log(formatProjectInfo(info));
227
+ }
228
+ catch (error) {
229
+ console.error(error instanceof Error ? error.message : String(error));
230
+ process.exit(1);
231
+ }
232
+ });
233
+ program
234
+ .command("log")
235
+ .description("顯示 App 容器 log 與遮罩後的診斷資訊")
236
+ .requiredOption("--host <name>", "synoagentcli profile 或 NAS 名稱")
237
+ .option("--dev", "查詢 Development Package Identity")
238
+ .action(async (options, command) => {
239
+ const global = command.parent?.opts() ?? {};
240
+ const project = resolveProject(process.cwd(), global.project);
241
+ const host = createNasHost({ host: options.host });
242
+ if (!host) {
243
+ console.error("找不到 NAS 憑證。請設定 synoagentcli profile 或 DSM_URL/DSM_PASSWORD。");
244
+ process.exit(1);
245
+ }
246
+ try {
247
+ const result = await followProjectLogs({ project, host, dev: options.dev });
248
+ if (!result.ok) {
249
+ console.error("套件尚未安裝。先執行 synopkgland deploy --host。");
250
+ process.exit(1);
251
+ }
252
+ for (const line of result.lines) {
253
+ console.log(line);
254
+ }
255
+ if (result.diagnostics) {
256
+ console.log(JSON.stringify(result.diagnostics, null, 2));
257
+ }
258
+ }
259
+ catch (error) {
260
+ console.error(error instanceof Error ? error.message : String(error));
261
+ process.exit(1);
262
+ }
263
+ });
264
+ const skill = program
265
+ .command("skill")
266
+ .description("列出或讀取隨 CLI 內建的 Agent Skill Pack");
267
+ skill
268
+ .command("path")
269
+ .description("輸出 Skill Pack 根目錄(供 agent 載入 SKILL.md)")
270
+ .action(() => {
271
+ console.log(resolveSkillsRoot());
272
+ });
273
+ skill
274
+ .command("list")
275
+ .description("列出漸進式 skill 與檔案路徑")
276
+ .action(() => {
277
+ console.log(formatSkillList());
278
+ });
279
+ skill
280
+ .command("show <id>")
281
+ .description("輸出指定 skill 內容(orchestrator、create、wrap、identity、files、deploy、diagnose)")
282
+ .action((id) => {
283
+ try {
284
+ console.log(readSkill(id));
285
+ }
286
+ catch (error) {
287
+ console.error(error instanceof Error ? error.message : String(error));
288
+ process.exit(1);
289
+ }
290
+ });
291
+ program
292
+ .command("build-spk")
293
+ .description("產生本機 thin SPK(未簽署)")
294
+ .option("-o, --output <path>", "輸出 .spk 路徑")
295
+ .action(async (options, command) => {
296
+ const global = command.parent?.opts() ?? {};
297
+ const project = resolveProject(process.cwd(), global.project);
298
+ const result = await buildSpk({
299
+ project,
300
+ outputPath: options.output ? resolve(process.cwd(), options.output) : undefined,
301
+ });
302
+ if (!result.ok) {
303
+ console.error(`建置失敗:${result.errors.join(", ")}`);
304
+ process.exit(1);
305
+ }
306
+ console.log(`已產生 ${result.outputPath}`);
307
+ });
308
+ program
309
+ .command("publish")
310
+ .description("對 Community Apps 建立 GitHub Pull Request(不上傳 SPK、不簽署)")
311
+ .option("--base <branch>", "PR 目標分支", "master")
312
+ .action(async (options, command) => {
313
+ const global = command.parent?.opts() ?? {};
314
+ const project = resolveProject(process.cwd(), global.project);
315
+ const communityAppsRoot = resolveCommunityAppsRoot(project.root);
316
+ if (!communityAppsRoot) {
317
+ console.error("找不到 synopkgland-community-apps/apps。請在 monorepo 根目錄執行,或設定 SYNOPKGLAND_COMMUNITY_APPS_ROOT。");
318
+ process.exit(1);
319
+ }
320
+ const repoRoot = dirname(communityAppsRoot);
321
+ try {
322
+ const result = await publishProject({
323
+ project,
324
+ communityAppsRoot,
325
+ baseBranch: options.base,
326
+ github: createExecGitHubCli(repoRoot, communityAppsGitHubRepo()),
327
+ git: createExecGitOps(repoRoot),
328
+ });
329
+ if (!result.ok) {
330
+ console.error(result.message ?? result.code ?? "publish 失敗。");
331
+ process.exit(1);
332
+ }
333
+ if (result.noChanges) {
334
+ console.log(result.message ?? "Community Apps 定義已是最新。");
335
+ return;
336
+ }
337
+ console.log(`已建立 PR:${result.prUrl}`);
338
+ }
339
+ catch (error) {
340
+ console.error(error instanceof Error ? error.message : String(error));
341
+ process.exit(1);
342
+ }
343
+ });
344
+ await program.parseAsync(argv);
345
+ }
@@ -0,0 +1,25 @@
1
+ import type { ComposeDocument, ComposeService, ValidationError } from "../types.js";
2
+ import { type PrivilegeFlag } from "./privilege.js";
3
+ export interface ParsedCompose {
4
+ compose: ComposeDocument;
5
+ source: string;
6
+ }
7
+ export declare function loadCompose(composePath: string): ParsedCompose | null;
8
+ export declare function listServiceNames(compose: ComposeDocument): string[];
9
+ export declare function extractPublishedTcpPorts(service: ComposeService): number[];
10
+ export declare function collectPublishedTcpPorts(compose: ComposeDocument): Map<number, string[]>;
11
+ export declare function collectComposeInterpolations(compose: ComposeDocument): Set<string>;
12
+ export declare function validateComposeSecurity(compose: ComposeDocument, composeFile: string, options: {
13
+ profile: "personal" | "listing";
14
+ declaredPrivilege: ReadonlySet<PrivilegeFlag>;
15
+ }): ValidationError[];
16
+ export interface HostBindContext {
17
+ sharedFolderParams: ReadonlySet<string>;
18
+ installParamNames: ReadonlySet<string>;
19
+ }
20
+ export declare function collectVolumeSourceInterpolations(compose: ComposeDocument): Set<string>;
21
+ export declare function validateComposeHostBinds(compose: ComposeDocument, composeFile: string, context: HostBindContext): ValidationError[];
22
+ export declare function validateComposeRunAs(compose: ComposeDocument, composeFile: string, context: {
23
+ hasSharedFolder: boolean;
24
+ }): ValidationError[];
25
+ export declare function validateAdminPortMapping(adminport: number, compose: ComposeDocument, composeFile: string): ValidationError[];