@synopackageland/cli 0.7.0 → 0.7.2
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.
- package/CHANGELOG.md +301 -1
- package/assets/default-icon/README.md +19 -0
- package/assets/default-icon/default-icon-small.svg +9 -0
- package/assets/default-icon/default-icon.svg +10 -0
- package/assets/default-icon/icon_16.png +0 -0
- package/assets/default-icon/icon_24.png +0 -0
- package/assets/default-icon/icon_256.png +0 -0
- package/assets/default-icon/icon_32.png +0 -0
- package/assets/default-icon/icon_48.png +0 -0
- package/assets/default-icon/icon_64.png +0 -0
- package/assets/default-icon/icon_72.png +0 -0
- package/dist/add.d.ts +4 -1
- package/dist/add.js +14 -9
- package/dist/build-spk/dsm-icons.d.ts +31 -2
- package/dist/build-spk/dsm-icons.js +135 -56
- package/dist/build-spk/dsm-platforms.json +4 -1
- package/dist/build-spk/gateway.d.ts +1 -1
- package/dist/build-spk/gateway.js +8 -4
- package/dist/build-spk/generator.js +17 -3
- package/dist/build-spk/go-toolchain.d.ts +20 -0
- package/dist/build-spk/go-toolchain.js +44 -0
- package/dist/build-spk/png.d.ts +14 -0
- package/dist/build-spk/png.js +161 -0
- package/dist/build-spk/ui-texts.d.ts +20 -0
- package/dist/build-spk/ui-texts.js +42 -0
- package/dist/cli.js +39 -9
- package/dist/contract/i18n.d.ts +5 -0
- package/dist/contract/i18n.js +45 -0
- package/dist/contract/icon.d.ts +5 -0
- package/dist/contract/icon.js +27 -0
- package/dist/contract/validate.js +4 -0
- package/dist/contract/web-launch.js +6 -2
- package/dist/deploy/native-install-values.d.ts +17 -0
- package/dist/deploy/native-install-values.js +32 -0
- package/dist/deploy/plan.d.ts +6 -2
- package/dist/deploy/plan.js +57 -6
- package/dist/error-registry.js +15 -6
- package/dist/host/dsm-client.js +53 -17
- package/dist/host/dsm-http.js +24 -2
- package/dist/host/types.d.ts +1 -0
- package/dist/info.js +28 -9
- package/dist/init.js +1 -1
- package/dist/runtime/dsm-version.d.ts +6 -0
- package/dist/runtime/dsm-version.js +8 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +8 -5
- package/dist/standalone/daemon.d.ts +2 -1
- package/dist/standalone/daemon.js +10 -6
- package/dist/standalone/dsm-window.d.ts +2 -0
- package/dist/standalone/dsm-window.js +26 -40
- package/dist/standalone/i18n.d.ts +12 -0
- package/dist/standalone/i18n.js +30 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +41 -2
- package/dist/standalone/manifest.d.ts +5 -6
- package/dist/standalone/manifest.js +21 -34
- package/dist/standalone/native-authorization-window.d.ts +1 -1
- package/dist/standalone/native-authorization-window.js +7 -1
- package/dist/standalone/native-layout.d.ts +2 -0
- package/dist/standalone/native-layout.js +22 -1
- package/dist/standalone/native-root-window.d.ts +4 -2
- package/dist/standalone/native-root-window.js +33 -18
- package/dist/standalone/root-window.d.ts +4 -1
- package/dist/standalone/root-window.js +143 -33
- package/dist/standalone/spk.js +7 -4
- package/dist/templates/_daemon/go.mod +1 -1
- package/dist/templates/_daemon/main.go +6 -3
- package/dist/templates/_daemon/main_test.go +21 -1
- package/dist/templates/_daemon/testdata/child.go +3 -0
- package/dist/templates/_executor/README.md +4 -1
- package/dist/templates/_executor/anchor.go +10 -0
- package/dist/templates/_executor/anchor_test.go +40 -5
- package/dist/templates/_executor/digest_review_test.go +2 -2
- package/dist/templates/_executor/dsm.go +40 -3
- package/dist/templates/_executor/go.mod +2 -0
- package/dist/templates/_executor/main.go +11 -5
- package/dist/templates/_executor/managed.go +3 -3
- package/dist/templates/_executor/managed_test.go +3 -3
- package/dist/templates/_executor/path_test.go +18 -0
- package/dist/templates/_executor/pid.go +37 -0
- package/dist/templates/_executor/pid_test.go +113 -0
- package/dist/templates/_executor/platform_other.go +8 -0
- package/dist/templates/_executor/purge_log.go +18 -0
- package/dist/templates/_executor/tasks.go +45 -7
- package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
- package/dist/templates/_executor/tasks_test.go +58 -1
- package/dist/templates/_executor/upgrade_watch_test.go +13 -5
- package/dist/templates/_gateway/go.mod +2 -0
- package/dist/templates/binary/dsm-package.yaml +3 -0
- package/dist/templates/compose/synology-app.yaml +3 -0
- package/dist/templates/native-go/backend/go.mod +1 -1
- package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
- package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
- package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
- package/dist/templates/native-go/dsm-package.yaml +3 -0
- package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
- package/package.json +2 -1
- package/schema/synology-app.schema.json +22 -0
- package/templates/_daemon/go.mod +1 -1
- package/templates/_daemon/main.go +6 -3
- package/templates/_daemon/main_test.go +21 -1
- package/templates/_daemon/testdata/child.go +3 -0
- package/templates/_executor/README.md +4 -1
- package/templates/_executor/anchor.go +10 -0
- package/templates/_executor/anchor_test.go +40 -5
- package/templates/_executor/digest_review_test.go +2 -2
- package/templates/_executor/dsm.go +40 -3
- package/templates/_executor/go.mod +2 -0
- package/templates/_executor/main.go +11 -5
- package/templates/_executor/managed.go +3 -3
- package/templates/_executor/managed_test.go +3 -3
- package/templates/_executor/path_test.go +18 -0
- package/templates/_executor/pid.go +37 -0
- package/templates/_executor/pid_test.go +113 -0
- package/templates/_executor/platform_other.go +8 -0
- package/templates/_executor/purge_log.go +18 -0
- package/templates/_executor/tasks.go +45 -7
- package/templates/_executor/tasks_dsm71_test.go +114 -0
- package/templates/_executor/tasks_test.go +58 -1
- package/templates/_executor/upgrade_watch_test.go +13 -5
- package/templates/_gateway/go.mod +2 -0
- package/templates/binary/dsm-package.yaml +3 -0
- package/templates/compose/synology-app.yaml +3 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/templates/native-go/backend/internal/identity/README.md +5 -1
- package/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/templates/native-go/backend/internal/identity/http.go +1 -1
- package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/templates/native-go/backend/internal/identity/service.go +1 -1
- package/templates/native-go/dsm-package.yaml +3 -0
- package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Command, CommanderError } from "commander";
|
|
2
|
-
import { readFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
3
3
|
import { resolve, dirname, join } from "node:path";
|
|
4
4
|
import { findProjectRoot } from "./discovery.js";
|
|
5
5
|
import { loadContract } from "./contract/load.js";
|
|
6
|
+
import { contractIconPath } from "./contract/icon.js";
|
|
6
7
|
import { formatErrors } from "./errors.js";
|
|
7
8
|
import { formatInspectReport, inspectProject } from "./inspect.js";
|
|
8
9
|
import { formatPinReport, pinProjectImages } from "./images/pin.js";
|
|
@@ -16,6 +17,7 @@ import { testProject } from "./test-command.js";
|
|
|
16
17
|
import { buildSpk } from "./build-spk/generator.js";
|
|
17
18
|
import { CONTAINER_ARCH_FAMILIES, isContainerArchFamily, isNativeArchFamily, NATIVE_ARCH_FAMILIES, parseArchList, } from "./build-spk/dsm-arch.js";
|
|
18
19
|
import { DEFAULT_MIN_DSM, CONTAINER_MIN_DSM } from "./build-spk/dsm-version.js";
|
|
20
|
+
import { iconReport } from "./build-spk/dsm-icons.js";
|
|
19
21
|
import { deployProject } from "./deploy/deploy.js";
|
|
20
22
|
import { planDeployment, deployStandalone } from "./deploy/plan.js";
|
|
21
23
|
import { createNasHost } from "./host/dsm-client.js";
|
|
@@ -48,6 +50,17 @@ function collectKv(value, previous, flag) {
|
|
|
48
50
|
[value.slice(0, separator)]: value.slice(separator + 1),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
53
|
+
/** check's icon lines (stdout) and its single-colour warning (stderr). */
|
|
54
|
+
function formatIconReport(report, manifestFile) {
|
|
55
|
+
return {
|
|
56
|
+
lines: report.source === "custom"
|
|
57
|
+
? [`圖示:自訂 ${report.path}`]
|
|
58
|
+
: report.source === "layout"
|
|
59
|
+
? ["圖示:由 layout metadata 的 PACKAGE_ICON*.PNG 提供(若有)", `info:在 ${manifestFile} 設定 icon: 可改由 CLI 產生 Package Center 圖示。`]
|
|
60
|
+
: ["圖示:SDK 預設圖示", `info:在 ${manifestFile} 設定 icon: <正方形 PNG(至少 256×256)或 icon_<size>.png 目錄> 換成自己的圖示。`],
|
|
61
|
+
...(report.solid ? { warning: "warning:圖示只有單一顏色,Package Center 與 DSM 主選單會像一個色塊;建議換成有圖形的圖示。" } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
51
64
|
function formatSpkInspection(spk, report) {
|
|
52
65
|
return [
|
|
53
66
|
`SPK:${spk}`,
|
|
@@ -55,6 +68,7 @@ function formatSpkInspection(spk, report) {
|
|
|
55
68
|
`arch:${report.arch ?? "?"} runtime:${report.kind ?? "?"}`,
|
|
56
69
|
`簽署:${report.signed ? "有 Synology 簽章" : "未簽署(Package Center 需允許任何發行者)"}`,
|
|
57
70
|
`CGI:${report.cgi ?? "無"}`,
|
|
71
|
+
...(report.icon ? [`圖示:${report.icon.source === "default" ? "SDK 預設圖示" : report.icon.source === "custom" ? `自訂(PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "無 PACKAGE_ICON"}`] : []),
|
|
58
72
|
...(report.entry === "admin-only" ? ["入口:entry: admin-only,只有 DSM 管理員能開啟與呼叫任何 route(含 SDK routes)"] : []),
|
|
59
73
|
...(report.entry === "app-privilege" ? ["入口:entry: app-privilege,由 DSM 控制台「應用程式權限」決定誰能使用"] : []),
|
|
60
74
|
...(report.portal ? [`Application Portal:預設別名 /${report.portal.alias}/(${report.portal.appId}),由管理員套用後啟用`] : []),
|
|
@@ -62,7 +76,11 @@ function formatSpkInspection(spk, report) {
|
|
|
62
76
|
"payload:",
|
|
63
77
|
...(report.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed):${item.target} — ${item.condition} ${item.action}`),
|
|
64
78
|
...report.payload.map((e) => ` ${e.mode} ${e.path}`),
|
|
65
|
-
report.findings.length
|
|
79
|
+
report.findings.length
|
|
80
|
+
? "問題:"
|
|
81
|
+
: report.executor
|
|
82
|
+
? "檢查:沒有異常。管理員核准後會建立 root executor 排程(boot 與 control task),以 root 執行此 SPK 固定的 executor 與 policy。"
|
|
83
|
+
: "檢查:沒有 root 權限需求或異常。",
|
|
66
84
|
...report.findings.map((f) => ` - ${f}`),
|
|
67
85
|
].join("\n");
|
|
68
86
|
}
|
|
@@ -149,10 +167,16 @@ export async function runCli(argv) {
|
|
|
149
167
|
const loaded = isNativeLayout(standalone) ? loadNativeLayout(standalone) : loadStandaloneManifest(standalone);
|
|
150
168
|
if (!loaded.ok)
|
|
151
169
|
throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "將 dsm-package.yaml 更新為 schema: 2,修正宣告後重試。");
|
|
170
|
+
const declared = "icon" in loaded.manifest && loaded.manifest.icon ? resolve(standalone, loaded.manifest.icon) : undefined;
|
|
171
|
+
const icon = isNativeLayout(standalone) && !declared ? { source: "layout", solid: false } : iconReport(standalone, declared);
|
|
172
|
+
const iconText = formatIconReport(icon, "dsm-package.yaml");
|
|
173
|
+
if (iconText.warning)
|
|
174
|
+
console.error(iconText.warning);
|
|
152
175
|
emit("check", {
|
|
153
176
|
package: loaded.manifest.package,
|
|
154
177
|
capabilities: "capabilities" in loaded.manifest ? loaded.manifest.capabilities : {},
|
|
155
|
-
|
|
178
|
+
icon,
|
|
179
|
+
}, ["驗證通過。", ...iconText.lines].join("\n"));
|
|
156
180
|
return;
|
|
157
181
|
}
|
|
158
182
|
const project = requireProject(rootOf(command));
|
|
@@ -162,7 +186,11 @@ export async function runCli(argv) {
|
|
|
162
186
|
if (!result.ok)
|
|
163
187
|
throw failure(result.errors[0].code, formatErrors(result.errors), result.errors[0].suggestion);
|
|
164
188
|
const report = inspectProject(project);
|
|
165
|
-
|
|
189
|
+
const icon = iconReport(project.root, contractIconPath(loadContract(project.contractPath).contract, project.root));
|
|
190
|
+
const iconText = formatIconReport(icon, "synology-app.yaml");
|
|
191
|
+
if (iconText.warning)
|
|
192
|
+
console.error(iconText.warning);
|
|
193
|
+
emit("check", { report, warnings: result.warnings ?? [], icon }, [`驗證通過。`, formatInspectReport(report), ...iconText.lines].join("\n"));
|
|
166
194
|
});
|
|
167
195
|
program
|
|
168
196
|
.command("inspect <spk>")
|
|
@@ -234,13 +262,15 @@ export async function runCli(argv) {
|
|
|
234
262
|
});
|
|
235
263
|
stdout(`已建立 ${options.template} 專案於 ${targetDir}`);
|
|
236
264
|
if (options.template === "native-go")
|
|
237
|
-
stdout(`下一步:\n cd ${dir}\n (cd backend && go test ./...)\n synopkgland build\n說明見 README.md。`);
|
|
265
|
+
stdout(`下一步:\n cd ${dir}\n (cd backend && go test ./...)\n${existsSync(join(targetDir, "frontend", "package.json")) ? " (cd frontend && npm ci)\n" : ""} synopkgland build\n說明見 README.md。`);
|
|
238
266
|
});
|
|
239
267
|
program
|
|
240
268
|
.command("add <caps...>")
|
|
241
269
|
.description(`加入能力(${CAPABILITY_NAMES.join("、")})或 portal 與 SDK-owned overlay;不覆寫既有應用檔案`)
|
|
242
270
|
.action((caps, _options, command) => {
|
|
243
271
|
const result = addCapabilities(rootOf(command), caps);
|
|
272
|
+
for (const note of result.notes ?? [])
|
|
273
|
+
console.error(`[注意] ${note}`);
|
|
244
274
|
stdout(`已新增:${result.added.join(", ") || "無"};已存在:${result.unchanged.join(", ") || "無"}`);
|
|
245
275
|
});
|
|
246
276
|
program
|
|
@@ -281,7 +311,7 @@ export async function runCli(argv) {
|
|
|
281
311
|
.option("--dev", "Development Package Identity")
|
|
282
312
|
.option("--share <slot=name>", "Compose 共用資料夾", (value, previous) => collectKv(value, previous, "--share"))
|
|
283
313
|
.option("--run-as <slot=name>", "Compose DSM 使用者", (value, previous) => collectKv(value, previous, "--run-as"))
|
|
284
|
-
.option("--param <NAME=value>", "Compose
|
|
314
|
+
.option("--param <NAME=value>", "安裝參數(Compose)或安裝精靈值(native-go/binary)", (value, previous) => collectKv(value, previous, "--param"))
|
|
285
315
|
.option("--dry-run", "只輸出本機部署計畫,不連接 NAS")
|
|
286
316
|
.action(async (options, command) => {
|
|
287
317
|
if (options.arch !== undefined &&
|
|
@@ -292,15 +322,15 @@ export async function runCli(argv) {
|
|
|
292
322
|
dev: options.dev,
|
|
293
323
|
});
|
|
294
324
|
if (plan.kind !== "container" &&
|
|
295
|
-
(options.share || options.runAs
|
|
296
|
-
throw failure("CLI_OPTION_INVALID", "native-go/binary 不接受
|
|
325
|
+
(options.share || options.runAs))
|
|
326
|
+
throw failure("CLI_OPTION_INVALID", "native-go/binary 不接受 --share/--run-as;安裝精靈值請用 --param。");
|
|
297
327
|
if (options.dryRun) {
|
|
298
328
|
emit("deploy", plan, JSON.stringify(plan, null, 2));
|
|
299
329
|
return;
|
|
300
330
|
}
|
|
301
331
|
if (plan.kind !== "container") {
|
|
302
332
|
const host = hostOf(options.host);
|
|
303
|
-
const output = await deployStandalone(plan, host);
|
|
333
|
+
const output = await deployStandalone(plan, host, { params: options.param });
|
|
304
334
|
const status = await standaloneInfo({ root: plan.root, host });
|
|
305
335
|
emit("deploy", { ...plan, output, status }, `已部署 ${plan.packageId}: ${output}\n${formatProjectInfo(status)}${plan.notes.length ? "\n" + plan.notes.join("\n") : ""}`);
|
|
306
336
|
return;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AppContract, ValidationError } from "../types.js";
|
|
2
|
+
import { type ParsedContract } from "./load.js";
|
|
3
|
+
import type { AppTitle } from "../build-spk/ui-texts.js";
|
|
4
|
+
export declare function contractAppTitle(contract: AppContract): AppTitle;
|
|
5
|
+
export declare function validateContractI18n(parsed: ParsedContract, contractFile: string): ValidationError[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { contractSpan, makeError } from "../errors.js";
|
|
2
|
+
import { getValuePosition } from "./load.js";
|
|
3
|
+
import { readInfoTranslations } from "../standalone/i18n.js";
|
|
4
|
+
/** Same rule as dsm-package.yaml text: INFO and DSM strings files quote with " and cannot escape. */
|
|
5
|
+
function plain(value) {
|
|
6
|
+
return typeof value === "string" && value.trim() !== "" && !/[\r\n"\\]/.test(value);
|
|
7
|
+
}
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
/** `i18n.info` as validation accepted it; undefined when absent or invalid. */
|
|
12
|
+
function contractInfo(contract) {
|
|
13
|
+
if (!isRecord(contract.i18n))
|
|
14
|
+
return undefined;
|
|
15
|
+
return readInfoTranslations(contract.i18n.info, { invalid: () => undefined, text: (value) => (plain(value) ? value : undefined) });
|
|
16
|
+
}
|
|
17
|
+
export function contractAppTitle(contract) {
|
|
18
|
+
return {
|
|
19
|
+
displayname: String(contract.displayname ?? contract.package),
|
|
20
|
+
description: String(contract.description ?? ""),
|
|
21
|
+
info: contractInfo(contract),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function validateContractI18n(parsed, contractFile) {
|
|
25
|
+
const { contract, doc } = parsed;
|
|
26
|
+
if (contract.i18n === undefined)
|
|
27
|
+
return [];
|
|
28
|
+
const errors = [];
|
|
29
|
+
const invalid = (why) => {
|
|
30
|
+
const pos = getValuePosition(doc, ["i18n"]) ?? { line: 1, column: 1 };
|
|
31
|
+
errors.push(makeError("CONTRACT_I18N_INVALID", contractSpan(contractFile, pos.line, pos.column, "i18n"), `i18n ${why}`, "i18n 只接受 info;info 以 zh-TW、ja 等支援語言為 key,值為單行、不含 \" 或 \\ 的 displayname / description(英文寫在頂層)。"));
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
const text = (value, field) => plain(value) ? value : invalid(`${field} 必須是單行、非空且不含 " 或 \\ 的文字。`);
|
|
35
|
+
if (!isRecord(contract.i18n) || Object.keys(contract.i18n).some((key) => key !== "info")) {
|
|
36
|
+
return invalid("只接受 info(各語言的 displayname 與 description)。") ?? errors;
|
|
37
|
+
}
|
|
38
|
+
const info = readInfoTranslations(contract.i18n.info, { invalid: (why) => invalid(`.info ${why}`), text });
|
|
39
|
+
if (info && Object.keys(info).length > 0) {
|
|
40
|
+
// DSM resolves the window title from ui/texts strings, which carry the English text too.
|
|
41
|
+
text(contract.displayname, "displayname");
|
|
42
|
+
text(contract.description, "description");
|
|
43
|
+
}
|
|
44
|
+
return errors;
|
|
45
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AppContract, ValidationError } from "../types.js";
|
|
2
|
+
import { type ParsedContract } from "./load.js";
|
|
3
|
+
/** `icon` resolved against the project root; validation keeps it inside the project. */
|
|
4
|
+
export declare function contractIconPath(contract: AppContract, root: string): string | undefined;
|
|
5
|
+
export declare function validateContractIcon(parsed: ParsedContract, contractFile: string): ValidationError[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { contractSpan, makeError } from "../errors.js";
|
|
3
|
+
import { getValuePosition } from "./load.js";
|
|
4
|
+
import { renderPackageIcons } from "../build-spk/dsm-icons.js";
|
|
5
|
+
/** `icon` resolved against the project root; validation keeps it inside the project. */
|
|
6
|
+
export function contractIconPath(contract, root) {
|
|
7
|
+
return typeof contract.icon === "string" ? resolve(root, contract.icon) : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function validateContractIcon(parsed, contractFile) {
|
|
10
|
+
const { contract, doc, contractPath } = parsed;
|
|
11
|
+
if (contract.icon === undefined || !contractPath)
|
|
12
|
+
return [];
|
|
13
|
+
const pos = getValuePosition(doc, ["icon"]) ?? { line: 1, column: 1 };
|
|
14
|
+
const invalid = (message) => [makeError("CONTRACT_ICON_INVALID", contractSpan(contractFile, pos.line, pos.column, "icon"), `icon:${message}`, "icon 填專案內的正方形 PNG(至少 256×256),或含 icon_256.png 的 icon_<size>.png 目錄;省略則使用 SDK 預設圖示。")];
|
|
15
|
+
const root = dirname(contractPath);
|
|
16
|
+
const rel = typeof contract.icon === "string" && contract.icon.trim() !== "" ? relative(root, resolve(root, contract.icon)) : undefined;
|
|
17
|
+
if (rel === undefined || isAbsolute(String(contract.icon)) || rel.startsWith("..") || isAbsolute(rel)) {
|
|
18
|
+
return invalid(`「${String(contract.icon)}」必須是位於專案目錄內的相對路徑。`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
renderPackageIcons(resolve(root, rel));
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return invalid(error.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -5,6 +5,8 @@ import { collectInstallParameters, getValuePosition, stringViolations, } from ".
|
|
|
5
5
|
import { validateContractSchema } from "./schema-validate.js";
|
|
6
6
|
import { DSM_GROUP_NAME, isWebAccessMode, isWebLaunchMode, readWebIntegration } from "./web-launch.js";
|
|
7
7
|
import { contractPortalSupported, readPortalDeclaration } from "../portal/alias.js";
|
|
8
|
+
import { validateContractIcon } from "./icon.js";
|
|
9
|
+
import { validateContractI18n } from "./i18n.js";
|
|
8
10
|
import { loadCompose } from "../compose/analyze.js";
|
|
9
11
|
import { collectPosixAllSharesParameters, isPosixAllSharesType, validatePosixAllSharesSources, } from "./posix-all-shares.js";
|
|
10
12
|
import { getErrorCode } from "../error-registry.js";
|
|
@@ -56,6 +58,8 @@ export function validateContract(parsed, contractFile) {
|
|
|
56
58
|
errors.push(...validateWebLaunch(parsed, contractFile));
|
|
57
59
|
errors.push(...validateWebAccess(parsed, contractFile));
|
|
58
60
|
errors.push(...validateWebPortal(parsed, contractFile));
|
|
61
|
+
errors.push(...validateContractIcon(parsed, contractFile));
|
|
62
|
+
errors.push(...validateContractI18n(parsed, contractFile));
|
|
59
63
|
return errors;
|
|
60
64
|
}
|
|
61
65
|
function validatePosixAllShares(parsed, contractFile) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
3
|
+
import { contractAppTitle } from "./i18n.js";
|
|
2
4
|
/** DSM Web Station portal alias max length (error 1311 above this). */
|
|
3
5
|
export const WEB_STATION_ALIAS_MAX = 20;
|
|
4
6
|
export const WEB_LAUNCH_MODES = ["dsm-window", "new-page"];
|
|
@@ -139,13 +141,15 @@ export function renderWebStationResource(contract) {
|
|
|
139
141
|
}
|
|
140
142
|
export function renderDsmWindowConfig(contract) {
|
|
141
143
|
const packageId = String(contract.package);
|
|
144
|
+
const { title, desc, preloadTexts } = uiTitleFields(contractAppTitle(contract));
|
|
142
145
|
return `${JSON.stringify({
|
|
143
146
|
".url": {
|
|
144
147
|
[packageId]: {
|
|
145
148
|
type: "url",
|
|
146
149
|
allUsers: true,
|
|
147
|
-
title
|
|
148
|
-
desc
|
|
150
|
+
title,
|
|
151
|
+
desc,
|
|
152
|
+
...(preloadTexts ? { preloadTexts } : {}),
|
|
149
153
|
icon: "images/app_{0}.png",
|
|
150
154
|
url: trustedAppEntryPath(contract),
|
|
151
155
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StandaloneManifest } from "../standalone/manifest.js";
|
|
2
|
+
export type NativeInstallValuesResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
extraValues: Record<string, string>;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Install wizard values for a native-go/binary deploy. Only an install-selected
|
|
12
|
+
* delegate asks the wizard for anything; preinst refuses an empty selection on
|
|
13
|
+
* the first install, so refuse it here before building or uploading.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveNativeInstallValues(manifest: StandaloneManifest, provided: Record<string, string>, state: {
|
|
16
|
+
installed: boolean;
|
|
17
|
+
}): NativeInstallValuesResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const USAGE = "synopkgland deploy --param delegate_share=<shared folder>,或 --param delegate_home=true";
|
|
2
|
+
/** Mirrors the preinst draft check in build-spk/delegate-scope.ts. */
|
|
3
|
+
function validShareName(name) {
|
|
4
|
+
return (name !== "" && name !== "." && !name.includes("..") &&
|
|
5
|
+
!/[/\\:"%\x00-\x1f\x7f]/.test(name) &&
|
|
6
|
+
!/^homes?$/i.test(name) && name.trim() === name && name.length <= 256);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Install wizard values for a native-go/binary deploy. Only an install-selected
|
|
10
|
+
* delegate asks the wizard for anything; preinst refuses an empty selection on
|
|
11
|
+
* the first install, so refuse it here before building or uploading.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveNativeInstallValues(manifest, provided, state) {
|
|
14
|
+
const fail = (code, message) => ({ ok: false, code, message });
|
|
15
|
+
const selected = manifest.capabilities.delegate?.scope === "install-selected";
|
|
16
|
+
const names = Object.keys(provided);
|
|
17
|
+
if (!selected && names.length)
|
|
18
|
+
return fail("DEPLOY_PARAM_UNDECLARED", `${manifest.package} has no install wizard values; remove --param ${names[0]}.`);
|
|
19
|
+
const unknown = names.find((name) => name !== "delegate_share" && name !== "delegate_home");
|
|
20
|
+
if (unknown)
|
|
21
|
+
return fail("DEPLOY_PARAM_UNDECLARED", `--param ${unknown} is not an install wizard value. Use: ${USAGE}.`);
|
|
22
|
+
const { delegate_share: share, delegate_home: home } = provided;
|
|
23
|
+
if (home !== undefined && home !== "true" && home !== "false")
|
|
24
|
+
return fail("DEPLOY_PARAM_TYPE_INVALID", "--param delegate_home must be true or false.");
|
|
25
|
+
if (share !== undefined && !validShareName(share))
|
|
26
|
+
return fail("DEPLOY_PARAM_TYPE_INVALID", `--param delegate_share must be one Shared Folder name, not a path, home or homes.`);
|
|
27
|
+
if (share !== undefined && home === "true")
|
|
28
|
+
return fail("DEPLOY_PARAM_TYPE_INVALID", "Choose either --param delegate_share or --param delegate_home=true, not both.");
|
|
29
|
+
if (selected && !state.installed && share === undefined && home !== "true")
|
|
30
|
+
return fail("DEPLOY_PARAM_REQUIRED", `The first install of ${manifest.package} must select the delegate scope: ${USAGE}.`);
|
|
31
|
+
return { ok: true, extraValues: { ...provided } };
|
|
32
|
+
}
|
package/dist/deploy/plan.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { type CommandRunner } from "../standalone/build.js";
|
|
1
2
|
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
3
|
import type { NasHost } from "../host/types.js";
|
|
3
4
|
export interface DeploymentPlan {
|
|
4
5
|
root: string;
|
|
5
|
-
kind: "native-go" | "binary" | "container";
|
|
6
|
+
kind: "native-go" | "binary" | "native-layout" | "container";
|
|
6
7
|
packageId: string;
|
|
7
8
|
version: string;
|
|
8
9
|
arch?: NativeArchFamily;
|
|
@@ -15,4 +16,7 @@ export declare function planDeployment(root: string, options?: {
|
|
|
15
16
|
arch?: NativeArchFamily;
|
|
16
17
|
dev?: boolean;
|
|
17
18
|
}): DeploymentPlan;
|
|
18
|
-
export declare function deployStandalone(plan: DeploymentPlan, host: NasHost
|
|
19
|
+
export declare function deployStandalone(plan: DeploymentPlan, host: NasHost, options?: {
|
|
20
|
+
params?: Record<string, string>;
|
|
21
|
+
run?: CommandRunner;
|
|
22
|
+
}): Promise<string>;
|
package/dist/deploy/plan.js
CHANGED
|
@@ -3,14 +3,18 @@ import { loadContract } from "../contract/load.js";
|
|
|
3
3
|
import { validateProject } from "../validate.js";
|
|
4
4
|
import { findStandaloneRoot, loadStandaloneManifest, } from "../standalone/manifest.js";
|
|
5
5
|
import { buildStandalone } from "../standalone/build.js";
|
|
6
|
+
import { buildNativeLayout, isNativeLayout, loadNativeLayout } from "../standalone/native-layout.js";
|
|
6
7
|
import { resolveDeployIdentity } from "./identity.js";
|
|
7
8
|
import { nextDeployVersion } from "./spk-version.js";
|
|
9
|
+
import { resolveNativeInstallValues } from "./native-install-values.js";
|
|
8
10
|
function fail(code, message) {
|
|
9
11
|
return Object.assign(new Error(`[${code}] ${message}`), { code });
|
|
10
12
|
}
|
|
11
13
|
/** Purely local planning: no credentials, network, build or installation. */
|
|
12
14
|
export function planDeployment(root, options = {}) {
|
|
13
15
|
const standalone = findStandaloneRoot(root);
|
|
16
|
+
if (standalone && isNativeLayout(standalone))
|
|
17
|
+
return planNativeLayout(standalone, options);
|
|
14
18
|
if (standalone) {
|
|
15
19
|
const validation = validateProject(findProjectRoot(standalone));
|
|
16
20
|
if (!validation.ok)
|
|
@@ -33,7 +37,7 @@ export function planDeployment(root, options = {}) {
|
|
|
33
37
|
transport: "dsm-local-spk",
|
|
34
38
|
notes: m.capabilities.rootOps || m.capabilities.delegate
|
|
35
39
|
? [
|
|
36
|
-
"Installation does not mean ready. Approve boot and control tasks once in the App window
|
|
40
|
+
"Installation does not mean ready. Approve boot and control tasks once in the App window. An executor-only upgrade stays ready without approval (INFO anchors the executor digest); a policy digest change requires approval again.",
|
|
37
41
|
]
|
|
38
42
|
: [],
|
|
39
43
|
};
|
|
@@ -59,7 +63,56 @@ export function planDeployment(root, options = {}) {
|
|
|
59
63
|
],
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
/** DSM reports the aarch64 family; a native layout may name it armv8. */
|
|
67
|
+
function layoutArchFor(arches, family) {
|
|
68
|
+
const names = family === "aarch64" ? ["aarch64", "armv8"] : [family];
|
|
69
|
+
return names.find((name) => arches.includes(name));
|
|
70
|
+
}
|
|
71
|
+
function loadLayout(root) {
|
|
72
|
+
const loaded = loadNativeLayout(root);
|
|
73
|
+
if (!loaded.ok)
|
|
74
|
+
throw fail(loaded.errors[0].code, loaded.errors.map((e) => e.message).join("\n"));
|
|
75
|
+
return loaded.manifest;
|
|
76
|
+
}
|
|
77
|
+
function planNativeLayout(root, options) {
|
|
78
|
+
const m = loadLayout(root);
|
|
79
|
+
if (options.dev)
|
|
80
|
+
throw fail("DEVELOPMENT_INSTANCE_UNSUPPORTED", "--dev is not supported for a native layout; choose a distinct package name");
|
|
81
|
+
if (options.arch && !layoutArchFor(m.arches, options.arch))
|
|
82
|
+
throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${options.arch}; add it to dsm-package.yaml arches to deploy it`);
|
|
83
|
+
return {
|
|
84
|
+
root,
|
|
85
|
+
kind: "native-layout",
|
|
86
|
+
packageId: m.package,
|
|
87
|
+
version: `${m.version}-${String(m.build).padStart(4, "0")}`,
|
|
88
|
+
arch: options.arch,
|
|
89
|
+
capabilities: [],
|
|
90
|
+
transport: "dsm-local-spk",
|
|
91
|
+
notes: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function buildForDeploy(plan, arch, installed, options) {
|
|
95
|
+
if (plan.kind === "native-layout") {
|
|
96
|
+
const m = loadLayout(plan.root);
|
|
97
|
+
const layoutArch = layoutArchFor(m.arches, arch);
|
|
98
|
+
if (!layoutArch)
|
|
99
|
+
throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${arch}; add it to dsm-package.yaml arches to deploy it`);
|
|
100
|
+
const name = Object.keys(options.params ?? {})[0];
|
|
101
|
+
if (name)
|
|
102
|
+
throw fail("DEPLOY_PARAM_UNDECLARED", `${m.package} has no install wizard values; remove --param ${name}.`);
|
|
103
|
+
const { outputs } = await buildNativeLayout({ root: plan.root, arches: [layoutArch], run: options.run });
|
|
104
|
+
return { outputs, extraValues: {} };
|
|
105
|
+
}
|
|
106
|
+
const loaded = loadStandaloneManifest(plan.root);
|
|
107
|
+
if (!loaded.ok)
|
|
108
|
+
throw fail(loaded.errors[0].code, loaded.errors.map((e) => e.message).join("\n"));
|
|
109
|
+
const values = resolveNativeInstallValues(loaded.manifest, options.params ?? {}, { installed });
|
|
110
|
+
if (!values.ok)
|
|
111
|
+
throw fail(values.code, values.message);
|
|
112
|
+
const { outputs } = await buildStandalone({ root: plan.root, arches: [arch], run: options.run });
|
|
113
|
+
return { outputs, extraValues: values.extraValues };
|
|
114
|
+
}
|
|
115
|
+
export async function deployStandalone(plan, host, options = {}) {
|
|
63
116
|
if (plan.kind === "container")
|
|
64
117
|
throw fail("DEPLOY_PLAN_INVALID", "Expected native-go or binary deployment plan");
|
|
65
118
|
const arch = plan.arch ?? (await host.getArchFamily?.());
|
|
@@ -70,16 +123,14 @@ export async function deployStandalone(plan, host) {
|
|
|
70
123
|
nextDeployVersion(plan.version, installed) !== plan.version) {
|
|
71
124
|
throw fail("DEPLOY_VERSION_INVALID", `Increase dsm-package.yaml version/build above installed ${installed}`);
|
|
72
125
|
}
|
|
73
|
-
const { outputs } = await
|
|
74
|
-
root: plan.root,
|
|
75
|
-
arches: [arch],
|
|
76
|
-
});
|
|
126
|
+
const { outputs, extraValues } = await buildForDeploy(plan, arch, Boolean(installed), options);
|
|
77
127
|
if (outputs.length !== 1)
|
|
78
128
|
throw fail("DEPLOY_BUILD_OUTPUT_INVALID", "Expected one SPK");
|
|
79
129
|
await host.installFromLocalSpk({
|
|
80
130
|
packageId: plan.packageId,
|
|
81
131
|
version: plan.version,
|
|
82
132
|
spkPath: outputs[0],
|
|
133
|
+
extraValues,
|
|
83
134
|
});
|
|
84
135
|
return outputs[0];
|
|
85
136
|
}
|
package/dist/error-registry.js
CHANGED
|
@@ -97,12 +97,12 @@ export const ERROR_REGISTRY = {
|
|
|
97
97
|
"GATEWAY_GO_MISSING": {
|
|
98
98
|
"code": "GATEWAY_GO_MISSING",
|
|
99
99
|
"description": "建置 DSM 驗證閘道需要 Go,但找不到 go 指令。",
|
|
100
|
-
"suggestion": "請安裝 Go 1.
|
|
100
|
+
"suggestion": "請安裝 Go 1.21 以上版本(https://go.dev/dl/,建置時固定使用 go1.26.9),或在 integration.web.access 明確選 public。"
|
|
101
101
|
},
|
|
102
102
|
"GATEWAY_BUILD_FAILED": {
|
|
103
103
|
"code": "GATEWAY_BUILD_FAILED",
|
|
104
104
|
"description": "DSM 驗證閘道建置失敗或產出不是對應架構的靜態 ELF。",
|
|
105
|
-
"suggestion": "請查看 go build
|
|
105
|
+
"suggestion": "請查看 go build 輸出;閘道固定以 go1.26.9 建置,離線時請安裝 go1.26.9 或讓 GOPROXY 可提供 golang.org/toolchain,並確認 templates/_gateway 未被修改。"
|
|
106
106
|
},
|
|
107
107
|
"CONTRACT_INVALID_STRING": {
|
|
108
108
|
"code": "CONTRACT_INVALID_STRING",
|
|
@@ -346,8 +346,13 @@ export const ERROR_REGISTRY = {
|
|
|
346
346
|
},
|
|
347
347
|
"DEPLOY_PARAM_UNDECLARED": {
|
|
348
348
|
"code": "DEPLOY_PARAM_UNDECLARED",
|
|
349
|
-
"description": "deploy --param 提供了契約未宣告的 Install Parameter
|
|
350
|
-
"suggestion": "請只傳 synology-app.yaml install 中宣告的 string 或 boolean
|
|
349
|
+
"description": "deploy --param 提供了契約未宣告的 Install Parameter,或 native-go/binary 套件沒有的安裝精靈值。",
|
|
350
|
+
"suggestion": "Compose 請只傳 synology-app.yaml install 中宣告的 string 或 boolean 名稱;native-go/binary 只接受 install-selected delegate 的 delegate_share 或 delegate_home。"
|
|
351
|
+
},
|
|
352
|
+
"DEPLOY_PARAM_REQUIRED": {
|
|
353
|
+
"code": "DEPLOY_PARAM_REQUIRED",
|
|
354
|
+
"description": "首次安裝 install-selected delegate 套件時沒有提供安裝精靈的範圍值,preinst 會拒絕安裝。",
|
|
355
|
+
"suggestion": "加上 --param delegate_share=<共用資料夾> 或 --param delegate_home=true 再部署。"
|
|
351
356
|
},
|
|
352
357
|
"DEPLOY_PARAM_TYPE_INVALID": {
|
|
353
358
|
"code": "DEPLOY_PARAM_TYPE_INVALID",
|
|
@@ -412,7 +417,7 @@ export const ERROR_REGISTRY = {
|
|
|
412
417
|
"STANDALONE_DAEMON_BUILD_FAILED": {
|
|
413
418
|
"code": "STANDALONE_DAEMON_BUILD_FAILED",
|
|
414
419
|
"description": "無法建置套件 daemon 的 Go supervisor。",
|
|
415
|
-
"suggestion": "安裝 Go 1.
|
|
420
|
+
"suggestion": "安裝 Go 1.21 以上(建置固定使用 go1.26.9,離線時請先備妥)並檢查 compiler stderr;NAS 不需要 Go toolchain。"
|
|
416
421
|
},
|
|
417
422
|
"STANDALONE_DAEMON_PAYLOAD_INVALID": {
|
|
418
423
|
"code": "STANDALONE_DAEMON_PAYLOAD_INVALID",
|
|
@@ -685,7 +690,7 @@ export function getErrorCode(code) {
|
|
|
685
690
|
return ERROR_REGISTRY[code];
|
|
686
691
|
}
|
|
687
692
|
for (const [code, description, suggestion] of [
|
|
688
|
-
["EXECUTOR_BUILD_FAILED", "executor 或 lifecycle helper 編譯失敗,或 CLI 套件缺少 executor 原始碼。", "確認已安裝 Go
|
|
693
|
+
["EXECUTOR_BUILD_FAILED", "executor 或 lifecycle helper 編譯失敗,或 CLI 套件缺少 executor 原始碼。", "確認已安裝 Go 1.21 以上且可取得 go1.26.9(離線時請先備妥),查看訊息中的編譯器輸出;原始碼缺少時重新安裝 CLI。"],
|
|
689
694
|
["EXECUTOR_PIN_REQUIRED", "宣告 rootOps 或 delegate 的套件建置時沒有 executor 產物。", "使用 synopkgland build,讓 CLI 編譯並 pin executor;不要直接呼叫低階 SPK API。"],
|
|
690
695
|
["EXECUTOR_POLICY", "capabilities.delegate 的 operations 或 scope 不合法,無法產生 executor policy。", "改用 init/add 產生的 delegate 宣告,或依 manifest 參考修正 scope 與 operations。"],
|
|
691
696
|
["EXECUTOR_PACKAGE", "pin 進 root task 的套件名稱不合法。", "package 必須以英文字母開頭,只含英數字(2–64 字)。"],
|
|
@@ -697,3 +702,7 @@ for (const [code, description, suggestion] of [
|
|
|
697
702
|
ERROR_REGISTRY["STANDALONE_ENTRY_INVALID"] = { "code": "STANDALONE_ENTRY_INVALID", "description": "entry 設定不合法。", "suggestion": "entry 填 admin-only 或 app-privilege;app-privilege 需要至少一個 authenticated route。" };
|
|
698
703
|
ERROR_REGISTRY["STANDALONE_ENTRY_WIDENED"] = { "code": "STANDALONE_ENTRY_WIDENED", "description": "entry: admin-only 與放寬權限的 route 設定衝突。", "suggestion": "把 permissions.json 中 authenticated 或 public 的 route 改為 administrators;要開放給一般使用者請改用 entry: app-privilege 或省略 entry。" };
|
|
699
704
|
ERROR_REGISTRY["STANDALONE_ENTRY_UNSUPPORTED"] = { "code": "STANDALONE_ENTRY_UNSUPPORTED", "description": "backend 無法強制 entry 模式。", "suggestion": "使用 Go backend,複製 SDK 的 internal/dsm entry gate 並在 cmd/app/main.go 呼叫 dsm.NewEntryGate。" };
|
|
705
|
+
ERROR_REGISTRY["DSM_TLS_UNTRUSTED"] = { "code": "DSM_TLS_UNTRUSTED", "description": "DSM 的 TLS 憑證不受這台電腦信任(自簽、過期或名稱不符)。", "suggestion": "把 NAS 憑證或其 CA 匯出成 PEM,用 NODE_EXTRA_CA_CERTS=<該檔> 執行;或讓 DSM 使用與 host profile 名稱相符的受信任憑證。不要關閉憑證驗證。" };
|
|
706
|
+
ERROR_REGISTRY["STANDALONE_ICON_INVALID"] = { "code": "STANDALONE_ICON_INVALID", "description": "dsm-package.yaml 的 icon 不是可用的套件圖示。", "suggestion": "icon 填專案內的正方形 PNG(至少 256×256),或含 icon_256.png 的 icon_<size>.png 目錄;省略則使用 SDK 預設圖示。" };
|
|
707
|
+
ERROR_REGISTRY["CONTRACT_ICON_INVALID"] = { "code": "CONTRACT_ICON_INVALID", "description": "synology-app.yaml 的 icon 不是可用的套件圖示。", "suggestion": "icon 填專案內的正方形 PNG(至少 256×256),或含 icon_256.png 的 icon_<size>.png 目錄;省略則使用 SDK 預設圖示。" };
|
|
708
|
+
ERROR_REGISTRY["CONTRACT_I18N_INVALID"] = { "code": "CONTRACT_I18N_INVALID", "description": "synology-app.yaml 的 i18n 設定不合法。", "suggestion": "i18n 只接受 info;info 以 zh-TW、ja 等支援語言為 key,值為單行、不含 \" 或 \\ 的 displayname / description(英文寫在頂層)。" };
|
package/dist/host/dsm-client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listShareNamesWithSynoagentcli, lookupSharePathWithSynoagentcli, } from
|
|
|
3
3
|
import { createSynoagentcliApiCall, fetchSharePermissions, fetchUserGroups, } from "./share-permission-lookup.js";
|
|
4
4
|
import { fetchPortInfo } from "./port-info-lookup.js";
|
|
5
5
|
import { dsmFetch } from "./dsm-http.js";
|
|
6
|
+
import { versionUnsupported } from "../runtime/dsm-version.js";
|
|
6
7
|
import { fetchDsmPortProfileInventory } from "./dsm-port-profile.js";
|
|
7
8
|
import { installPackageFromLocalSpk, fetchInstalledPackageState, packageStateFromRecord, } from "./catalog-install.js";
|
|
8
9
|
import { resolveHostCredentials } from "./credentials.js";
|
|
@@ -53,6 +54,16 @@ export function homesPathFromDsmUserHomeRead(result) {
|
|
|
53
54
|
}
|
|
54
55
|
return `${volume}/homes`;
|
|
55
56
|
}
|
|
57
|
+
function setCookiePairs(headers) {
|
|
58
|
+
const lines = typeof headers.getSetCookie === "function"
|
|
59
|
+
? headers.getSetCookie()
|
|
60
|
+
: headers.get("set-cookie")
|
|
61
|
+
? [headers.get("set-cookie")]
|
|
62
|
+
: [];
|
|
63
|
+
return lines
|
|
64
|
+
.map((line) => line.split(";")[0]?.trim())
|
|
65
|
+
.filter((pair) => Boolean(pair));
|
|
66
|
+
}
|
|
56
67
|
function createDsmNasHost(credentials, profileName) {
|
|
57
68
|
const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
|
|
58
69
|
let cookieHeader = credentials.cookieHeader;
|
|
@@ -77,15 +88,7 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
77
88
|
const login = (await response.json());
|
|
78
89
|
if (!response.ok || login.success !== true)
|
|
79
90
|
throw new Error("DSM login failed");
|
|
80
|
-
|
|
81
|
-
? response.headers.getSetCookie()
|
|
82
|
-
: response.headers.get("set-cookie")
|
|
83
|
-
? [response.headers.get("set-cookie")]
|
|
84
|
-
: [];
|
|
85
|
-
const pairs = setCookie
|
|
86
|
-
.map((line) => line.split(";")[0]?.trim())
|
|
87
|
-
.filter((pair) => Boolean(pair));
|
|
88
|
-
cookieHeader = pairs.join("; ");
|
|
91
|
+
cookieHeader = setCookiePairs(response.headers).join("; ");
|
|
89
92
|
if (!cookieHeader)
|
|
90
93
|
throw new Error("DSM login returned no session cookie");
|
|
91
94
|
};
|
|
@@ -106,12 +109,38 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
106
109
|
async getExecutorStatus(packageIdentity) {
|
|
107
110
|
try {
|
|
108
111
|
await ensureLogin();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const appCall = async (method, cookie) => {
|
|
113
|
+
const response = await dsmFetch(`${dsmOrigin}/webman/3rdparty/${encodeURIComponent(packageIdentity)}/cgi/api.cgi`, {
|
|
114
|
+
method: "POST", headers: { cookie, origin: dsmOrigin, "content-type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ method, params: method === "identity.session" ? { bootstrap: true } : {} }),
|
|
116
|
+
});
|
|
117
|
+
const body = await response.json();
|
|
118
|
+
return { body, setCookie: setCookiePairs(response.headers) };
|
|
119
|
+
};
|
|
120
|
+
// A paused executor answers with why and what to do next.
|
|
121
|
+
const explain = (status) => status.state === "paused" ? { ...status, error: `Executor paused (${status.reason}). ${status.next}` } : status;
|
|
122
|
+
const direct = await appCall("executor.status", cookieHeader ?? "");
|
|
123
|
+
if (direct.body.success)
|
|
124
|
+
return explain(direct.body.data);
|
|
125
|
+
// identity: standalone wraps business methods in an App session; trade the
|
|
126
|
+
// administrator's DSM cookie for one, read once, then end that session.
|
|
127
|
+
if (direct.body.error?.code !== "session_required")
|
|
128
|
+
return undefined;
|
|
129
|
+
const session = await appCall("identity.session", cookieHeader ?? "");
|
|
130
|
+
if (!session.body.success) {
|
|
131
|
+
const code = session.body.error?.code;
|
|
132
|
+
return { ready: false, executor_digest: "", policy_digest: "", error: code === "https_required"
|
|
133
|
+
? "identity: standalone status needs an https:// DSM host profile to open an App session."
|
|
134
|
+
: `identity: standalone App session bootstrap failed (${code ?? "error"}).` };
|
|
135
|
+
}
|
|
136
|
+
const appCookie = [cookieHeader ?? "", ...session.setCookie].filter(Boolean).join("; ");
|
|
137
|
+
try {
|
|
138
|
+
const status = await appCall("executor.status", appCookie);
|
|
139
|
+
return status.body.success ? explain(status.body.data) : undefined;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await appCall("identity.logout", appCookie).catch(() => undefined);
|
|
143
|
+
}
|
|
115
144
|
}
|
|
116
145
|
catch {
|
|
117
146
|
return undefined;
|
|
@@ -130,6 +159,12 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
130
159
|
throw Object.assign(new Error(`DSM ${body.error?.code ?? "error"}: scheduler readback denied; check the administrator session. No retry was made.`), { code: body.error?.code });
|
|
131
160
|
return body.data;
|
|
132
161
|
};
|
|
162
|
+
// DSM 7.1 stops at TaskScheduler list v2 (offset) and get v3; only an unsupported version falls back.
|
|
163
|
+
const taskRead = (method, version, params, legacyVersion, legacyParams) => call("SYNO.Core.TaskScheduler", method, version, params).catch((e) => {
|
|
164
|
+
if (!versionUnsupported(e.code))
|
|
165
|
+
throw e;
|
|
166
|
+
return call("SYNO.Core.TaskScheduler", method, legacyVersion, legacyParams);
|
|
167
|
+
});
|
|
133
168
|
const records = await call("SYNO.Core.EventScheduler", "list", 1, {});
|
|
134
169
|
if (!Array.isArray(records))
|
|
135
170
|
throw new Error("Invalid EventScheduler readback.");
|
|
@@ -142,13 +177,14 @@ function createDsmNasHost(credentials, profileName) {
|
|
|
142
177
|
throw new Error("Invalid boot task readback.");
|
|
143
178
|
tasks.push({ ...detail, name: detail.task_name, kind: "boot" });
|
|
144
179
|
}
|
|
145
|
-
const listed = await
|
|
180
|
+
const listed = await taskRead("list", 3, { start: 0, limit: 500 }, 2, { offset: 0, limit: 500 });
|
|
146
181
|
if (!Array.isArray(listed?.tasks) || !Number.isInteger(listed.total) || listed.total !== listed.tasks.length || listed.total > 500)
|
|
147
182
|
throw new Error("Incomplete TaskScheduler readback.");
|
|
148
183
|
for (const summary of listed.tasks) {
|
|
149
184
|
if (summary?.name !== `${packageIdentity} executor control`)
|
|
150
185
|
continue;
|
|
151
|
-
const
|
|
186
|
+
const ref = { id: summary.id, real_owner: summary.real_owner };
|
|
187
|
+
const detail = await taskRead("get", 4, ref, 3, ref);
|
|
152
188
|
if (!detail || String(detail.id) !== String(summary.id) || detail.name !== summary.name)
|
|
153
189
|
throw new Error("Invalid control task readback.");
|
|
154
190
|
tasks.push({ ...detail, kind: "control" });
|