@synopackageland/cli 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +57 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/port-preflight.d.ts +21 -0
- package/dist/deploy/port-preflight.js +42 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -857
- package/dist/host/dsm-client.js +23 -263
- package/dist/host/dsm-port-profile.d.ts +47 -0
- package/dist/host/dsm-port-profile.js +206 -0
- package/dist/host/port-info-lookup.d.ts +9 -0
- package/dist/host/port-info-lookup.js +87 -0
- package/dist/host/port-info.d.ts +33 -0
- package/dist/host/port-info.js +66 -0
- package/dist/host/types.d.ts +6 -78
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -721
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -41
- package/dist/host/fake-nas.js +0 -174
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const ROOT_OPS_DIR = "root";
|
|
4
|
+
export const ROOT_OP_END = "SYNOPKGLAND_ROOT_OP_END";
|
|
5
|
+
const MAX_ROOT_OP_BYTES = 16 * 1024;
|
|
6
|
+
const OP_FILE = /^([a-z][a-z0-9_]{0,31})\.sh$/;
|
|
7
|
+
export function rootTaskName(packageId, op) {
|
|
8
|
+
return `${packageId} root: ${op}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The fixed start of an operation's task text. It does not carry the version,
|
|
12
|
+
* so the window can tell its own outdated task from someone else's.
|
|
13
|
+
*/
|
|
14
|
+
export function rootTaskHeader(packageId, op) {
|
|
15
|
+
return `#!/bin/sh\n# synopkgland root operation "${op}" of ${packageId} `;
|
|
16
|
+
}
|
|
17
|
+
/** Root-owned tmpfs folder the tasks write to; the package account can only read it. */
|
|
18
|
+
export function rootOutputDir(packageId) {
|
|
19
|
+
return `/run/synopkgland-${packageId}`;
|
|
20
|
+
}
|
|
21
|
+
export function loadRootOps(dir) {
|
|
22
|
+
const names = existsSync(dir) ? readdirSync(dir).sort() : [];
|
|
23
|
+
if (names.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
errors: [{ code: "STANDALONE_ROOT_OPS_MISSING", message: `privilege: root 需要 ${ROOT_OPS_DIR}/<名稱>.sh,至少一個 root 操作。` }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const errors = [];
|
|
30
|
+
const ops = [];
|
|
31
|
+
for (const file of names) {
|
|
32
|
+
const op = readRootOp(join(dir, file), file);
|
|
33
|
+
if ("error" in op) {
|
|
34
|
+
errors.push({ code: "STANDALONE_ROOT_OP_INVALID", message: `${ROOT_OPS_DIR}/${file}:${op.error}` });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
ops.push(op);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return errors.length === 0 ? { ok: true, ops } : { ok: false, errors };
|
|
41
|
+
}
|
|
42
|
+
function readRootOp(path, file) {
|
|
43
|
+
const name = OP_FILE.exec(file)?.[1];
|
|
44
|
+
if (!name) {
|
|
45
|
+
return { error: "檔名必須是小寫英數與底線組成的 <名稱>.sh(最多 32 字元),此資料夾不放其他檔案。" };
|
|
46
|
+
}
|
|
47
|
+
const stat = lstatSync(path);
|
|
48
|
+
if (!stat.isFile()) {
|
|
49
|
+
return { error: "必須是一般檔案(不可是 symlink 或資料夾)。" };
|
|
50
|
+
}
|
|
51
|
+
if (stat.size > MAX_ROOT_OP_BYTES) {
|
|
52
|
+
return { error: `不可超過 ${MAX_ROOT_OP_BYTES} bytes。` };
|
|
53
|
+
}
|
|
54
|
+
const body = readFileSync(path, "utf8");
|
|
55
|
+
if (!body.startsWith("#!/bin/sh\n") || body.includes("\0") || body.includes("�")) {
|
|
56
|
+
return { error: "必須是以 #!/bin/sh 開頭的 UTF-8 文字 script。" };
|
|
57
|
+
}
|
|
58
|
+
if (body.split("\n").includes(ROOT_OP_END)) {
|
|
59
|
+
return { error: `不可包含單獨一行的 ${ROOT_OP_END}。` };
|
|
60
|
+
}
|
|
61
|
+
return { name, body };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The exact text of the DSM task. The body runs from a quoted here-document
|
|
65
|
+
* (no expansion) and its stdout replaces `<op>.out` in the root-owned run
|
|
66
|
+
* folder. The package version is part of the text, so every upgrade changes
|
|
67
|
+
* what the administrator approved and needs approval again.
|
|
68
|
+
*/
|
|
69
|
+
export function renderRootTaskScript(manifest, op) {
|
|
70
|
+
const body = op.body.endsWith("\n") ? op.body : `${op.body}\n`;
|
|
71
|
+
return `${rootTaskHeader(manifest.package, op.name)}${manifest.spkVersion}.
|
|
72
|
+
# Approved by a DSM administrator; runs only when this task is run.
|
|
73
|
+
set -eu
|
|
74
|
+
umask 022
|
|
75
|
+
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
76
|
+
export PATH
|
|
77
|
+
dir=${rootOutputDir(manifest.package)}
|
|
78
|
+
${op.protectedOutput ? `[ "$(id -u)" = 0 ]
|
|
79
|
+
[ ! -L /run ] && [ "$(stat -c %u /run)" = 0 ]
|
|
80
|
+
[ ! -L "$dir" ]
|
|
81
|
+
if [ ! -e "$dir" ]; then mkdir -m 0755 "$dir"; fi
|
|
82
|
+
[ -d "$dir" ] && [ "$(stat -c %u "$dir")" = 0 ]
|
|
83
|
+
output_mode=$(stat -c %a "$dir")
|
|
84
|
+
[ "$((0$output_mode & 0022))" = 0 ]
|
|
85
|
+
` : `if [ -L "$dir" ] || [ ! -d "$dir" ]; then
|
|
86
|
+
rm -f "$dir"
|
|
87
|
+
mkdir -m 0755 "$dir"
|
|
88
|
+
fi
|
|
89
|
+
`}tmp=$(mktemp "$dir/.${op.name}.XXXXXX")
|
|
90
|
+
if /bin/sh -eu >"$tmp" <<'${ROOT_OP_END}'
|
|
91
|
+
${body}${ROOT_OP_END}
|
|
92
|
+
then
|
|
93
|
+
chmod 0644 "$tmp"
|
|
94
|
+
mv -f "$tmp" "$dir/${op.name}.out"
|
|
95
|
+
else
|
|
96
|
+
status=$?
|
|
97
|
+
rm -f "$tmp"
|
|
98
|
+
exit "$status"
|
|
99
|
+
fi
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type RootOp } from "./root-ops.js";
|
|
2
|
+
/** What the window embeds per root operation: the task it may create, and how to recognize it. */
|
|
3
|
+
export interface RootTask {
|
|
4
|
+
op: string;
|
|
5
|
+
name: string;
|
|
6
|
+
header: string;
|
|
7
|
+
script: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function rootTasks(manifest: {
|
|
10
|
+
package: string;
|
|
11
|
+
spkVersion: string;
|
|
12
|
+
rootOps?: RootOp[];
|
|
13
|
+
}): RootTask[];
|
|
14
|
+
/**
|
|
15
|
+
* `createRootAccess(options)` → `{ state, authorize, run, remove }`, plain
|
|
16
|
+
* ES5 for DSM's desktop. Every DSM call goes through `options.send` (the
|
|
17
|
+
* AppWindow's sendWebAPI) as the signed-in administrator:
|
|
18
|
+
*
|
|
19
|
+
* - authorize: DSM's own password confirmation, then one disabled root task
|
|
20
|
+
* with exactly the embedded script. The one-use token goes into that single
|
|
21
|
+
* create call and is never kept.
|
|
22
|
+
* - run: re-reads the task and runs it only if it is still exactly ours, then
|
|
23
|
+
* waits for its history entry and asks the backend for the output.
|
|
24
|
+
* - an older version of our task is "outdated" and never runs; authorizing
|
|
25
|
+
* again replaces it. A same-named task that is not ours is a "conflict" and
|
|
26
|
+
* is never run, replaced or deleted.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ROOT_RUNTIME = "function createRootAccess(options) {\n var API = \"SYNO.Core.TaskScheduler\";\n var POLL_MS = 500, POLLS = 60;\n var byOp = {};\n options.tasks.forEach(function (task) { byOp[task.op] = task; });\n\n function fail(code) {\n var error = new Error(code);\n error.code = code;\n return error;\n }\n function begin(op) {\n if (!Object.prototype.hasOwnProperty.call(byOp, op)) return Promise.reject(fail(\"root_unknown_op\"));\n if (options.user().admin !== true) return Promise.reject(fail(\"admin_required\"));\n return Promise.resolve(byOp[op]);\n }\n function isRootTask(summary) {\n return summary.owner === \"root\" && summary.real_owner === \"root\" && summary.enable === false && summary.type === \"script\";\n }\n function find(task) {\n return options.send(API, \"list\", 3, { start: 0, limit: 500 }).then(function (data) {\n if (!data || !Array.isArray(data.tasks)) throw fail(\"root_bad_response\");\n var named = data.tasks.filter(function (summary) { return summary && summary.name === task.name; });\n if (named.length === 0) return { state: \"missing\" };\n if (named.length > 1 || !isRootTask(named[0])) return { state: \"conflict\" };\n var ref = { id: named[0].id, real_owner: named[0].real_owner };\n return options.send(API, \"get\", 4, ref).then(function (detail) {\n var script = detail && detail.extra && detail.extra.script;\n if (!detail || String(detail.id) !== String(ref.id) || detail.name !== task.name\n || detail.owner !== \"root\" || detail.enable !== false || detail.type !== \"script\" || typeof script !== \"string\") {\n return { state: \"conflict\" };\n }\n if (script === task.script) return { state: \"ready\", ref: ref };\n if (script.indexOf(task.header) === 0) return { state: \"outdated\", ref: ref };\n return { state: \"conflict\" };\n });\n });\n }\n function definition(task, user) {\n var hours = [];\n for (var h = 1; h <= 23; h++) hours.push(h);\n return {\n name: task.name,\n real_owner: user,\n owner: \"root\",\n enable: false,\n type: \"script\",\n schedule: {\n date_type: 0, week_day: \"0,1,2,3,4,5,6\", repeat_date: 1001, monthly_week: [], hour: 0, minute: 0,\n repeat_hour: 0, repeat_min: 0, last_work_hour: 0,\n repeat_min_store_config: [1, 5, 10, 15, 20, 30], repeat_hour_store_config: hours\n },\n extra: { notify_enable: false, script: task.script, notify_mail: \"\", notify_if_error: false }\n };\n }\n function newest(history) {\n if (!Array.isArray(history)) throw fail(\"root_bad_response\");\n return history.reduce(function (latest, entry) {\n return entry && (!latest || Number(entry.timestamp) > Number(latest.timestamp)) ? entry : latest;\n }, null);\n }\n function history(ref) {\n return options.send(API, \"get_history_status_list\", 1, { id: ref.id }).then(newest);\n }\n function waitForRun(ref, before, polls) {\n return history(ref).then(function (latest) {\n var finished = latest && (!before || latest.timestamp !== before.timestamp)\n && latest.exit_type !== \"running\" && latest.stop_time;\n if (finished) {\n if (latest.exit_code !== 0) throw fail(\"root_task_failed\");\n return;\n }\n if (polls <= 1) throw fail(\"root_timeout\");\n return options.wait(POLL_MS).then(function () { return waitForRun(ref, before, polls - 1); });\n });\n }\n\n return {\n state: function (op) {\n return begin(op).then(find).then(function (found) { return found.state; });\n },\n authorize: function (op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state === \"ready\") return \"ready\";\n if (found.state === \"conflict\") throw fail(\"root_conflict\");\n return options.confirmPassword().then(function (token) {\n if (!token) return \"cancelled\";\n var params = definition(task, options.user().name);\n params.SynoConfirmPWToken = token;\n var replaced = found.state === \"outdated\"\n ? options.send(API, \"delete\", 2, { tasks: [found.ref] })\n : Promise.resolve();\n return replaced.then(function () {\n return options.send(API + \".Root\", \"create\", 4, params);\n }).then(function () {\n return find(task);\n }).then(function (after) {\n if (after.state !== \"ready\") throw fail(\"root_verify_failed\");\n return \"ready\";\n });\n });\n });\n });\n },\n run: function (op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state !== \"ready\") throw fail(\"root_not_ready\");\n return history(found.ref).then(function (before) {\n return options.send(API, \"run\", 2, { tasks: [found.ref] }).then(function () {\n return waitForRun(found.ref, before, POLLS);\n });\n });\n }).then(function () {\n return options.call(\"root.output\", { op: op });\n });\n });\n },\n remove: function (op) {\n return begin(op).then(function (task) {\n return find(task).then(function (found) {\n if (found.state === \"missing\") return \"missing\";\n if (found.state === \"conflict\") throw fail(\"root_conflict\");\n return options.send(API, \"delete\", 2, { tasks: [found.ref] }).then(function () {\n return find(task);\n }).then(function (after) {\n if (after.state !== \"missing\") throw fail(\"root_verify_failed\");\n return \"missing\";\n });\n });\n });\n },\n output: function (op) {\n return begin(op).then(function () {\n return options.call(\"root.output\", { op: op });\n });\n }\n };\n}";
|
|
29
|
+
/**
|
|
30
|
+
* The authorization panel the window shows above the app for each root
|
|
31
|
+
* operation, identical in every package. `rootPanelView(model)` decides what
|
|
32
|
+
* it shows; `mountRootPanel(host, access, i18n, tasks, admin)` draws it with
|
|
33
|
+
* DOM text nodes only (no HTML from data).
|
|
34
|
+
*/
|
|
35
|
+
export declare const ROOT_PANEL = "function rootPanelShowsResult(state) {\n return state === \"ready\" || state === \"outdated\";\n}\n\nfunction rootPanelView(model) {\n if (!model.admin) return { status: null, buttons: [], message: { key: \"admin_only\" } };\n function buttons(actions) {\n return actions.map(function (action) { return { action: action, disabled: model.busy === true }; });\n }\n if (model.error) {\n return {\n status: \"state.\" + model.state, buttons: buttons([\"retry\"]),\n message: { key: \"error.\" + model.error, params: { name: model.name, code: model.error } }\n };\n }\n var message = model.notice ? { key: model.notice } : null;\n switch (model.state) {\n case \"missing\":\n return { status: \"state.missing\", buttons: buttons([\"authorize\"]), message: message };\n case \"outdated\":\n return { status: \"state.outdated\", buttons: buttons([\"authorize\", \"remove\"]), message: message };\n case \"ready\":\n return { status: \"state.ready\", buttons: buttons([\"run\", \"remove\"]), message: message };\n case \"conflict\":\n return { status: \"state.conflict\", buttons: buttons([\"retry\"]), message: { key: \"error.root_conflict\", params: { name: model.name } } };\n default:\n return { status: \"state.checking\", buttons: [], message: null };\n }\n}\n\nfunction mountRootPanel(host, access, i18n, tasks, admin) {\n var t = i18n.t;\n function el(tag, className, text) {\n var node = document.createElement(tag);\n if (className) node.className = className;\n if (text !== undefined) node.textContent = text;\n return node;\n }\n function say(message) {\n if (!message) return \"\";\n var text = t(message.key, message.params);\n return text === message.key && message.key.indexOf(\"error.\") === 0 ? t(\"error.default\", message.params) : text;\n }\n tasks.forEach(function (task) {\n var model = { admin: admin, name: task.name, state: \"checking\", busy: false, error: null, notice: null };\n var result = null;\n var section = el(\"section\", \"dsmapp-rootpanel\");\n host.appendChild(section);\n\n function render() {\n var view = rootPanelView(model);\n while (section.firstChild) section.removeChild(section.firstChild);\n section.appendChild(el(\"h2\", null, t(\"title\")));\n section.appendChild(el(\"p\", null, t(\"intro\", { op: task.op })));\n if (view.status) {\n var facts = el(\"dl\");\n facts.appendChild(el(\"dt\", null, t(\"status\")));\n facts.appendChild(el(\"dd\", \"dsmapp-rootpanel-state\", t(view.status, { name: task.name })));\n facts.appendChild(el(\"dt\", null, t(\"last_run\")));\n facts.appendChild(el(\"dd\", null, result && result.modified\n ? new Date(result.modified * 1000).toLocaleString(i18n.locale) : t(\"never\")));\n section.appendChild(facts);\n }\n if (view.message) section.appendChild(el(\"p\", \"dsmapp-rootpanel-message\", say(view.message)));\n if (result && typeof result.output === \"string\") {\n section.appendChild(el(\"div\", \"dsmapp-rootpanel-label\", t(\"output\")));\n section.appendChild(el(\"pre\", \"dsmapp-rootpanel-output\", result.output));\n }\n var actions = el(\"div\", \"dsmapp-rootpanel-actions\");\n view.buttons.forEach(function (button) {\n var node = el(\"button\", button.action === \"remove\" ? null : \"dsmapp-rootpanel-primary\", t(button.action));\n node.type = \"button\";\n node.disabled = button.disabled;\n node.onclick = function () { act(button.action); };\n actions.appendChild(node);\n });\n section.appendChild(actions);\n if (admin && rootPanelShowsResult(model.state)) section.appendChild(el(\"p\", \"dsmapp-rootpanel-hint\", t(\"uninstall_hint\")));\n }\n function settle(promise, notice) {\n model.busy = true;\n model.error = null;\n model.notice = null;\n render();\n return promise.then(function (value) {\n model.notice = typeof notice === \"function\" ? notice(value) : notice;\n }, function (error) {\n model.error = (error && error.code) || \"unknown\";\n }).then(function () {\n model.busy = false;\n render();\n });\n }\n function refresh(notice) {\n return settle(access.state(task.op).then(function (state) {\n model.state = state;\n if (!rootPanelShowsResult(state)) return;\n return access.output(task.op).then(function (out) { result = out; });\n }), notice);\n }\n function refreshAfterChange() {\n return model.error ? null : refresh(model.notice);\n }\n function act(action) {\n if (action === \"retry\") return refresh(null);\n if (action === \"authorize\") {\n return settle(access.authorize(task.op), function (outcome) {\n return outcome === \"cancelled\" ? \"cancelled\" : null;\n }).then(refreshAfterChange);\n }\n if (action === \"run\") {\n return settle(access.run(task.op).then(function (out) { result = out; }), \"completed\");\n }\n if (action === \"remove\") {\n return settle(access.remove(task.op).then(function () { result = null; }), null).then(refreshAfterChange);\n }\n }\n render();\n if (admin) refresh(null);\n });\n}";
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { renderRootTaskScript, rootTaskHeader, rootTaskName } from "./root-ops.js";
|
|
2
|
+
export function rootTasks(manifest) {
|
|
3
|
+
return (manifest.rootOps ?? []).map((op) => ({
|
|
4
|
+
op: op.name,
|
|
5
|
+
name: rootTaskName(manifest.package, op.name),
|
|
6
|
+
header: rootTaskHeader(manifest.package, op.name),
|
|
7
|
+
script: renderRootTaskScript(manifest, op),
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `createRootAccess(options)` → `{ state, authorize, run, remove }`, plain
|
|
12
|
+
* ES5 for DSM's desktop. Every DSM call goes through `options.send` (the
|
|
13
|
+
* AppWindow's sendWebAPI) as the signed-in administrator:
|
|
14
|
+
*
|
|
15
|
+
* - authorize: DSM's own password confirmation, then one disabled root task
|
|
16
|
+
* with exactly the embedded script. The one-use token goes into that single
|
|
17
|
+
* create call and is never kept.
|
|
18
|
+
* - run: re-reads the task and runs it only if it is still exactly ours, then
|
|
19
|
+
* waits for its history entry and asks the backend for the output.
|
|
20
|
+
* - an older version of our task is "outdated" and never runs; authorizing
|
|
21
|
+
* again replaces it. A same-named task that is not ours is a "conflict" and
|
|
22
|
+
* is never run, replaced or deleted.
|
|
23
|
+
*/
|
|
24
|
+
export const ROOT_RUNTIME = `function createRootAccess(options) {
|
|
25
|
+
var API = "SYNO.Core.TaskScheduler";
|
|
26
|
+
var POLL_MS = 500, POLLS = 60;
|
|
27
|
+
var byOp = {};
|
|
28
|
+
options.tasks.forEach(function (task) { byOp[task.op] = task; });
|
|
29
|
+
|
|
30
|
+
function fail(code) {
|
|
31
|
+
var error = new Error(code);
|
|
32
|
+
error.code = code;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
function begin(op) {
|
|
36
|
+
if (!Object.prototype.hasOwnProperty.call(byOp, op)) return Promise.reject(fail("root_unknown_op"));
|
|
37
|
+
if (options.user().admin !== true) return Promise.reject(fail("admin_required"));
|
|
38
|
+
return Promise.resolve(byOp[op]);
|
|
39
|
+
}
|
|
40
|
+
function isRootTask(summary) {
|
|
41
|
+
return summary.owner === "root" && summary.real_owner === "root" && summary.enable === false && summary.type === "script";
|
|
42
|
+
}
|
|
43
|
+
function find(task) {
|
|
44
|
+
return options.send(API, "list", 3, { start: 0, limit: 500 }).then(function (data) {
|
|
45
|
+
if (!data || !Array.isArray(data.tasks)) throw fail("root_bad_response");
|
|
46
|
+
var named = data.tasks.filter(function (summary) { return summary && summary.name === task.name; });
|
|
47
|
+
if (named.length === 0) return { state: "missing" };
|
|
48
|
+
if (named.length > 1 || !isRootTask(named[0])) return { state: "conflict" };
|
|
49
|
+
var ref = { id: named[0].id, real_owner: named[0].real_owner };
|
|
50
|
+
return options.send(API, "get", 4, ref).then(function (detail) {
|
|
51
|
+
var script = detail && detail.extra && detail.extra.script;
|
|
52
|
+
if (!detail || String(detail.id) !== String(ref.id) || detail.name !== task.name
|
|
53
|
+
|| detail.owner !== "root" || detail.enable !== false || detail.type !== "script" || typeof script !== "string") {
|
|
54
|
+
return { state: "conflict" };
|
|
55
|
+
}
|
|
56
|
+
if (script === task.script) return { state: "ready", ref: ref };
|
|
57
|
+
if (script.indexOf(task.header) === 0) return { state: "outdated", ref: ref };
|
|
58
|
+
return { state: "conflict" };
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function definition(task, user) {
|
|
63
|
+
var hours = [];
|
|
64
|
+
for (var h = 1; h <= 23; h++) hours.push(h);
|
|
65
|
+
return {
|
|
66
|
+
name: task.name,
|
|
67
|
+
real_owner: user,
|
|
68
|
+
owner: "root",
|
|
69
|
+
enable: false,
|
|
70
|
+
type: "script",
|
|
71
|
+
schedule: {
|
|
72
|
+
date_type: 0, week_day: "0,1,2,3,4,5,6", repeat_date: 1001, monthly_week: [], hour: 0, minute: 0,
|
|
73
|
+
repeat_hour: 0, repeat_min: 0, last_work_hour: 0,
|
|
74
|
+
repeat_min_store_config: [1, 5, 10, 15, 20, 30], repeat_hour_store_config: hours
|
|
75
|
+
},
|
|
76
|
+
extra: { notify_enable: false, script: task.script, notify_mail: "", notify_if_error: false }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function newest(history) {
|
|
80
|
+
if (!Array.isArray(history)) throw fail("root_bad_response");
|
|
81
|
+
return history.reduce(function (latest, entry) {
|
|
82
|
+
return entry && (!latest || Number(entry.timestamp) > Number(latest.timestamp)) ? entry : latest;
|
|
83
|
+
}, null);
|
|
84
|
+
}
|
|
85
|
+
function history(ref) {
|
|
86
|
+
return options.send(API, "get_history_status_list", 1, { id: ref.id }).then(newest);
|
|
87
|
+
}
|
|
88
|
+
function waitForRun(ref, before, polls) {
|
|
89
|
+
return history(ref).then(function (latest) {
|
|
90
|
+
var finished = latest && (!before || latest.timestamp !== before.timestamp)
|
|
91
|
+
&& latest.exit_type !== "running" && latest.stop_time;
|
|
92
|
+
if (finished) {
|
|
93
|
+
if (latest.exit_code !== 0) throw fail("root_task_failed");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (polls <= 1) throw fail("root_timeout");
|
|
97
|
+
return options.wait(POLL_MS).then(function () { return waitForRun(ref, before, polls - 1); });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
state: function (op) {
|
|
103
|
+
return begin(op).then(find).then(function (found) { return found.state; });
|
|
104
|
+
},
|
|
105
|
+
authorize: function (op) {
|
|
106
|
+
return begin(op).then(function (task) {
|
|
107
|
+
return find(task).then(function (found) {
|
|
108
|
+
if (found.state === "ready") return "ready";
|
|
109
|
+
if (found.state === "conflict") throw fail("root_conflict");
|
|
110
|
+
return options.confirmPassword().then(function (token) {
|
|
111
|
+
if (!token) return "cancelled";
|
|
112
|
+
var params = definition(task, options.user().name);
|
|
113
|
+
params.SynoConfirmPWToken = token;
|
|
114
|
+
var replaced = found.state === "outdated"
|
|
115
|
+
? options.send(API, "delete", 2, { tasks: [found.ref] })
|
|
116
|
+
: Promise.resolve();
|
|
117
|
+
return replaced.then(function () {
|
|
118
|
+
return options.send(API + ".Root", "create", 4, params);
|
|
119
|
+
}).then(function () {
|
|
120
|
+
return find(task);
|
|
121
|
+
}).then(function (after) {
|
|
122
|
+
if (after.state !== "ready") throw fail("root_verify_failed");
|
|
123
|
+
return "ready";
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
run: function (op) {
|
|
130
|
+
return begin(op).then(function (task) {
|
|
131
|
+
return find(task).then(function (found) {
|
|
132
|
+
if (found.state !== "ready") throw fail("root_not_ready");
|
|
133
|
+
return history(found.ref).then(function (before) {
|
|
134
|
+
return options.send(API, "run", 2, { tasks: [found.ref] }).then(function () {
|
|
135
|
+
return waitForRun(found.ref, before, POLLS);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}).then(function () {
|
|
139
|
+
return options.call("root.output", { op: op });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
remove: function (op) {
|
|
144
|
+
return begin(op).then(function (task) {
|
|
145
|
+
return find(task).then(function (found) {
|
|
146
|
+
if (found.state === "missing") return "missing";
|
|
147
|
+
if (found.state === "conflict") throw fail("root_conflict");
|
|
148
|
+
return options.send(API, "delete", 2, { tasks: [found.ref] }).then(function () {
|
|
149
|
+
return find(task);
|
|
150
|
+
}).then(function (after) {
|
|
151
|
+
if (after.state !== "missing") throw fail("root_verify_failed");
|
|
152
|
+
return "missing";
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
output: function (op) {
|
|
158
|
+
return begin(op).then(function () {
|
|
159
|
+
return options.call("root.output", { op: op });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}`;
|
|
164
|
+
/**
|
|
165
|
+
* The authorization panel the window shows above the app for each root
|
|
166
|
+
* operation, identical in every package. `rootPanelView(model)` decides what
|
|
167
|
+
* it shows; `mountRootPanel(host, access, i18n, tasks, admin)` draws it with
|
|
168
|
+
* DOM text nodes only (no HTML from data).
|
|
169
|
+
*/
|
|
170
|
+
export const ROOT_PANEL = `function rootPanelShowsResult(state) {
|
|
171
|
+
return state === "ready" || state === "outdated";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function rootPanelView(model) {
|
|
175
|
+
if (!model.admin) return { status: null, buttons: [], message: { key: "admin_only" } };
|
|
176
|
+
function buttons(actions) {
|
|
177
|
+
return actions.map(function (action) { return { action: action, disabled: model.busy === true }; });
|
|
178
|
+
}
|
|
179
|
+
if (model.error) {
|
|
180
|
+
return {
|
|
181
|
+
status: "state." + model.state, buttons: buttons(["retry"]),
|
|
182
|
+
message: { key: "error." + model.error, params: { name: model.name, code: model.error } }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
var message = model.notice ? { key: model.notice } : null;
|
|
186
|
+
switch (model.state) {
|
|
187
|
+
case "missing":
|
|
188
|
+
return { status: "state.missing", buttons: buttons(["authorize"]), message: message };
|
|
189
|
+
case "outdated":
|
|
190
|
+
return { status: "state.outdated", buttons: buttons(["authorize", "remove"]), message: message };
|
|
191
|
+
case "ready":
|
|
192
|
+
return { status: "state.ready", buttons: buttons(["run", "remove"]), message: message };
|
|
193
|
+
case "conflict":
|
|
194
|
+
return { status: "state.conflict", buttons: buttons(["retry"]), message: { key: "error.root_conflict", params: { name: model.name } } };
|
|
195
|
+
default:
|
|
196
|
+
return { status: "state.checking", buttons: [], message: null };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function mountRootPanel(host, access, i18n, tasks, admin) {
|
|
201
|
+
var t = i18n.t;
|
|
202
|
+
function el(tag, className, text) {
|
|
203
|
+
var node = document.createElement(tag);
|
|
204
|
+
if (className) node.className = className;
|
|
205
|
+
if (text !== undefined) node.textContent = text;
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
function say(message) {
|
|
209
|
+
if (!message) return "";
|
|
210
|
+
var text = t(message.key, message.params);
|
|
211
|
+
return text === message.key && message.key.indexOf("error.") === 0 ? t("error.default", message.params) : text;
|
|
212
|
+
}
|
|
213
|
+
tasks.forEach(function (task) {
|
|
214
|
+
var model = { admin: admin, name: task.name, state: "checking", busy: false, error: null, notice: null };
|
|
215
|
+
var result = null;
|
|
216
|
+
var section = el("section", "dsmapp-rootpanel");
|
|
217
|
+
host.appendChild(section);
|
|
218
|
+
|
|
219
|
+
function render() {
|
|
220
|
+
var view = rootPanelView(model);
|
|
221
|
+
while (section.firstChild) section.removeChild(section.firstChild);
|
|
222
|
+
section.appendChild(el("h2", null, t("title")));
|
|
223
|
+
section.appendChild(el("p", null, t("intro", { op: task.op })));
|
|
224
|
+
if (view.status) {
|
|
225
|
+
var facts = el("dl");
|
|
226
|
+
facts.appendChild(el("dt", null, t("status")));
|
|
227
|
+
facts.appendChild(el("dd", "dsmapp-rootpanel-state", t(view.status, { name: task.name })));
|
|
228
|
+
facts.appendChild(el("dt", null, t("last_run")));
|
|
229
|
+
facts.appendChild(el("dd", null, result && result.modified
|
|
230
|
+
? new Date(result.modified * 1000).toLocaleString(i18n.locale) : t("never")));
|
|
231
|
+
section.appendChild(facts);
|
|
232
|
+
}
|
|
233
|
+
if (view.message) section.appendChild(el("p", "dsmapp-rootpanel-message", say(view.message)));
|
|
234
|
+
if (result && typeof result.output === "string") {
|
|
235
|
+
section.appendChild(el("div", "dsmapp-rootpanel-label", t("output")));
|
|
236
|
+
section.appendChild(el("pre", "dsmapp-rootpanel-output", result.output));
|
|
237
|
+
}
|
|
238
|
+
var actions = el("div", "dsmapp-rootpanel-actions");
|
|
239
|
+
view.buttons.forEach(function (button) {
|
|
240
|
+
var node = el("button", button.action === "remove" ? null : "dsmapp-rootpanel-primary", t(button.action));
|
|
241
|
+
node.type = "button";
|
|
242
|
+
node.disabled = button.disabled;
|
|
243
|
+
node.onclick = function () { act(button.action); };
|
|
244
|
+
actions.appendChild(node);
|
|
245
|
+
});
|
|
246
|
+
section.appendChild(actions);
|
|
247
|
+
if (admin && rootPanelShowsResult(model.state)) section.appendChild(el("p", "dsmapp-rootpanel-hint", t("uninstall_hint")));
|
|
248
|
+
}
|
|
249
|
+
function settle(promise, notice) {
|
|
250
|
+
model.busy = true;
|
|
251
|
+
model.error = null;
|
|
252
|
+
model.notice = null;
|
|
253
|
+
render();
|
|
254
|
+
return promise.then(function (value) {
|
|
255
|
+
model.notice = typeof notice === "function" ? notice(value) : notice;
|
|
256
|
+
}, function (error) {
|
|
257
|
+
model.error = (error && error.code) || "unknown";
|
|
258
|
+
}).then(function () {
|
|
259
|
+
model.busy = false;
|
|
260
|
+
render();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function refresh(notice) {
|
|
264
|
+
return settle(access.state(task.op).then(function (state) {
|
|
265
|
+
model.state = state;
|
|
266
|
+
if (!rootPanelShowsResult(state)) return;
|
|
267
|
+
return access.output(task.op).then(function (out) { result = out; });
|
|
268
|
+
}), notice);
|
|
269
|
+
}
|
|
270
|
+
function refreshAfterChange() {
|
|
271
|
+
return model.error ? null : refresh(model.notice);
|
|
272
|
+
}
|
|
273
|
+
function act(action) {
|
|
274
|
+
if (action === "retry") return refresh(null);
|
|
275
|
+
if (action === "authorize") {
|
|
276
|
+
return settle(access.authorize(task.op), function (outcome) {
|
|
277
|
+
return outcome === "cancelled" ? "cancelled" : null;
|
|
278
|
+
}).then(refreshAfterChange);
|
|
279
|
+
}
|
|
280
|
+
if (action === "run") {
|
|
281
|
+
return settle(access.run(task.op).then(function (out) { result = out; }), "completed");
|
|
282
|
+
}
|
|
283
|
+
if (action === "remove") {
|
|
284
|
+
return settle(access.remove(task.op).then(function () { result = null; }), null).then(refreshAfterChange);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
render();
|
|
288
|
+
if (admin) refresh(null);
|
|
289
|
+
});
|
|
290
|
+
}`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import type { StandaloneManifest } from "./manifest.js";
|
|
3
|
+
import type { HelperArtifact } from "./helper.js";
|
|
4
|
+
export declare const LIFECYCLE_SCRIPTS: readonly ["preinst", "postinst", "preuninst", "postuninst", "preupgrade", "postupgrade", "start-stop-status"];
|
|
5
|
+
export declare class StandaloneBuildError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
constructor(code: string, message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface AssembleOptions {
|
|
10
|
+
manifest: StandaloneManifest;
|
|
11
|
+
arch: NativeArchFamily;
|
|
12
|
+
/** The compiled or prebuilt CGI executable, already checked. */
|
|
13
|
+
cgiPath: string;
|
|
14
|
+
outputPath: string;
|
|
15
|
+
helper?: HelperArtifact;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Packs an unsigned Standalone Package SPK. Nothing in it runs as root: the
|
|
19
|
+
* package account owns the payload, lifecycle scripts do nothing, and the
|
|
20
|
+
* only entry point is the CGI DSM runs behind its own login.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assembleStandaloneSpk(options: AssembleOptions): Promise<void>;
|