@synopackageland/cli 0.9.4 → 0.9.6
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 +94 -0
- package/README.md +1 -1
- package/assets/licenses/MUSL-COPYRIGHT +193 -0
- package/assets/licenses/RUST-COPYRIGHT-library.html +5556 -0
- package/dist/add.js +29 -29
- package/dist/build-spk/dsm-version.d.ts +14 -4
- package/dist/build-spk/dsm-version.js +17 -5
- package/dist/build-spk/notices.d.ts +8 -1
- package/dist/build-spk/notices.js +13 -4
- package/dist/build-spk/uninstall-wizard.d.ts +11 -0
- package/dist/build-spk/uninstall-wizard.js +16 -3
- package/dist/build-spk/wizard-i18n.d.ts +1 -1
- package/dist/build-spk/wizard-i18n.js +1 -1
- package/dist/build-spk/wizard-messages.d.ts +44 -0
- package/dist/build-spk/wizard-messages.js +44 -0
- package/dist/capability-overlays.d.ts +10 -5
- package/dist/capability-overlays.js +32 -10
- package/dist/cli.js +5 -5
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/plan.js +1 -1
- package/dist/error-registry.js +46 -4
- package/dist/info.js +4 -3
- package/dist/init.js +53 -28
- package/dist/portal/alias.d.ts +8 -0
- package/dist/portal/alias.js +16 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +2 -1
- package/dist/runtime/executor-digests.json +22 -0
- package/dist/runtime/executor-policy.d.ts +15 -0
- package/dist/runtime/executor-policy.js +32 -6
- package/dist/runtime/sdk-source.d.ts +14 -2
- package/dist/runtime/sdk-source.js +39 -4
- package/dist/runtime/uninstall-cleanup.d.ts +5 -1
- package/dist/runtime/uninstall-cleanup.js +11 -1
- package/dist/standalone/api-permissions.d.ts +4 -2
- package/dist/standalone/api-permissions.js +6 -5
- package/dist/standalone/binary.d.ts +3 -2
- package/dist/standalone/binary.js +17 -5
- package/dist/standalone/build.js +16 -12
- package/dist/standalone/daemon.d.ts +6 -0
- package/dist/standalone/daemon.js +8 -4
- package/dist/standalone/dsm-window.js +16 -3
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +46 -13
- package/dist/standalone/lifecycle.js +38 -4
- package/dist/standalone/manifest.d.ts +12 -0
- package/dist/standalone/manifest.js +62 -20
- package/dist/standalone/native-layout.d.ts +4 -7
- package/dist/standalone/native-layout.js +41 -102
- package/dist/standalone/native-root-window.d.ts +2 -1
- package/dist/standalone/native-root-window.js +15 -5
- package/dist/standalone/portal-window.d.ts +4 -2
- package/dist/standalone/portal-window.js +54 -12
- package/dist/standalone/portal.d.ts +4 -1
- package/dist/standalone/portal.js +8 -2
- package/dist/standalone/root-ops.d.ts +2 -0
- package/dist/standalone/root-ops.js +2 -0
- package/dist/standalone/rust-build.d.ts +35 -0
- package/dist/standalone/rust-build.js +127 -0
- package/dist/standalone/sdk-check.d.ts +5 -3
- package/dist/standalone/sdk-check.js +21 -8
- package/dist/standalone/spk.js +15 -3
- package/dist/standalone/toolkit.d.ts +10 -0
- package/dist/standalone/toolkit.js +101 -0
- package/dist/standalone/uninstall-hook.d.ts +10 -0
- package/dist/standalone/uninstall-hook.js +71 -0
- package/dist/standalone/uninstall.d.ts +53 -0
- package/dist/standalone/uninstall.js +136 -0
- package/dist/types.d.ts +1 -1
- package/dist/yaml-edit.d.ts +6 -0
- package/dist/yaml-edit.js +31 -0
- package/package.json +2 -2
- package/skills/reference/portal.md +8 -4
package/dist/error-registry.js
CHANGED
|
@@ -409,6 +409,41 @@ export const ERROR_REGISTRY = {
|
|
|
409
409
|
"description": "套件 daemon 的宣告不符合固定 argv、環境與生命週期契約。",
|
|
410
410
|
"suggestion": "核對 daemon.exec、各架構 targets、args、env、uninstall_args 與整數秒 timeouts;移除 shell、user 與自訂 conf。"
|
|
411
411
|
},
|
|
412
|
+
"UNINSTALL_INVALID": {
|
|
413
|
+
"code": "UNINSTALL_INVALID",
|
|
414
|
+
"description": "uninstall 的宣告不符合固定 exec、argv、環境與逾時契約。",
|
|
415
|
+
"suggestion": "核對 uninstall.purge 的 exec(套件內、不在 executor/ 或 root/)、args、env(不可用 SYNOPKGLAND_*)與 2–120 秒 timeout,以及 state.items 的唯一 key。"
|
|
416
|
+
},
|
|
417
|
+
"UNINSTALL_STATE_REQUIRES_PURGE": {
|
|
418
|
+
"code": "UNINSTALL_STATE_REQUIRES_PURGE",
|
|
419
|
+
"description": "uninstall.state 沒有可刪除狀態的 App 卸載 hook。",
|
|
420
|
+
"suggestion": "加上 uninstall.purge,由 App 在 SYNOPKGLAND_DELETE_STATE=1 時刪除所列狀態;或移除 uninstall.state。"
|
|
421
|
+
},
|
|
422
|
+
"UNINSTALL_STATE_TEXT_MISSING": {
|
|
423
|
+
"code": "UNINSTALL_STATE_TEXT_MISSING",
|
|
424
|
+
"description": "卸載精靈無法列出 uninstall.state 項目的文字。",
|
|
425
|
+
"suggestion": "在 i18n.messages 的 en.json(與其他語言)加入 uninstall.state.<key>。"
|
|
426
|
+
},
|
|
427
|
+
"UNINSTALL_ROOT_REQUIRES_ROOT_OPS": {
|
|
428
|
+
"code": "UNINSTALL_ROOT_REQUIRES_ROOT_OPS",
|
|
429
|
+
"description": "uninstall.purge.root 需要管理員核准的 root 操作。",
|
|
430
|
+
"suggestion": "宣告 capabilities.rootOps: true 並提供 root/<名稱>.sh,或移除 uninstall.purge.root。"
|
|
431
|
+
},
|
|
432
|
+
"UNINSTALL_ROOT_OP_MISSING": {
|
|
433
|
+
"code": "UNINSTALL_ROOT_OP_MISSING",
|
|
434
|
+
"description": "uninstall.purge.root 指向不存在的 root 操作。",
|
|
435
|
+
"suggestion": "新增 root/<名稱>.sh,或把 uninstall.purge.root 改成既有的 root 操作名稱。"
|
|
436
|
+
},
|
|
437
|
+
"UNINSTALL_EXEC_MISSING": {
|
|
438
|
+
"code": "UNINSTALL_EXEC_MISSING",
|
|
439
|
+
"description": "uninstall.purge.exec 不是建置後套件內可執行的一般檔案。",
|
|
440
|
+
"suggestion": "指向套件內的執行檔,例如 ui/cgi/api.cgi、background/worker 或 daemon/payload/ 下具執行權限的檔案。"
|
|
441
|
+
},
|
|
442
|
+
"UNINSTALL_EXEC_UNSAFE": {
|
|
443
|
+
"code": "UNINSTALL_EXEC_UNSAFE",
|
|
444
|
+
"description": "uninstall.purge.exec 是嘗試取得 root 的 shell script。",
|
|
445
|
+
"suggestion": "hook 一律以套件帳號執行;需要 root 的清理請放在 root/<名稱>.sh 並以 uninstall.purge.root 宣告。"
|
|
446
|
+
},
|
|
412
447
|
"STANDALONE_DAEMON_BUILD_FAILED": {
|
|
413
448
|
"code": "STANDALONE_DAEMON_BUILD_FAILED",
|
|
414
449
|
"description": "無法建置套件 daemon 的 Go supervisor。",
|
|
@@ -584,6 +619,11 @@ export const ERROR_REGISTRY = {
|
|
|
584
619
|
"description": "go build 失敗。",
|
|
585
620
|
"suggestion": "請在 backend 目錄執行 go build ./... 並修正錯誤。取不到 github.com/synopackageland/syno-pkg-land-sdk 時,設定 GOPRIVATE=github.com/synopackageland/* 並讓 git 能以憑證讀 github.com。"
|
|
586
621
|
},
|
|
622
|
+
"STANDALONE_RUST_BUILD_FAILED": {
|
|
623
|
+
"code": "STANDALONE_RUST_BUILD_FAILED",
|
|
624
|
+
"description": "cargo build 失敗。",
|
|
625
|
+
"suggestion": "請在 backend 目錄執行 cargo build --release --locked 並修正錯誤;Cargo.lock 必須已提交。取不到 github.com/synopackageland/syno-pkg-land-sdk 時,設定 CARGO_NET_GIT_FETCH_WITH_CLI=true 並讓 git 能以憑證讀 github.com。"
|
|
626
|
+
},
|
|
587
627
|
"STANDALONE_PORTAL_INVALID": {
|
|
588
628
|
"code": "STANDALONE_PORTAL_INVALID",
|
|
589
629
|
"description": "dsm-package.yaml 的 portal 格式或 alias 不正確。",
|
|
@@ -649,7 +689,8 @@ for (const [code, description, suggestion] of [
|
|
|
649
689
|
ERROR_REGISTRY[code] = { code, description, suggestion };
|
|
650
690
|
ERROR_REGISTRY["ADD_LAYOUT_UNSUPPORTED"] = { "code": "ADD_LAYOUT_UNSUPPORTED", "description": "SDK overlay 需要標準 backend 目錄。", "suggestion": "使用 backend.dir: backend,或手動合併 SDK overlay。" };
|
|
651
691
|
ERROR_REGISTRY["MIN_DSM_CONTAINER_FLOOR"] = { "code": "MIN_DSM_CONTAINER_FLOOR", "description": "Compose 最低 DSM 版本過低。", "suggestion": "Compose 需要 DSM 7.2.1-69057 或以上。" };
|
|
652
|
-
ERROR_REGISTRY.
|
|
692
|
+
ERROR_REGISTRY.STANDALONE_PORTAL_ENTRY_INVALID = { "code": "STANDALONE_PORTAL_ENTRY_INVALID", "description": "os_min_ver 低於 DSM 7.2-64570 時由 executor 註冊 portal,套件名稱或 displayname 不符合它的規則。", "suggestion": "套件名稱只能用英數字,以 . _ - 分隔,最多 64 字;displayname 1–64 個字,不可含控制字元。" };
|
|
693
|
+
ERROR_REGISTRY.STANDALONE_PORTAL_MIN_DSM = { "code": "STANDALONE_PORTAL_MIN_DSM", "description": "宣告 portal 的套件 os_min_ver 低於 DSM 7.1-42661。", "suggestion": "DSM 7.2-64570 以前由 executor 寫入 DSM 的 app.d 註冊入口,這個格式只在 DSM 7.1 起驗證過。把 os_min_ver 提高到 7.1-42661 以上,或移除 portal。" };
|
|
653
694
|
ERROR_REGISTRY["MIN_DSM_INVALID"] = { "code": "MIN_DSM_INVALID", "description": "最低 DSM 版本格式不合法。", "suggestion": "指定 7.x release 或完整 7.x-build。" };
|
|
654
695
|
ERROR_REGISTRY["NOTICE_UNSAFE"] = { "code": "NOTICE_UNSAFE", "description": "授權檔案不符合一般檔案限制。", "suggestion": "移除 symlink、特殊檔與超大授權檔案。" };
|
|
655
696
|
ERROR_REGISTRY["NOTICE_MISSING"] = { "code": "NOTICE_MISSING", "description": "缺少第三方授權資訊。", "suggestion": "重新安裝模板依賴並產生 third-party notices。" };
|
|
@@ -697,9 +738,10 @@ for (const [code, description, suggestion] of [
|
|
|
697
738
|
["EXECUTOR_BUILD_FAILED", "從 App 相依的 SDK 版本編譯 executor 或 lifecycle helper 失敗。", "確認已安裝 Go 1.21 以上且可取得 go1.26.9(離線時請先備妥),查看訊息中的編譯器輸出。"],
|
|
698
739
|
["EXECUTOR_DIGEST_MISMATCH", "由 SDK 版本建出的 executor 與 CLI 內建的 sha256 表不符,或表中沒有這個 SDK 版本。", "確認 backend/go.mod 的 github.com/synopackageland/syno-pkg-land-sdk 是正式版本且 Go 為 go1.26.9;表中沒有該版本時升級 CLI。"],
|
|
699
740
|
["EXECUTOR_DIGEST_UNVERIFIED", "SYNOPKGLAND_SDK_DIR 指向本機 SDK,executor 未對照 sha256 表(警告)。", "只用於開發;要發佈的 SPK 請取消 SYNOPKGLAND_SDK_DIR 後重新 build。"],
|
|
700
|
-
["SDK_REQUIRED", "rootOps 或 delegate 需要 SDK
|
|
701
|
-
["
|
|
702
|
-
["
|
|
741
|
+
["SDK_REQUIRED", "rootOps 或 delegate 需要 SDK,但後端沒有相依 syno-pkg-land-sdk(Go:backend/go.mod;Rust:backend/Cargo.toml)。", "Go 在 backend/go.mod 加入 require github.com/synopackageland/syno-pkg-land-sdk;Rust 在 backend/Cargo.toml 以 git+tag 相依 syno-pkg-land-sdk;版本與 CLI 同一 minor,或改用 init 產生的後端。"],
|
|
742
|
+
["SDK_TAG_REQUIRED", "Rust 後端的 backend/Cargo.lock 沒有以正式 release tag 鎖定唯一一個 syno-pkg-land-sdk(branch、rev、path、registry 或多個版本都不接受)。", "在 backend/Cargo.toml 寫 syno-pkg-land-sdk = { git = \"https://github.com/synopackageland/syno-pkg-land-sdk\", tag = \"vX.Y.Z\" },執行 cargo update -p syno-pkg-land-sdk 並提交 Cargo.lock。"],
|
|
743
|
+
["SDK_VERSION_MISMATCH", "frontend/package.json 的 @synopackageland/sdk 與後端(backend/go.mod 或 backend/Cargo.lock)要求的 SDK 版本不同。", "把兩邊改成同一個版本;file: 相依時,比對的是該目錄 package.json 的 version。"],
|
|
744
|
+
["SDK_UNSUPPORTED", "後端(backend/go.mod 或 backend/Cargo.lock)要求的 SDK 不是這個 CLI 的 minor 版本線。", "改用與 SDK 同一 minor 的 CLI,或把 SDK 升到 CLI 支援的版本線(訊息列出)。"],
|
|
703
745
|
["SDK_DIR_NOT_READY", "SYNOPKGLAND_SDK_DIR 指向的 SDK checkout 根目錄缺少 go.mod,或 Vue 專案需要的 ts 套件尚未建置(沒有 ts/dist)。", "指向 syno-pkg-land-sdk checkout;Vue 專案先在 $SYNOPKGLAND_SDK_DIR/ts 執行 npm ci && npm run build(npm 安裝 file: 套件時不會替它 build)。"],
|
|
704
746
|
["SDK_DOWNLOAD_FAILED", "go mod download 取不到 backend/go.mod 要求的 SDK 版本。", "SDK repo 目前是 private,不能經 proxy.golang.org 取得:設定 GOPRIVATE=github.com/synopackageland/*(例如 go env -w),並讓 git 能以憑證讀 github.com(git ls-remote 可驗證);再確認版本存在。離線時先在同一台機器 go mod download 一次。"],
|
|
705
747
|
["TEMPLATES_MISSING", "找不到 @synopackageland/templates。", "重新安裝 @synopackageland/cli,或把 SYNOPKGLAND_TEMPLATES_DIR 指向 templates checkout。"],
|
package/dist/info.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { portalByExecutor } from "./build-spk/dsm-version.js";
|
|
3
4
|
import { loadContract } from "./contract/load.js";
|
|
4
5
|
import { appOriginFromContract } from "./host/open-url.js";
|
|
5
6
|
import { resolveDeployIdentity } from "./deploy/identity.js";
|
|
6
|
-
import { loadStandaloneManifest, } from "./standalone/manifest.js";
|
|
7
|
+
import { loadStandaloneManifest, sdkBackend, } from "./standalone/manifest.js";
|
|
7
8
|
import { inspectSpk } from "./standalone/inspect-spk.js";
|
|
8
9
|
import { isNativeLayout, loadNativeLayout } from "./standalone/native-layout.js";
|
|
9
10
|
import { executorPolicy, executorPolicyText, sha256 } from "./runtime/executor-policy.js";
|
|
@@ -69,7 +70,7 @@ export function classifyRootTasks(expected, inventory) {
|
|
|
69
70
|
async function liveRootCapability(m, host) {
|
|
70
71
|
try {
|
|
71
72
|
const policyDigest = sha256(executorPolicyText(executorPolicy(m)));
|
|
72
|
-
const classified = classifyRootTasks(executorTasks({ package: m.package, policyDigest }, requiredSdk(m
|
|
73
|
+
const classified = classifyRootTasks(executorTasks({ package: m.package, policyDigest }, requiredSdk(sdkBackend(m)).executorDir), await host.getRootTaskInventory?.(m.package));
|
|
73
74
|
if (classified.state !== "ready")
|
|
74
75
|
return classified;
|
|
75
76
|
const observed = await host.getExecutorStatus?.(m.package);
|
|
@@ -169,7 +170,7 @@ export async function standaloneInfo(options) {
|
|
|
169
170
|
...state,
|
|
170
171
|
capabilities,
|
|
171
172
|
...(executor ? { executor } : {}),
|
|
172
|
-
uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate), portal: Boolean(m.portal) }),
|
|
173
|
+
uninstallCleanup: uninstallCleanup(m.package, { executor: Boolean(m.capabilities.rootOps || m.capabilities.delegate), portal: Boolean(m.portal && !portalByExecutor(m.os_min_ver)), portalEntry: Boolean(m.portal && portalByExecutor(m.os_min_ver)) }),
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
/** What the installed App's portal.status says; DSM owns the alias, an administrator sets it. */
|
package/dist/init.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { isMap, isScalar, parse, parseDocument
|
|
2
|
+
import { isMap, isScalar, parse, parseDocument } from "yaml";
|
|
3
|
+
import { setTopLevel } from "./yaml-edit.js";
|
|
3
4
|
import { capabilityNames, capabilitySettings } from "./capabilities.js";
|
|
4
5
|
import { MOVE_FROM_STAGING } from './runtime/delegate-contract.js';
|
|
5
6
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
8
|
import { SDK_MODULE, SDK_PACKAGE } from "./runtime/sdk-source.js";
|
|
8
9
|
import { templatesRoot } from "./template-paths.js";
|
|
9
|
-
import { borrowedTarget, capabilityOverlays } from "./capability-overlays.js";
|
|
10
|
+
import { borrowedFiles, borrowedTarget, capabilityOverlays, VUE_OVERLAYS } from "./capability-overlays.js";
|
|
11
|
+
import { API_PERMISSIONS_FILE } from "./standalone/api-permissions.js";
|
|
10
12
|
import { resolveMinDsm, assertContainerMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast } from "./build-spk/dsm-version.js";
|
|
11
13
|
import { readSharedFolder } from "./standalone/shared-folder.js";
|
|
12
14
|
const TEMPLATE_NAMES = [
|
|
13
15
|
"compose",
|
|
14
16
|
"binary",
|
|
15
17
|
"native-go",
|
|
18
|
+
"native-rust",
|
|
16
19
|
];
|
|
20
|
+
/** Templates built from source with the SDK: capabilities, a Vue or plain UI, a shared folder. */
|
|
21
|
+
const NATIVE_TEMPLATES = ["native-go", "native-rust"];
|
|
22
|
+
const isNativeTemplate = (template) => NATIVE_TEMPLATES.includes(template);
|
|
23
|
+
const BACKEND_KIND = { "native-go": "go", "native-rust": "rust" };
|
|
17
24
|
/** Templates whose App runs through the DSM docker-project resource worker. */
|
|
18
25
|
const CONTAINER_TEMPLATES = [
|
|
19
26
|
"compose",
|
|
@@ -38,7 +45,7 @@ export function nativeCapabilitySettings(names) {
|
|
|
38
45
|
return settings;
|
|
39
46
|
}
|
|
40
47
|
/** Templates whose identity is filled in at init: every generated file may carry a placeholder. */
|
|
41
|
-
const IDENTIFIED = [
|
|
48
|
+
const IDENTIFIED = [...NATIVE_TEMPLATES, "binary"];
|
|
42
49
|
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
43
50
|
/** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
|
|
44
51
|
const RENAMED = { gitignore: ".gitignore" };
|
|
@@ -67,9 +74,9 @@ function templateFiles(root, dir = root) {
|
|
|
67
74
|
function targetName(file) {
|
|
68
75
|
return RENAMED[file] ?? file;
|
|
69
76
|
}
|
|
70
|
-
/**
|
|
77
|
+
/** Native templates default to the Vue shell; the CLI and the library entry point agree. */
|
|
71
78
|
function resolvedUi(template, options) {
|
|
72
|
-
return template
|
|
79
|
+
return isNativeTemplate(template) ? options.ui ?? "vue" : options.ui;
|
|
73
80
|
}
|
|
74
81
|
/**
|
|
75
82
|
* Copy one template tree into a project. A template directory may carry a local
|
|
@@ -93,9 +100,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
93
100
|
throw new Error("[CAPABILITY_COMBINATION_INVALID] delegate 的 install-selected scope 只能在 Vue 面板確認;plain UI 不支援,請改用 --ui vue。");
|
|
94
101
|
if (options.from && template !== "compose")
|
|
95
102
|
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --from 只適用於 compose。");
|
|
96
|
-
if (caps.length && template
|
|
103
|
+
if (caps.length && !isNativeTemplate(template))
|
|
97
104
|
throw new Error("[CAPABILITY_UNSUPPORTED] binary/compose 不支援 --with 能力。");
|
|
98
|
-
const localSdk = template
|
|
105
|
+
const localSdk = isNativeTemplate(template) && process.env.SYNOPKGLAND_SDK_DIR ? resolve(process.env.SYNOPKGLAND_SDK_DIR) : undefined;
|
|
99
106
|
if (localSdk)
|
|
100
107
|
assertLocalSdkReady(localSdk, resolvedUi(template, options) === "vue");
|
|
101
108
|
const source = join(templatesRoot(), template);
|
|
@@ -112,14 +119,15 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
112
119
|
if (rootExample && minDsm !== undefined && !isDsmAtLeast(minDsm, DEFAULT_MIN_DSM)) {
|
|
113
120
|
throw new Error(`[MIN_DSM_INVALID] root templates require DSM ${DEFAULT_MIN_DSM} or later.`);
|
|
114
121
|
}
|
|
115
|
-
const capability = capabilityOverlays(caps);
|
|
116
|
-
const
|
|
122
|
+
const capability = isNativeTemplate(template) ? capabilityOverlays(template, caps) : { overlays: [], borrowed: [], removed: [] };
|
|
123
|
+
const borrowed = borrowedFiles(templatesRoot(), capability.borrowed);
|
|
124
|
+
const overlays = [...(ui === "vue" && isNativeTemplate(template) ? VUE_OVERLAYS[template] : []), ...capability.overlays].map(name => join(templatesRoot(), name));
|
|
117
125
|
// Shared legal resources also live in the CLI package; only the template
|
|
118
126
|
// terms are an unconditional part of a generated project.
|
|
119
127
|
const shared = join(templatesRoot(), "_shared");
|
|
120
128
|
const sharedFiles = ["LICENSES/TEMPLATE.txt", "AGENTS.md", "CLAUDE.md"];
|
|
121
129
|
const files = [...templateFiles(source), ...sharedFiles, ...overlays.flatMap(root => templateFiles(root)),
|
|
122
|
-
...
|
|
130
|
+
...borrowed.map(borrowedTarget), ...(ui === "vue" ? ["frontend/third-party-notices.mjs"] : [])];
|
|
123
131
|
const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
|
|
124
132
|
if (collisions.length > 0) {
|
|
125
133
|
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
|
|
@@ -134,12 +142,14 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
134
142
|
for (const dir of (ui === "vue" ? ["frontend"] : [])) {
|
|
135
143
|
cpSync(join(shared, "third-party-notices.mjs"), join(targetDir, dir, "third-party-notices.mjs"));
|
|
136
144
|
}
|
|
137
|
-
for (const path of
|
|
145
|
+
for (const path of borrowed) {
|
|
146
|
+
mkdirSync(dirname(join(targetDir, borrowedTarget(path))), { recursive: true });
|
|
138
147
|
cpSync(join(templatesRoot(), path), join(targetDir, borrowedTarget(path)));
|
|
148
|
+
}
|
|
139
149
|
for (const file of capability.removed)
|
|
140
150
|
rmSync(join(targetDir, file));
|
|
141
|
-
if (caps.includes("delegate")) {
|
|
142
|
-
const path = join(targetDir, "backend
|
|
151
|
+
if (caps.includes("delegate") && isNativeTemplate(template)) {
|
|
152
|
+
const path = join(targetDir, "backend", API_PERMISSIONS_FILE[BACKEND_KIND[template]]);
|
|
143
153
|
const policy = JSON.parse(readFileSync(path, "utf8"));
|
|
144
154
|
policy.methods["app.status"] = "authenticated";
|
|
145
155
|
writeFileSync(path, JSON.stringify(policy, null, 2) + "\n");
|
|
@@ -151,8 +161,8 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
151
161
|
}
|
|
152
162
|
for (const file of files.map(targetName))
|
|
153
163
|
substitute(join(targetDir, file), substitutions);
|
|
154
|
-
if (localSdk)
|
|
155
|
-
useLocalSdk(targetDir, localSdk);
|
|
164
|
+
if (localSdk && isNativeTemplate(template))
|
|
165
|
+
useLocalSdk(targetDir, localSdk, BACKEND_KIND[template]);
|
|
156
166
|
if (minDsm) {
|
|
157
167
|
const manifest = join(targetDir, template !== "compose" ? "dsm-package.yaml" : "synology-app.yaml");
|
|
158
168
|
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(/^os_min_ver:.*$/m, `os_min_ver: "${minDsm}"`));
|
|
@@ -165,10 +175,9 @@ export function initFromTemplate(template, targetDir, options = {}) {
|
|
|
165
175
|
if (ui !== "vue")
|
|
166
176
|
rmSync(join(targetDir, "frontend"), { recursive: true, force: true });
|
|
167
177
|
if (template !== "compose") {
|
|
178
|
+
// Only the capabilities change: the template's comments document the optional fields.
|
|
168
179
|
const path = join(targetDir, "dsm-package.yaml");
|
|
169
|
-
|
|
170
|
-
raw.capabilities = nativeCapabilitySettings(caps);
|
|
171
|
-
writeFileSync(path, stringify(raw));
|
|
180
|
+
writeFileSync(path, setTopLevel(readFileSync(path, "utf8"), { capabilities: nativeCapabilitySettings(caps) }));
|
|
172
181
|
}
|
|
173
182
|
if (template === "compose" && options.from)
|
|
174
183
|
importCompose(options.from, targetDir, options);
|
|
@@ -236,16 +245,32 @@ function assertLocalSdkReady(sdkDir, vue) {
|
|
|
236
245
|
throw new Error(`[SDK_DIR_NOT_READY] ${ts} 尚未建置;前端安裝本機 SDK 時 npm 不會替它 build。請先執行 (cd ${ts} && npm ci && npm run build),SDK 原始碼改動後也要重新 build。`);
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
|
-
/**
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
/**
|
|
249
|
+
* SYNOPKGLAND_SDK_DIR: the project builds against a local sdk checkout instead
|
|
250
|
+
* of the released version. A Rust backend keeps its Cargo files; at build time
|
|
251
|
+
* git reads the SDK repository from the checkout.
|
|
252
|
+
*/
|
|
253
|
+
function useLocalSdk(targetDir, sdkDir, backend) {
|
|
254
|
+
if (backend === "go") {
|
|
255
|
+
const goMod = join(targetDir, "backend/go.mod");
|
|
256
|
+
writeFileSync(goMod, `${readFileSync(goMod, "utf8")}\nreplace ${SDK_MODULE} => ${sdkDir}\n`);
|
|
257
|
+
}
|
|
243
258
|
const frontend = join(targetDir, "frontend/package.json");
|
|
244
259
|
if (!existsSync(frontend))
|
|
245
260
|
return;
|
|
261
|
+
const local = `file:${join(sdkDir, "ts")}`;
|
|
246
262
|
const pkg = JSON.parse(readFileSync(frontend, "utf8"));
|
|
247
|
-
pkg.dependencies[SDK_PACKAGE] =
|
|
263
|
+
pkg.dependencies[SDK_PACKAGE] = local;
|
|
248
264
|
writeFileSync(frontend, JSON.stringify(pkg, null, 2) + "\n");
|
|
265
|
+
// npm installs what the lockfile records; without the released entry it installs the local package.
|
|
266
|
+
const lockPath = join(targetDir, "frontend/package-lock.json");
|
|
267
|
+
if (!existsSync(lockPath))
|
|
268
|
+
return;
|
|
269
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
270
|
+
if (lock.packages?.[""]?.dependencies)
|
|
271
|
+
lock.packages[""].dependencies[SDK_PACKAGE] = local;
|
|
272
|
+
delete lock.packages?.[`node_modules/${SDK_PACKAGE}`];
|
|
273
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
249
274
|
}
|
|
250
275
|
/** Fill the placeholders of one generated file; files without one, or removed by an overlay, stay untouched. */
|
|
251
276
|
function substitute(path, substitutions) {
|
|
@@ -258,11 +283,11 @@ function substitute(path, substitutions) {
|
|
|
258
283
|
}
|
|
259
284
|
function substitutionsFor(template, targetDir, options) {
|
|
260
285
|
const caps = capabilityNames(options.with ?? []);
|
|
261
|
-
if (options.ui !== undefined && template
|
|
262
|
-
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go。");
|
|
286
|
+
if (options.ui !== undefined && !isNativeTemplate(template))
|
|
287
|
+
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui 只適用於 native-go 與 native-rust。");
|
|
263
288
|
if (options.sharedFolder) {
|
|
264
|
-
if (template
|
|
265
|
-
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder 只適用於 -t native-go。");
|
|
289
|
+
if (!isNativeTemplate(template))
|
|
290
|
+
throw new Error("[INIT_SHARED_FOLDER_INVALID] --shared-folder 只適用於 -t native-go 與 native-rust。");
|
|
266
291
|
sharedFolderYaml(options.sharedFolder);
|
|
267
292
|
}
|
|
268
293
|
if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
|
package/dist/portal/alias.d.ts
CHANGED
|
@@ -25,3 +25,11 @@ export declare function isPortalAlias(value: unknown): value is string;
|
|
|
25
25
|
export declare function defaultPortalAlias(packageId: string): string;
|
|
26
26
|
/** `{}` or `{alias}`; undefined for any other shape. */
|
|
27
27
|
export declare function readPortalDeclaration(value: unknown, packageId: string): PortalDeclaration | undefined;
|
|
28
|
+
/** The Application Portal entry a root executor registers in DSM's app.d, as its approved policy states it. */
|
|
29
|
+
export interface PortalEntry {
|
|
30
|
+
/** Control Panel display name. */
|
|
31
|
+
application: string;
|
|
32
|
+
alias: string;
|
|
33
|
+
}
|
|
34
|
+
/** Why the executor would refuse this entry; undefined when it accepts it. */
|
|
35
|
+
export declare function portalEntryProblem(packageId: string, entry: PortalEntry): string | undefined;
|
package/dist/portal/alias.js
CHANGED
|
@@ -41,3 +41,19 @@ export function readPortalDeclaration(value, packageId) {
|
|
|
41
41
|
return { alias: defaultPortalAlias(packageId) };
|
|
42
42
|
return isPortalAlias(alias) ? { alias } : undefined;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The executor's own rules (executor/portal_entry.go): the package name and
|
|
46
|
+
* alias become DSM file and nginx names, the application a JSON string.
|
|
47
|
+
*/
|
|
48
|
+
const ENTRY_PACKAGE = /^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/;
|
|
49
|
+
const ENTRY_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
50
|
+
/** Why the executor would refuse this entry; undefined when it accepts it. */
|
|
51
|
+
export function portalEntryProblem(packageId, entry) {
|
|
52
|
+
if (packageId.length > 64 || !ENTRY_PACKAGE.test(packageId))
|
|
53
|
+
return `套件名稱「${packageId}」不能當作 Application Portal 的檔名:只能用英數字,以 . _ - 分隔,最多 64 字。`;
|
|
54
|
+
if (!ENTRY_ALIAS.test(entry.alias))
|
|
55
|
+
return `portal 別名「${entry.alias}」不能當作 Application Portal 的路徑。`;
|
|
56
|
+
if (!entry.application || [...entry.application].length > 64 || /\p{Cc}/u.test(entry.application))
|
|
57
|
+
return `displayname「${entry.application}」不能當作 Application Portal 的名稱:1–64 個字,不可含控制字元。`;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NativeArchFamily } from '../build-spk/dsm-arch.js';
|
|
2
2
|
import type { CommandRunner } from '../standalone/build.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type StandaloneManifest } from '../standalone/manifest.js';
|
|
4
4
|
import { type ExecutorPolicy } from './executor-policy.js';
|
|
5
5
|
import { type ExecutorPins, type ExecutorTask } from './executor-tasks.js';
|
|
6
6
|
import { type ExecutorDigestTable } from './executor-source.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from '../build-spk/go-toolchain.js';
|
|
4
|
+
import { sdkBackend } from '../standalone/manifest.js';
|
|
4
5
|
import { StandaloneBuildError } from '../standalone/spk.js';
|
|
5
6
|
import { checkCgiBinary } from '../standalone/binary.js';
|
|
6
7
|
import { executorPolicy, executorPolicyText, sha256 } from './executor-policy.js';
|
|
@@ -23,7 +24,7 @@ export async function buildExecutor(m, arch, work, run, options = {}) {
|
|
|
23
24
|
return undefined;
|
|
24
25
|
let sdk;
|
|
25
26
|
try {
|
|
26
|
-
sdk = requiredSdk(m
|
|
27
|
+
sdk = requiredSdk(sdkBackend(m), options.env, options.download);
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
30
|
throw failure(error);
|
|
@@ -64,5 +64,27 @@
|
|
|
64
64
|
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
65
65
|
},
|
|
66
66
|
"goToolchain": "go1.26.9"
|
|
67
|
+
},
|
|
68
|
+
"0.9.6": {
|
|
69
|
+
"x86_64": "4241dfebd2b819c2566eca062ee0021b99e7f86bbe4703f50833874f79b3cf54",
|
|
70
|
+
"aarch64": "923514aca8cf6576c6c52c737d53c2c7cc4bfcec40591dcec995005be41add95",
|
|
71
|
+
"armv7": "df959648ac8e6201f1cf9b86b57730c09ba1afb7ecc151ba2f530ad15dd90ccf",
|
|
72
|
+
"lifecycle": {
|
|
73
|
+
"x86_64": "819459580135e016184ad80c258350d57194d6134e8aa4cf1caf5237085b1004",
|
|
74
|
+
"aarch64": "5abcfc21403e84a0af15d972bcb819ac6fedfd9aaf5898ddc8c4bb06130f9f59",
|
|
75
|
+
"armv7": "b65a14f0fc77a1d3221c7dc6781eb66059aa200956b094d58a62bd1cad10f116"
|
|
76
|
+
},
|
|
77
|
+
"goToolchain": "go1.26.9"
|
|
78
|
+
},
|
|
79
|
+
"0.9.7": {
|
|
80
|
+
"x86_64": "d029ca6129e76cdc5b7016378722af166b511fb346fd1002082e611bec0aef4d",
|
|
81
|
+
"aarch64": "ac8018a9846a8aaca6f06323f033a27f7abef6705601efde57ea02565105a48d",
|
|
82
|
+
"armv7": "f7b4ee5fb90c8f2dd2c56ec1eeb7749300ad117a23d9df6da6840d8b2eff83b8",
|
|
83
|
+
"lifecycle": {
|
|
84
|
+
"x86_64": "f2f0a6aa83399688441b111adee3958d7df50e9d8e33939e3951da393f013819",
|
|
85
|
+
"aarch64": "d0f0d42f367093d5c9318afb4719b9ac76b1019574f1db34dd0fcb3aba44945b",
|
|
86
|
+
"armv7": "02da8b7016abfe00b8f18e27ef2d69bcff1ca376b43e17b8c04983ca6a6ef37d"
|
|
87
|
+
},
|
|
88
|
+
"goToolchain": "go1.26.9"
|
|
67
89
|
}
|
|
68
90
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { RootOp } from '../standalone/root-ops.js';
|
|
2
2
|
import type { EntryMode } from '../standalone/manifest.js';
|
|
3
|
+
import { type PortalDeclaration, type PortalEntry } from '../portal/alias.js';
|
|
3
4
|
export declare const FILE_OPERATIONS: readonly ["files.list", "files.info", "files.createFolder", "files.checkPermission", "files.write", "files.read", "files.rename", "files.copy", "files.move", "files.delete"];
|
|
4
5
|
/** Started File Station tasks are followed with these; they are implied, never declared. */
|
|
5
6
|
export declare const FILE_TASK_OPERATIONS: readonly ["files.taskStatus", "files.taskCancel"];
|
|
7
|
+
/** An executor that registers the portal entry serves both, and only with a policy `portal`. */
|
|
8
|
+
export declare const PORTAL_OPERATIONS: readonly ["portal.register", "portal.unregister"];
|
|
6
9
|
export interface DelegateCapability {
|
|
7
10
|
operations: string[];
|
|
8
11
|
scope: string;
|
|
@@ -21,6 +24,10 @@ export interface ExecutorPolicy {
|
|
|
21
24
|
lifetime?: 'until-revoked';
|
|
22
25
|
package: string;
|
|
23
26
|
operations: string[];
|
|
27
|
+
/** Below DSM 7.2 the executor registers this Application Portal entry; DSM 7.1 refuses web-config for unsigned packages. */
|
|
28
|
+
portal?: PortalEntry;
|
|
29
|
+
/** The root op the executor runs at uninstall while the approval is live; a key of root. */
|
|
30
|
+
purge?: string;
|
|
24
31
|
root: Record<string, string>;
|
|
25
32
|
}
|
|
26
33
|
/** The exact policy file bytes whose sha256 is the policy pin. */
|
|
@@ -34,6 +41,14 @@ export declare function executorPolicy(m: {
|
|
|
34
41
|
rootOps?: true;
|
|
35
42
|
};
|
|
36
43
|
rootOps?: RootOp[];
|
|
44
|
+
portal?: PortalDeclaration;
|
|
45
|
+
os_min_ver?: string;
|
|
46
|
+
displayname?: string;
|
|
47
|
+
uninstall?: {
|
|
48
|
+
purge: {
|
|
49
|
+
root?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
37
52
|
}): ExecutorPolicy;
|
|
38
53
|
/** Inventory rejects malformed extensions even if a caller recomputes every pin. */
|
|
39
54
|
export declare function validExecutorPolicy(value: unknown, pkg: string): value is ExecutorPolicy;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { MOVE_FROM_STAGING, DELEGATE_EXTENSION_OPERATIONS } from './delegate-contract.js';
|
|
3
|
+
import { portalByExecutor } from '../build-spk/dsm-version.js';
|
|
4
|
+
import { portalEntryProblem } from '../portal/alias.js';
|
|
3
5
|
export const FILE_OPERATIONS = ['files.list', 'files.info', 'files.createFolder', 'files.checkPermission', 'files.write', 'files.read', 'files.rename', 'files.copy', 'files.move', 'files.delete'];
|
|
4
6
|
/** Started File Station tasks are followed with these; they are implied, never declared. */
|
|
5
7
|
export const FILE_TASK_OPERATIONS = ['files.taskStatus', 'files.taskCancel'];
|
|
6
8
|
const TASK_STARTING_OPERATIONS = ['files.copy', 'files.move', 'files.delete'];
|
|
9
|
+
/** An executor that registers the portal entry serves both, and only with a policy `portal`. */
|
|
10
|
+
export const PORTAL_OPERATIONS = ['portal.register', 'portal.unregister'];
|
|
7
11
|
export function validDelegate(value) {
|
|
8
12
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
9
13
|
return false;
|
|
@@ -24,7 +28,8 @@ export const executorPolicyText = (policy) => JSON.stringify(policy) + '\n';
|
|
|
24
28
|
export function executorPolicy(m) {
|
|
25
29
|
if (m.capabilities.delegate && !validDelegate(m.capabilities.delegate))
|
|
26
30
|
throw new Error('[EXECUTOR_POLICY] Invalid delegate operations or scope.');
|
|
27
|
-
const
|
|
31
|
+
const portal = executorPortal(m);
|
|
32
|
+
const operations = ['status', ...(portal ? PORTAL_OPERATIONS : [])];
|
|
28
33
|
if (m.capabilities.delegate) {
|
|
29
34
|
operations.push('delegate.create', 'delegate.list', 'delegate.revoke', ...m.capabilities.delegate.operations);
|
|
30
35
|
if (m.capabilities.delegate.operations.some(op => TASK_STARTING_OPERATIONS.includes(op)))
|
|
@@ -39,23 +44,37 @@ export function executorPolicy(m) {
|
|
|
39
44
|
root[op.name] = sha256(op.body);
|
|
40
45
|
}
|
|
41
46
|
const d = m.capabilities.delegate;
|
|
47
|
+
const purge = m.capabilities.rootOps ? m.uninstall?.purge.root : undefined;
|
|
42
48
|
return { v: 1, package: m.package, app: `SYNO.SDS.${m.package}.Instance`, ...(m.entry ? { entry: m.entry } : {}), ...(d ? { scope: d.scope } : {}),
|
|
43
49
|
...(d?.lifetime ? { destinations: [...d.destinations].sort(), lifetime: d.lifetime } : {}),
|
|
44
|
-
operations: [...new Set(operations)].sort(), root };
|
|
50
|
+
operations: [...new Set(operations)].sort(), ...(portal ? { portal } : {}), ...(purge ? { purge } : {}), root };
|
|
51
|
+
}
|
|
52
|
+
/** The entry the executor registers: a portal App whose os_min_ver is below DSM 7.2. */
|
|
53
|
+
function executorPortal(m) {
|
|
54
|
+
if (!m.portal || !m.os_min_ver || !portalByExecutor(m.os_min_ver))
|
|
55
|
+
return undefined;
|
|
56
|
+
const entry = { application: m.displayname ?? '', alias: m.portal.alias };
|
|
57
|
+
const problem = portalEntryProblem(m.package, entry);
|
|
58
|
+
if (problem)
|
|
59
|
+
throw new Error(`[EXECUTOR_POLICY] ${problem}`);
|
|
60
|
+
return entry;
|
|
45
61
|
}
|
|
46
62
|
/** Inventory rejects malformed extensions even if a caller recomputes every pin. */
|
|
47
63
|
export function validExecutorPolicy(value, pkg) {
|
|
48
64
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
49
65
|
return false;
|
|
50
66
|
const p = value;
|
|
51
|
-
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'root'].includes(key))
|
|
67
|
+
if (Object.keys(p).some(key => !['v', 'package', 'app', 'entry', 'scope', 'destinations', 'lifetime', 'operations', 'portal', 'purge', 'root'].includes(key))
|
|
52
68
|
|| p.v !== 1 || p.package !== pkg || p.app !== `SYNO.SDS.${pkg}.Instance`
|
|
53
69
|
|| 'entry' in p && p.entry !== 'admin-only' && p.entry !== 'app-privilege'
|
|
54
70
|
|| !Array.isArray(p.operations) || !p.operations.includes('status') || new Set(p.operations).size !== p.operations.length
|
|
55
|
-
|| !p.root || typeof p.root !== 'object' || Array.isArray(p.root)
|
|
71
|
+
|| !p.root || typeof p.root !== 'object' || Array.isArray(p.root)
|
|
72
|
+
|| 'purge' in p && (typeof p.purge !== 'string' || !Object.hasOwn(p.root, p.purge)))
|
|
56
73
|
return false;
|
|
57
74
|
const base = ['status', 'root.run', 'delegate.create', 'delegate.list', 'delegate.revoke'];
|
|
58
|
-
if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS].includes(op)))
|
|
75
|
+
if (p.operations.some(op => ![...base, ...FILE_OPERATIONS, ...FILE_TASK_OPERATIONS, ...DELEGATE_EXTENSION_OPERATIONS, ...PORTAL_OPERATIONS].includes(op)))
|
|
76
|
+
return false;
|
|
77
|
+
if (!PORTAL_OPERATIONS.every(op => p.operations.includes(op) === (p.portal !== undefined)) || p.portal !== undefined && !validPortalEntry(p.portal, pkg))
|
|
59
78
|
return false;
|
|
60
79
|
const delegated = p.operations.some(op => op.startsWith('files.') || op.startsWith('delegate.'));
|
|
61
80
|
if (!delegated)
|
|
@@ -65,5 +84,12 @@ export function validExecutorPolicy(value, pkg) {
|
|
|
65
84
|
if (!validDelegate(capability))
|
|
66
85
|
return false;
|
|
67
86
|
const expected = executorPolicy({ package: pkg, capabilities: { delegate: capability, ...(p.operations.includes('root.run') ? { rootOps: true } : {}) } }).operations;
|
|
68
|
-
|
|
87
|
+
const declared = p.operations.filter(op => !PORTAL_OPERATIONS.includes(op));
|
|
88
|
+
return expected.length === declared.length && expected.every(op => declared.includes(op));
|
|
89
|
+
}
|
|
90
|
+
function validPortalEntry(value, pkg) {
|
|
91
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => key !== 'application' && key !== 'alias'))
|
|
92
|
+
return false;
|
|
93
|
+
const { application, alias } = value;
|
|
94
|
+
return typeof application === 'string' && typeof alias === 'string' && portalEntryProblem(pkg, { application, alias }) === undefined;
|
|
69
95
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const SDK_PACKAGE = "@synopackageland/sdk";
|
|
2
2
|
export declare const SDK_MODULE = "github.com/synopackageland/syno-pkg-land-sdk";
|
|
3
3
|
export declare const EXECUTOR_MODULE = "github.com/synopackageland/syno-pkg-land-sdk/executor";
|
|
4
|
+
/** The Rust SDK crate, a git dependency on a release tag of the SDK repository. */
|
|
5
|
+
export declare const SDK_CRATE = "syno-pkg-land-sdk";
|
|
6
|
+
export declare const SDK_GIT_URL = "https://github.com/synopackageland/syno-pkg-land-sdk";
|
|
4
7
|
/** Where one SDK version's Go client and executor sources are read from. */
|
|
5
8
|
export interface SdkSource {
|
|
6
9
|
version: string;
|
|
@@ -8,7 +11,16 @@ export interface SdkSource {
|
|
|
8
11
|
executorDir: string;
|
|
9
12
|
local: boolean;
|
|
10
13
|
}
|
|
11
|
-
/**
|
|
14
|
+
/** How each backend declares the SDK. */
|
|
15
|
+
export declare const SDK_DEPENDENCY: Record<SdkBackend['kind'], string>;
|
|
16
|
+
/** The App backend whose dependency files name its SDK version. */
|
|
17
|
+
export interface SdkBackend {
|
|
18
|
+
kind: 'go' | 'rust';
|
|
19
|
+
dir: string;
|
|
20
|
+
}
|
|
21
|
+
/** The SDK version an App requires, read from its backend's dependency files. */
|
|
22
|
+
export declare function appSdkVersion(backend: SdkBackend): string | undefined;
|
|
23
|
+
/** The SDK version a Go backend requires: `require github.com/synopackageland/syno-pkg-land-sdk vX.Y.Z` in backend/go.mod, its only version source. */
|
|
12
24
|
export declare function sdkRequirement(backendDir: string): string | undefined;
|
|
13
25
|
/** The App's own Go module path, the prefix of every `-X` that targets its packages. */
|
|
14
26
|
export declare function goModulePath(backendDir: string): string | undefined;
|
|
@@ -21,7 +33,7 @@ export declare function sdkSource(version: string, env?: NodeJS.ProcessEnv, down
|
|
|
21
33
|
/** The module cache directory of module@version; the cache is read-only, so builds copy it first. */
|
|
22
34
|
export declare function goModDownload(spec: string, env?: NodeJS.ProcessEnv): string;
|
|
23
35
|
/** The SDK an App builds its executor with; delegate and rootOps cannot build without one. */
|
|
24
|
-
export declare function requiredSdk(
|
|
36
|
+
export declare function requiredSdk(backend: SdkBackend, env?: NodeJS.ProcessEnv, download?: (spec: string) => string): SdkSource;
|
|
25
37
|
/**
|
|
26
38
|
* The SDK repositories are private: Go must fetch them with git, not from
|
|
27
39
|
* proxy.golang.org and sum.golang.org. Says which of the two is missing.
|
|
@@ -5,7 +5,42 @@ import { join } from 'node:path';
|
|
|
5
5
|
export const SDK_PACKAGE = '@synopackageland/sdk';
|
|
6
6
|
export const SDK_MODULE = 'github.com/synopackageland/syno-pkg-land-sdk';
|
|
7
7
|
export const EXECUTOR_MODULE = `${SDK_MODULE}/executor`;
|
|
8
|
-
/** The SDK
|
|
8
|
+
/** The Rust SDK crate, a git dependency on a release tag of the SDK repository. */
|
|
9
|
+
export const SDK_CRATE = 'syno-pkg-land-sdk';
|
|
10
|
+
export const SDK_GIT_URL = `https://${SDK_MODULE}`;
|
|
11
|
+
/** How each backend declares the SDK. */
|
|
12
|
+
export const SDK_DEPENDENCY = {
|
|
13
|
+
go: `add \`require ${SDK_MODULE} v<version>\` to backend/go.mod.`,
|
|
14
|
+
rust: `add \`${SDK_CRATE} = { git = "${SDK_GIT_URL}", tag = "v<version>" }\` to backend/Cargo.toml and commit Cargo.lock.`,
|
|
15
|
+
};
|
|
16
|
+
/** The SDK version an App requires, read from its backend's dependency files. */
|
|
17
|
+
export function appSdkVersion(backend) {
|
|
18
|
+
return backend.kind === 'go' ? sdkRequirement(backend.dir) : cargoSdkRequirement(backend.dir);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The SDK release a Rust backend's Cargo.lock pins: the source
|
|
22
|
+
* `git+https://github.com/synopackageland/syno-pkg-land-sdk?tag=vX.Y.Z#<commit>`.
|
|
23
|
+
* A branch, revision, path or registry source is refused, so the executor and
|
|
24
|
+
* the SDK route policy always come from the same release as the crate.
|
|
25
|
+
*/
|
|
26
|
+
function cargoSdkRequirement(backendDir) {
|
|
27
|
+
let lock;
|
|
28
|
+
try {
|
|
29
|
+
lock = readFileSync(join(backendDir, 'Cargo.lock'), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const sdk = lock.split(/^\[\[package\]\]$/m).filter(entry => /^name = "(.*)"$/m.exec(entry)?.[1] === SDK_CRATE);
|
|
35
|
+
if (!sdk.length)
|
|
36
|
+
return undefined;
|
|
37
|
+
const tag = new RegExp(`^source = "git\\+${SDK_GIT_URL.replaceAll('.', '\\.')}\\?tag=v(\\d+\\.\\d+\\.\\d+)#[0-9a-f]{40}"$`, 'm');
|
|
38
|
+
const version = sdk.length === 1 ? tag.exec(sdk[0])?.[1] : undefined;
|
|
39
|
+
if (!version)
|
|
40
|
+
throw new Error(`[SDK_TAG_REQUIRED] backend/Cargo.lock must pin one ${SDK_CRATE} from a release tag: { git = "${SDK_GIT_URL}", tag = "vX.Y.Z" } in Cargo.toml, then cargo update -p ${SDK_CRATE}.`);
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
43
|
+
/** The SDK version a Go backend requires: `require github.com/synopackageland/syno-pkg-land-sdk vX.Y.Z` in backend/go.mod, its only version source. */
|
|
9
44
|
export function sdkRequirement(backendDir) {
|
|
10
45
|
const module = SDK_MODULE.replaceAll('.', '\\.');
|
|
11
46
|
return new RegExp(`^\\s*(?:require\\s+)?${module}\\s+v(\\S+)`, 'm').exec(goMod(backendDir))?.[1];
|
|
@@ -54,10 +89,10 @@ export function goModDownload(spec, env = process.env) {
|
|
|
54
89
|
return info.Dir;
|
|
55
90
|
}
|
|
56
91
|
/** The SDK an App builds its executor with; delegate and rootOps cannot build without one. */
|
|
57
|
-
export function requiredSdk(
|
|
58
|
-
const version =
|
|
92
|
+
export function requiredSdk(backend, env = process.env, download) {
|
|
93
|
+
const version = appSdkVersion(backend);
|
|
59
94
|
if (!version)
|
|
60
|
-
throw new Error(`[SDK_REQUIRED] capabilities.delegate and rootOps need the SDK:
|
|
95
|
+
throw new Error(`[SDK_REQUIRED] capabilities.delegate and rootOps need the SDK: ${SDK_DEPENDENCY[backend.kind]}`);
|
|
61
96
|
return download ? sdkSource(version, env, download) : sdkSource(version, env);
|
|
62
97
|
}
|
|
63
98
|
/**
|
|
@@ -9,5 +9,9 @@ export interface UninstallCleanupOptions {
|
|
|
9
9
|
executor?: boolean;
|
|
10
10
|
/** The App declares a Login Portal alias; only the executor tier can. */
|
|
11
11
|
portal?: boolean;
|
|
12
|
+
/** Below DSM 7.2 the executor registered the App's Application Portal entry. */
|
|
13
|
+
portalEntry?: boolean;
|
|
12
14
|
}
|
|
13
|
-
|
|
15
|
+
/** The root commands that remove an Application Portal entry the executor could not unregister. */
|
|
16
|
+
export declare function portalEntryCleanupCommands(pkg: string): string[];
|
|
17
|
+
export declare function uninstallCleanup(pkg: string, { executor, portal, portalEntry }?: UninstallCleanupOptions): UninstallCleanupItem[];
|