@synopackageland/cli 0.3.10 → 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 +45 -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/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 -862
- package/dist/host/dsm-client.js +12 -272
- package/dist/host/dsm-port-profile.d.ts +0 -1
- package/dist/host/dsm-port-profile.js +0 -2
- package/dist/host/types.d.ts +1 -83
- 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 -822
- 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 -49
- package/dist/host/fake-nas.js +0 -193
- 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
package/dist/init.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { mkdirSync,
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
const TEMPLATE_NAMES = [
|
|
5
5
|
"hello-web",
|
|
6
6
|
"hello-web-page",
|
|
7
7
|
"hello-files",
|
|
8
8
|
"compose-import",
|
|
9
|
-
"
|
|
9
|
+
"binary",
|
|
10
|
+
"native-go",
|
|
10
11
|
];
|
|
11
12
|
export function isTemplateName(value) {
|
|
12
13
|
return TEMPLATE_NAMES.includes(value);
|
|
@@ -22,11 +23,118 @@ function templatesRoot() {
|
|
|
22
23
|
}
|
|
23
24
|
return join(moduleDir, "..", "templates");
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* native-go --privilege root: files laid over the native-go template, and the
|
|
28
|
+
* files they replace.
|
|
29
|
+
*/
|
|
30
|
+
const ROOT_OVERLAY = "native-go-root";
|
|
31
|
+
const ROOT_REPLACED = ["backend/internal/app/root_none.go"];
|
|
32
|
+
/** Templates whose identity is filled in at init (placeholders in these files). */
|
|
33
|
+
const SUBSTITUTED = {
|
|
34
|
+
"native-go": ["dsm-package.yaml", "README.md"],
|
|
35
|
+
};
|
|
36
|
+
const PACKAGE_ID = /^[A-Za-z][A-Za-z0-9]{1,63}$/;
|
|
37
|
+
/** npm never publishes files named .gitignore, so templates ship them as `gitignore`. */
|
|
38
|
+
const RENAMED = { gitignore: ".gitignore" };
|
|
39
|
+
function words(dir) {
|
|
40
|
+
return basename(dir).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
function defaultPackageId(dir) {
|
|
43
|
+
const id = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
44
|
+
return PACKAGE_ID.test(id) ? id : "MyPackage";
|
|
45
|
+
}
|
|
46
|
+
function defaultDisplayName(dir) {
|
|
47
|
+
const name = words(dir).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
48
|
+
return name || "My Package";
|
|
49
|
+
}
|
|
50
|
+
function templateFiles(root, dir = root) {
|
|
51
|
+
return readdirSync(dir).flatMap((name) => {
|
|
52
|
+
const path = join(dir, name);
|
|
53
|
+
return statSync(path).isDirectory() ? templateFiles(root, path) : [relative(root, path)];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function targetName(file) {
|
|
57
|
+
return RENAMED[file] ?? file;
|
|
58
|
+
}
|
|
59
|
+
export function initFromTemplate(template, targetDir, options = {}) {
|
|
26
60
|
const source = join(templatesRoot(), template);
|
|
27
61
|
if (!existsSync(source)) {
|
|
28
62
|
throw new Error(`找不到模板:${template}`);
|
|
29
63
|
}
|
|
64
|
+
const substitutions = substitutionsFor(template, targetDir, options);
|
|
65
|
+
const privilege = options.privilege ?? (options.integration ? "root" : "none");
|
|
66
|
+
const overlays = [
|
|
67
|
+
...(privilege === "root" ? [ROOT_OVERLAY] : []),
|
|
68
|
+
...(options.ui === "vue" ? ["native-go-vue"] : []),
|
|
69
|
+
...(options.integration ? ["native-go-integration"] : []),
|
|
70
|
+
...(options.integration === "better-auth" ? ["native-go-better-auth"] : []),
|
|
71
|
+
].map(name => join(templatesRoot(), name));
|
|
72
|
+
const files = [...templateFiles(source), ...overlays.flatMap(root => templateFiles(root))];
|
|
73
|
+
const collisions = files.map(targetName).filter((file) => existsSync(join(targetDir, file)));
|
|
74
|
+
if (collisions.length > 0) {
|
|
75
|
+
throw new Error(`[INIT_TARGET_NOT_EMPTY] ${targetDir} 已有 ${collisions.join(", ")};init 不會覆蓋既有檔案。請改用空目錄。`);
|
|
76
|
+
}
|
|
30
77
|
mkdirSync(targetDir, { recursive: true });
|
|
31
78
|
cpSync(source, targetDir, { recursive: true });
|
|
79
|
+
for (const overlay of overlays)
|
|
80
|
+
cpSync(overlay, targetDir, { recursive: true });
|
|
81
|
+
if (privilege === "root")
|
|
82
|
+
ROOT_REPLACED.forEach(file => rmSync(join(targetDir, file)));
|
|
83
|
+
for (const [from, to] of Object.entries(RENAMED)) {
|
|
84
|
+
if (files.includes(from)) {
|
|
85
|
+
renameSync(join(targetDir, from), join(targetDir, to));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const file of new Set([...(SUBSTITUTED[template] ?? []), ...overlays.flatMap(root => templateFiles(root))])) {
|
|
89
|
+
const path = join(targetDir, targetName(file));
|
|
90
|
+
let text = readFileSync(path, "utf8");
|
|
91
|
+
for (const [token, value] of Object.entries(substitutions)) {
|
|
92
|
+
text = text.replaceAll(token, value);
|
|
93
|
+
}
|
|
94
|
+
writeFileSync(path, text);
|
|
95
|
+
}
|
|
96
|
+
if (options.ui === "vue") {
|
|
97
|
+
const manifest = join(targetDir, "dsm-package.yaml");
|
|
98
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8").replace(" dir: ui", " dir: frontend/dist\n build: {cwd: frontend, command: [npm, run, build]}"));
|
|
99
|
+
rmSync(join(targetDir, "ui"), { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
if (options.integration === "better-auth")
|
|
102
|
+
rmSync(join(targetDir, "backend/internal/app/identity_none.go"));
|
|
103
|
+
if (options.integration) {
|
|
104
|
+
const mod = join(targetDir, "backend/go.mod");
|
|
105
|
+
writeFileSync(mod, readFileSync(mod, "utf8").replace("go 1.24", "go 1.25"));
|
|
106
|
+
const manifest = join(targetDir, "dsm-package.yaml");
|
|
107
|
+
writeFileSync(manifest, readFileSync(manifest, "utf8") + "\nintegration:\n protocol: 1\n workers: {}\n" + (options.integration === "better-auth" ? " identity: better-auth\n" : ""));
|
|
108
|
+
const routes = join(targetDir, "backend/internal/app/routes.go");
|
|
109
|
+
writeFileSync(routes, readFileSync(routes, "utf8").replace("return append(routes, cfg.rootRoutes()...)", "routes = append(routes, cfg.integrationRoutes()...)\n\treturn append(routes, cfg.rootRoutes()...)"));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function substitutionsFor(template, targetDir, options) {
|
|
113
|
+
if (options.privilege !== undefined && template !== "native-go") {
|
|
114
|
+
throw new Error("[INIT_PRIVILEGE_UNSUPPORTED] --privilege 只適用於 -t native-go。");
|
|
115
|
+
}
|
|
116
|
+
if ((options.ui !== undefined || options.integration !== undefined) && template !== "native-go")
|
|
117
|
+
throw new Error("[INIT_OPTIONS_UNSUPPORTED] --ui/--integration 只適用於 native-go。");
|
|
118
|
+
if (options.ui !== undefined && !["plain", "vue"].includes(options.ui))
|
|
119
|
+
throw new Error("[INIT_UI_INVALID] --ui 只接受 plain 或 vue。");
|
|
120
|
+
if (options.integration !== undefined && !["go", "better-auth"].includes(options.integration))
|
|
121
|
+
throw new Error("[INIT_INTEGRATION_INVALID] --integration 只接受 go 或 better-auth。");
|
|
122
|
+
if (options.integration && (options.ui !== "vue" || options.privilege === "none"))
|
|
123
|
+
throw new Error("[INIT_INTEGRATION_OPTIONS] integration 需要 --ui vue 與 root privilege(自動選用)。");
|
|
124
|
+
const privilege = options.privilege ?? (options.integration ? "root" : "none");
|
|
125
|
+
if (privilege !== "none" && privilege !== "root") {
|
|
126
|
+
throw new Error(`[INIT_PRIVILEGE_INVALID] --privilege 只接受 none 或 root,收到「${String(privilege)}」。`);
|
|
127
|
+
}
|
|
128
|
+
if (!SUBSTITUTED[template]) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
const packageId = options.packageId ?? defaultPackageId(targetDir);
|
|
132
|
+
if (!PACKAGE_ID.test(packageId)) {
|
|
133
|
+
throw new Error(`[INIT_PACKAGE_INVALID] package「${packageId}」必須以英文字母開頭,只含英數字(2–64 字),例如 PhotoTagger。`);
|
|
134
|
+
}
|
|
135
|
+
const displayName = options.displayName ?? defaultDisplayName(targetDir);
|
|
136
|
+
if (displayName.trim() === "" || /[\r\n"\\]/.test(displayName)) {
|
|
137
|
+
throw new Error("[INIT_DISPLAY_NAME_INVALID] 顯示名稱必須是單行,且不含 \" 或 \\。");
|
|
138
|
+
}
|
|
139
|
+
return { __PACKAGE__: packageId, __DISPLAY_NAME__: displayName, __PRIVILEGE__: privilege };
|
|
32
140
|
}
|
package/dist/log.js
CHANGED
|
@@ -6,14 +6,13 @@ export async function followProjectLogs(options) {
|
|
|
6
6
|
return { ok: false, lines: [], diagnostics: { code: "NOT_INSTALLED" } };
|
|
7
7
|
}
|
|
8
8
|
const lines = await options.host.fetchContainerLogs(identity.packageId);
|
|
9
|
-
const info = await options.host.
|
|
9
|
+
const info = await options.host.getInstalledPackageState?.(identity.packageId);
|
|
10
10
|
return {
|
|
11
11
|
ok: true,
|
|
12
12
|
lines,
|
|
13
13
|
diagnostics: {
|
|
14
14
|
packageIdentity: identity.packageId,
|
|
15
|
-
|
|
16
|
-
readiness: info.readiness,
|
|
15
|
+
readiness: info?.status,
|
|
17
16
|
redacted: true,
|
|
18
17
|
},
|
|
19
18
|
};
|
package/dist/native/validate.js
CHANGED
|
@@ -11,7 +11,6 @@ function nativeSpan(file, doc, pathSegments, pathString) {
|
|
|
11
11
|
return { file, line: pos.line, column: pos.column, path: dotPath };
|
|
12
12
|
}
|
|
13
13
|
const HEX64_REGEX = /^[0-9a-fA-F]{64}$/;
|
|
14
|
-
const FORBIDDEN_SERVICE_FIELDS = ["user", "command", "pre_start", "post_start", "shell"];
|
|
15
14
|
export function validateNative(parsed, nativeFile, options = {}) {
|
|
16
15
|
const errors = [];
|
|
17
16
|
const profile = options.profile ?? "personal";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
import type { StandaloneError } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
5
|
+
* project, a 64-bit little-endian ELF for `arch`, and statically linked
|
|
6
|
+
* (no PT_INTERP), because DSM models ship different libc versions.
|
|
7
|
+
* Returns undefined when the file is acceptable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkCgiBinary(projectRoot: string, file: string, arch: NativeArchFamily): StandaloneError | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { closeSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
const ELF_MACHINE = { x86_64: 62, aarch64: 183 };
|
|
4
|
+
const PT_INTERP = 3;
|
|
5
|
+
const MAX_BINARY_BYTES = 256 * 1024 * 1024;
|
|
6
|
+
const HEADER_BYTES = 64 * 1024;
|
|
7
|
+
function fail(code, message) {
|
|
8
|
+
return { code, message };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks a prebuilt CGI before it is packaged: a regular file inside the
|
|
12
|
+
* project, a 64-bit little-endian ELF for `arch`, and statically linked
|
|
13
|
+
* (no PT_INTERP), because DSM models ship different libc versions.
|
|
14
|
+
* Returns undefined when the file is acceptable.
|
|
15
|
+
*/
|
|
16
|
+
export function checkCgiBinary(projectRoot, file, arch) {
|
|
17
|
+
let stat;
|
|
18
|
+
try {
|
|
19
|
+
stat = lstatSync(file);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fail("STANDALONE_BINARY_MISSING", `找不到 ${arch} 的 backend 執行檔:${file}`);
|
|
23
|
+
}
|
|
24
|
+
if (!stat.isFile()) {
|
|
25
|
+
return fail("STANDALONE_BINARY_NOT_REGULAR", `${file} 必須是一般檔案(不可是 symlink 或目錄)。`);
|
|
26
|
+
}
|
|
27
|
+
const rel = relative(realpathSync(projectRoot), realpathSync(file));
|
|
28
|
+
if (rel.startsWith("..")) {
|
|
29
|
+
return fail("STANDALONE_PATH_OUTSIDE_PROJECT", `${file} 實際位於專案目錄之外。`);
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > MAX_BINARY_BYTES) {
|
|
32
|
+
return fail("STANDALONE_BINARY_TOO_LARGE", `${file} 超過 256 MiB。`);
|
|
33
|
+
}
|
|
34
|
+
return checkElf(readHeader(file), file, arch);
|
|
35
|
+
}
|
|
36
|
+
function readHeader(file) {
|
|
37
|
+
const fd = openSync(file, "r");
|
|
38
|
+
try {
|
|
39
|
+
const buf = Buffer.alloc(HEADER_BYTES);
|
|
40
|
+
return buf.subarray(0, readSync(fd, buf, 0, HEADER_BYTES, 0));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function checkElf(buf, file, arch) {
|
|
47
|
+
const notElf = fail("STANDALONE_BINARY_NOT_ELF", `${file} 不是 64-bit little-endian Linux ELF 執行檔。`);
|
|
48
|
+
if (buf.length < 64 || buf.toString("latin1", 0, 4) !== "\x7fELF" || buf[4] !== 2 || buf[5] !== 1) {
|
|
49
|
+
return notElf;
|
|
50
|
+
}
|
|
51
|
+
const machine = buf.readUInt16LE(18);
|
|
52
|
+
if (machine !== ELF_MACHINE[arch]) {
|
|
53
|
+
return fail("STANDALONE_BINARY_ARCH_MISMATCH", `${file} 的 ELF machine 是 ${machine},不是 ${arch}。`);
|
|
54
|
+
}
|
|
55
|
+
const phoff = Number(buf.readBigUInt64LE(32));
|
|
56
|
+
const phentsize = buf.readUInt16LE(54);
|
|
57
|
+
const phnum = buf.readUInt16LE(56);
|
|
58
|
+
if (phentsize < 4 || phoff + phentsize * phnum > buf.length) {
|
|
59
|
+
return notElf;
|
|
60
|
+
}
|
|
61
|
+
for (let i = 0; i < phnum; i++) {
|
|
62
|
+
if (buf.readUInt32LE(phoff + i * phentsize) === PT_INTERP) {
|
|
63
|
+
return fail("STANDALONE_BINARY_DYNAMIC", `${file} 是動態連結執行檔。請改用靜態連結(Go:CGO_ENABLED=0;Rust:*-unknown-linux-musl)。`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
|
+
export interface CommandResult {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stderr: string;
|
|
5
|
+
/** The program itself was not found. */
|
|
6
|
+
missing?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type CommandRunner = (command: string, args: string[], options: {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: Record<string, string | undefined>;
|
|
11
|
+
}) => CommandResult;
|
|
12
|
+
export interface StandaloneBuildOptions {
|
|
13
|
+
root: string;
|
|
14
|
+
/** Defaults to every arch in the manifest. */
|
|
15
|
+
arches?: NativeArchFamily[];
|
|
16
|
+
/** Defaults to `<root>/dist`. */
|
|
17
|
+
outDir?: string;
|
|
18
|
+
run?: CommandRunner;
|
|
19
|
+
}
|
|
20
|
+
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept. */
|
|
21
|
+
export declare const runCommand: CommandRunner;
|
|
22
|
+
/** `build-spk` for a Standalone Package: build the UI, compile the CGI, pack one SPK per arch. */
|
|
23
|
+
export declare function buildStandalone(options: StandaloneBuildOptions): Promise<{
|
|
24
|
+
outputs: string[];
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { checkCgiBinary } from "./binary.js";
|
|
6
|
+
import { loadStandaloneManifest } from "./manifest.js";
|
|
7
|
+
import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
|
|
8
|
+
import { buildHelper } from "./helper.js";
|
|
9
|
+
const GOARCH = { x86_64: "amd64", aarch64: "arm64" };
|
|
10
|
+
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept. */
|
|
11
|
+
export const runCommand = (command, args, options) => {
|
|
12
|
+
const result = spawnSync(command, args, {
|
|
13
|
+
cwd: options.cwd,
|
|
14
|
+
env: options.env,
|
|
15
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
});
|
|
18
|
+
if (result.stderr) {
|
|
19
|
+
process.stderr.write(result.stderr);
|
|
20
|
+
}
|
|
21
|
+
const missing = result.error?.code === "ENOENT";
|
|
22
|
+
return { status: result.status, stderr: result.stderr ?? "", missing };
|
|
23
|
+
};
|
|
24
|
+
/** `build-spk` for a Standalone Package: build the UI, compile the CGI, pack one SPK per arch. */
|
|
25
|
+
export async function buildStandalone(options) {
|
|
26
|
+
const loaded = loadStandaloneManifest(options.root);
|
|
27
|
+
if (!loaded.ok) {
|
|
28
|
+
const [first] = loaded.errors;
|
|
29
|
+
throw new StandaloneBuildError(first.code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"));
|
|
30
|
+
}
|
|
31
|
+
const manifest = loaded.manifest;
|
|
32
|
+
const run = options.run ?? runCommand;
|
|
33
|
+
const arches = options.arches ?? manifest.arches;
|
|
34
|
+
for (const arch of arches) {
|
|
35
|
+
if (!manifest.arches.includes(arch)) {
|
|
36
|
+
throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch} 不在 dsm-package.yaml 的 arches 內。`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
buildUi(manifest, run);
|
|
40
|
+
if (manifest.integration?.identity === "better-auth") {
|
|
41
|
+
const result = run("npm", ["run", "build"], { cwd: join(manifest.root, "node"), env: { ...process.env } });
|
|
42
|
+
if (result.missing || result.status !== 0)
|
|
43
|
+
throw new StandaloneBuildError("STANDALONE_IDENTITY_BUILD_FAILED", `Optional Better Auth build failed.\n${result.stderr}`);
|
|
44
|
+
}
|
|
45
|
+
const outDir = options.outDir ?? join(options.root, "dist");
|
|
46
|
+
const work = mkdtempSync(join(tmpdir(), "syno-standalone-build-"));
|
|
47
|
+
try {
|
|
48
|
+
const outputs = [];
|
|
49
|
+
for (const arch of arches) {
|
|
50
|
+
const helper = buildHelper(manifest, arch, work, run);
|
|
51
|
+
if (helper) {
|
|
52
|
+
const problem = checkCgiBinary(work, helper.binary, arch);
|
|
53
|
+
if (problem)
|
|
54
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
55
|
+
}
|
|
56
|
+
const cgiPath = backendFor(manifest, arch, work, run, helper);
|
|
57
|
+
const problem = checkCgiBinary(manifest.backend.kind === "go" ? work : manifest.root, cgiPath, arch);
|
|
58
|
+
if (problem) {
|
|
59
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
60
|
+
}
|
|
61
|
+
const outputPath = join(outDir, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
62
|
+
const builtManifest = helper ? { ...manifest, rootOps: [...(manifest.rootOps ?? []), ...helper.ops] } : manifest;
|
|
63
|
+
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, helper });
|
|
64
|
+
outputs.push(outputPath);
|
|
65
|
+
}
|
|
66
|
+
return { outputs };
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
rmSync(work, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildUi(manifest, run) {
|
|
73
|
+
const build = manifest.ui.build;
|
|
74
|
+
if (!build) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const [command, ...args] = build.command;
|
|
78
|
+
const result = run(command, args, { cwd: build.cwd, env: { ...process.env } });
|
|
79
|
+
if (result.missing || result.status !== 0) {
|
|
80
|
+
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build(${build.command.join(" ")})失敗。\n${result.stderr}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function backendFor(manifest, arch, work, run, helper) {
|
|
84
|
+
const backend = manifest.backend;
|
|
85
|
+
if (backend.kind === "binary") {
|
|
86
|
+
return backend.files[arch];
|
|
87
|
+
}
|
|
88
|
+
const out = join(work, `api-${arch}.cgi`);
|
|
89
|
+
const ldflags = [
|
|
90
|
+
"-s",
|
|
91
|
+
"-w",
|
|
92
|
+
"-buildid=",
|
|
93
|
+
`-X main.packageID=${manifest.package}`,
|
|
94
|
+
`-X main.version=${manifest.spkVersion}`,
|
|
95
|
+
...(helper ? [`-X 'main.helperInventory=${JSON.stringify(helper.inventory)}'`] : []),
|
|
96
|
+
].join(" ");
|
|
97
|
+
const result = run("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, backend.main], {
|
|
98
|
+
cwd: backend.dir,
|
|
99
|
+
env: { ...process.env, CGO_ENABLED: "0", GOOS: "linux", GOARCH: GOARCH[arch] },
|
|
100
|
+
});
|
|
101
|
+
if (result.missing) {
|
|
102
|
+
throw new StandaloneBuildError("STANDALONE_GO_MISSING", "找不到 go 指令。請安裝 Go 1.24 以上版本(https://go.dev/dl/),或改用 backend.binary 打包既有執行檔。");
|
|
103
|
+
}
|
|
104
|
+
if (result.status !== 0) {
|
|
105
|
+
throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build(${arch})失敗。\n${result.stderr}`);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StandaloneManifest } from "./manifest.js";
|
|
2
|
+
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
3
|
+
export declare const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
4
|
+
export declare function cgiUrl(packageId: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* `createI18n(dsmLang, catalogs)` → `{ locale, t }`, plain ES5 so it runs in
|
|
7
|
+
* DSM's desktop as is. The DSM language picks a shipped catalog (else
|
|
8
|
+
* English); missing keys fall back to English, then to the key itself.
|
|
9
|
+
* `t(key, { count })` looks up `key.one` / `key.other` via Intl.PluralRules.
|
|
10
|
+
*/
|
|
11
|
+
export declare const I18N_RUNTIME: string;
|
|
12
|
+
/**
|
|
13
|
+
* The wrapper file name carries the build so DSM never serves a wrapper
|
|
14
|
+
* cached from the previous version after an upgrade.
|
|
15
|
+
*/
|
|
16
|
+
export declare function windowScriptName(manifest: StandaloneManifest): string;
|
|
17
|
+
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
18
|
+
export declare function renderUiConfig(manifest: StandaloneManifest): string;
|
|
19
|
+
/**
|
|
20
|
+
* The DSM AppWindow that hosts the project's UI. It loads app/app.js and
|
|
21
|
+
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call` is the
|
|
22
|
+
* only transport: same-origin POST with DSM's session cookie and SynoToken.
|
|
23
|
+
* `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
|
|
24
|
+
* `assetVersion` busts browser caches when the UI changes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string): string;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { DSM_LANGS } from "./i18n.js";
|
|
2
|
+
import { ROOT_MESSAGES } from "./root-messages.js";
|
|
3
|
+
import { ROOT_PANEL, ROOT_RUNTIME, rootTasks } from "./root-window.js";
|
|
4
|
+
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
5
|
+
export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
6
|
+
export function cgiUrl(packageId) {
|
|
7
|
+
return `/webman/3rdparty/${packageId}/cgi/api.cgi`;
|
|
8
|
+
}
|
|
9
|
+
const LOCALE_BY_DSM_LANG = Object.fromEntries(Object.entries(DSM_LANGS).map(([locale, lang]) => [lang, locale]));
|
|
10
|
+
/**
|
|
11
|
+
* `createI18n(dsmLang, catalogs)` → `{ locale, t }`, plain ES5 so it runs in
|
|
12
|
+
* DSM's desktop as is. The DSM language picks a shipped catalog (else
|
|
13
|
+
* English); missing keys fall back to English, then to the key itself.
|
|
14
|
+
* `t(key, { count })` looks up `key.one` / `key.other` via Intl.PluralRules.
|
|
15
|
+
*/
|
|
16
|
+
export const I18N_RUNTIME = `function createI18n(dsmLang, catalogs) {
|
|
17
|
+
var byLang = ${JSON.stringify(LOCALE_BY_DSM_LANG)};
|
|
18
|
+
var has = function (object, key) { return Object.prototype.hasOwnProperty.call(object, key); };
|
|
19
|
+
var wanted = has(byLang, dsmLang) ? byLang[dsmLang] : "en";
|
|
20
|
+
var locale = has(catalogs, wanted) ? wanted : "en";
|
|
21
|
+
var own = catalogs[locale] || {};
|
|
22
|
+
var en = catalogs.en || {};
|
|
23
|
+
var plural = typeof Intl !== "undefined" && Intl.PluralRules ? new Intl.PluralRules(locale) : null;
|
|
24
|
+
function lookup(key) {
|
|
25
|
+
return has(own, key) ? own[key] : has(en, key) ? en[key] : undefined;
|
|
26
|
+
}
|
|
27
|
+
function t(key, params) {
|
|
28
|
+
var text = lookup(key);
|
|
29
|
+
if (text === undefined && params && typeof params.count === "number") {
|
|
30
|
+
var form = plural ? plural.select(params.count) : params.count === 1 ? "one" : "other";
|
|
31
|
+
text = lookup(key + "." + form);
|
|
32
|
+
if (text === undefined) text = lookup(key + ".other");
|
|
33
|
+
}
|
|
34
|
+
if (text === undefined) return key;
|
|
35
|
+
return text.replace(/\\{(\\w+)\\}/g, function (match, name) {
|
|
36
|
+
return params && has(params, name) ? String(params[name]) : match;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { locale: locale, t: t };
|
|
40
|
+
}`;
|
|
41
|
+
/**
|
|
42
|
+
* The wrapper file name carries the build so DSM never serves a wrapper
|
|
43
|
+
* cached from the previous version after an upgrade.
|
|
44
|
+
*/
|
|
45
|
+
export function windowScriptName(manifest) {
|
|
46
|
+
return `window-${String(manifest.build).padStart(4, "0")}.js`;
|
|
47
|
+
}
|
|
48
|
+
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
49
|
+
export function renderUiConfig(manifest) {
|
|
50
|
+
const ns = `SYNO.SDS.${manifest.package}`;
|
|
51
|
+
return `${JSON.stringify({
|
|
52
|
+
[windowScriptName(manifest)]: {
|
|
53
|
+
[`${ns}.Instance`]: {
|
|
54
|
+
type: "app",
|
|
55
|
+
title: manifest.displayname,
|
|
56
|
+
desc: manifest.description,
|
|
57
|
+
icon: "images/app_{0}.png",
|
|
58
|
+
appWindow: `${ns}.MainWindow`,
|
|
59
|
+
allowMultiInstance: false,
|
|
60
|
+
allUsers: true,
|
|
61
|
+
depend: [`${ns}.MainWindow`],
|
|
62
|
+
},
|
|
63
|
+
[`${ns}.MainWindow`]: {
|
|
64
|
+
type: "lib",
|
|
65
|
+
title: manifest.displayname,
|
|
66
|
+
icon: "images/app_{0}.png",
|
|
67
|
+
depend: [],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}, null, 2)}\n`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The DSM AppWindow that hosts the project's UI. It loads app/app.js and
|
|
74
|
+
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call` is the
|
|
75
|
+
* only transport: same-origin POST with DSM's session cookie and SynoToken.
|
|
76
|
+
* `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
|
|
77
|
+
* `assetVersion` busts browser caches when the UI changes.
|
|
78
|
+
*/
|
|
79
|
+
export function renderWindowScript(manifest, assetVersion) {
|
|
80
|
+
const pkg = manifest.package;
|
|
81
|
+
const ns = `SYNO.SDS.${pkg}`;
|
|
82
|
+
const p = JSON.stringify(pkg);
|
|
83
|
+
const root = manifest.rootOps ? renderRootParts(ns, manifest) : undefined;
|
|
84
|
+
return `/* Generated by synopkgland build-spk. Edit your UI in the project's ui folder. */
|
|
85
|
+
Ext.ns(${JSON.stringify(ns)});
|
|
86
|
+
|
|
87
|
+
${ns}.call = function (method, params) {
|
|
88
|
+
var headers = { "Content-Type": "application/json" };
|
|
89
|
+
var token = SYNO.SDS.Session && SYNO.SDS.Session.SynoToken;
|
|
90
|
+
if (token) {
|
|
91
|
+
headers["X-SYNO-TOKEN"] = token;
|
|
92
|
+
}
|
|
93
|
+
return fetch(${JSON.stringify(cgiUrl(pkg))}, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
credentials: "same-origin",
|
|
96
|
+
headers: headers,
|
|
97
|
+
body: JSON.stringify({ method: method, params: params || {} })
|
|
98
|
+
}).then(function (response) {
|
|
99
|
+
return response.json().catch(function () {
|
|
100
|
+
return { success: false, error: { code: "bad_response", message: "HTTP " + response.status } };
|
|
101
|
+
});
|
|
102
|
+
}, function () {
|
|
103
|
+
return { success: false, error: { code: "network", message: "The NAS did not answer" } };
|
|
104
|
+
}).then(function (body) {
|
|
105
|
+
if (body && body.success === true) {
|
|
106
|
+
return body.data;
|
|
107
|
+
}
|
|
108
|
+
throw (body && body.error) || { code: "bad_response", message: "Unexpected response" };
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
${ns}.messages = ${JSON.stringify(manifest.i18n?.catalogs ?? { en: {} })};
|
|
113
|
+
${ns}.createI18n = (function () {
|
|
114
|
+
${I18N_RUNTIME}
|
|
115
|
+
return createI18n;
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
${root ? root.definitions : ""}${ns}.loadApp = function (done) {
|
|
119
|
+
var apps = window.DsmPackageApps || {};
|
|
120
|
+
if (apps[${p}]) {
|
|
121
|
+
done(apps[${p}]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
var base = "webman/3rdparty/${pkg}/app/";
|
|
125
|
+
var css = document.createElement("link");
|
|
126
|
+
css.rel = "stylesheet";
|
|
127
|
+
css.href = base + "app.css?v=${assetVersion}";
|
|
128
|
+
document.head.appendChild(css);
|
|
129
|
+
var js = document.createElement("script");
|
|
130
|
+
js.src = base + "app.js?v=${assetVersion}";
|
|
131
|
+
js.dataset.package = ${p};
|
|
132
|
+
js.onload = function () { done((window.DsmPackageApps || {})[${p}]); };
|
|
133
|
+
document.head.appendChild(js);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
Ext.define(${JSON.stringify(`${ns}.Instance`)}, {
|
|
137
|
+
extend: "SYNO.SDS.AppInstance",
|
|
138
|
+
appWindowName: ${JSON.stringify(`${ns}.MainWindow`)}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
142
|
+
extend: "SYNO.SDS.AppWindow",
|
|
143
|
+
constructor: function (config) {
|
|
144
|
+
this.callParent([Ext.apply({
|
|
145
|
+
width: 960, height: 640, minWidth: 640, minHeight: 420, layout: "fit",
|
|
146
|
+
${root ? root.html : `html: '<div class="dsmapp-root" style="height:100%"></div>'`}
|
|
147
|
+
}, config)]);
|
|
148
|
+
},
|
|
149
|
+
onOpen: function () {
|
|
150
|
+
this.callParent(arguments);
|
|
151
|
+
var me = this;
|
|
152
|
+
if (me.mounted) {
|
|
153
|
+
me.reopenHandlers.forEach(function (fn) { fn(); });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
me.reopenHandlers = [];
|
|
157
|
+
var lang = typeof _S === "function" ? _S("lang") : "enu";
|
|
158
|
+
${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
159
|
+
var root = me.body && me.body.dom.querySelector(".dsmapp-root");
|
|
160
|
+
if (app && root && !me.mounted) {
|
|
161
|
+
var i18n = ${ns}.createI18n(lang, ${ns}.messages);
|
|
162
|
+
root.lang = i18n.locale;
|
|
163
|
+
me.mounted = app.mount(root, {
|
|
164
|
+
call: ${ns}.call,
|
|
165
|
+
reopen: function (fn) { me.reopenHandlers.push(fn); },
|
|
166
|
+
appWindow: me,
|
|
167
|
+
lang: lang,
|
|
168
|
+
locale: i18n.locale,
|
|
169
|
+
t: i18n.t${root ? ",\n root: access" : ""}
|
|
170
|
+
}) || {};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
onDestroy: function () {
|
|
175
|
+
if (this.mounted && this.mounted.unmount) {
|
|
176
|
+
this.mounted.unmount();
|
|
177
|
+
}
|
|
178
|
+
this.mounted = null;
|
|
179
|
+
this.callParent(arguments);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
const ROOT_PANEL_CSS = [
|
|
185
|
+
".dsmapp-rootpanel{margin:16px 20px;padding:12px 16px;border:1px solid #d5dce5;border-radius:4px;background:#f7f9fb;font-size:13px;line-height:1.5}",
|
|
186
|
+
".dsmapp-rootpanel h2{margin:0 0 6px;font-size:14px;font-weight:600}",
|
|
187
|
+
".dsmapp-rootpanel dl{display:grid;grid-template-columns:max-content 1fr;gap:2px 12px;margin:8px 0}",
|
|
188
|
+
".dsmapp-rootpanel dt{color:#526070}.dsmapp-rootpanel dd{margin:0}",
|
|
189
|
+
".dsmapp-rootpanel pre{margin:4px 0 8px;padding:6px 8px;background:#fff;border:1px solid #e1e6ec;white-space:pre-wrap;max-height:160px;overflow:auto}",
|
|
190
|
+
".dsmapp-rootpanel-actions button{margin-right:8px;padding:4px 14px}",
|
|
191
|
+
".dsmapp-rootpanel-primary{background:#057feb;color:#fff;border:1px solid #057feb;border-radius:3px}",
|
|
192
|
+
".dsmapp-rootpanel-message{color:#c0392b}.dsmapp-rootpanel-hint{color:#526070;font-size:12px}",
|
|
193
|
+
].join("");
|
|
194
|
+
/**
|
|
195
|
+
* The privilege: root additions to the window: the exact tasks and panel
|
|
196
|
+
* text, the access runtime wired to DSM's own password confirmation and the
|
|
197
|
+
* AppWindow's WebAPI channel, and the panel above the app.
|
|
198
|
+
*/
|
|
199
|
+
function renderRootParts(ns, manifest) {
|
|
200
|
+
return {
|
|
201
|
+
definitions: `${ns}.rootTasks = ${JSON.stringify(rootTasks(manifest))};
|
|
202
|
+
${ns}.rootMessages = ${JSON.stringify(ROOT_MESSAGES)};
|
|
203
|
+
${ns}.createRootAccess = (function () {
|
|
204
|
+
${ROOT_RUNTIME}
|
|
205
|
+
return createRootAccess;
|
|
206
|
+
})();
|
|
207
|
+
${ns}.mountRootPanel = (function () {
|
|
208
|
+
${ROOT_PANEL}
|
|
209
|
+
return mountRootPanel;
|
|
210
|
+
})();
|
|
211
|
+
|
|
212
|
+
`,
|
|
213
|
+
html: `autoScroll: true,
|
|
214
|
+
html: '<style>${ROOT_PANEL_CSS}</style><div class="dsmapp-rootpanels"></div><div class="dsmapp-root"></div>'`,
|
|
215
|
+
mount: ` var session = function () {
|
|
216
|
+
var current = SYNO.SDS.Session || {};
|
|
217
|
+
return { name: current.user, admin: current.is_admin === true };
|
|
218
|
+
};
|
|
219
|
+
var access = ${ns}.createRootAccess({
|
|
220
|
+
tasks: ${ns}.rootTasks,
|
|
221
|
+
user: session,
|
|
222
|
+
send: function (api, method, version, params) {
|
|
223
|
+
return new Promise(function (resolve, reject) {
|
|
224
|
+
me.sendWebAPI({
|
|
225
|
+
api: api, method: method, version: version, params: params, scope: me,
|
|
226
|
+
callback: function (success, response) {
|
|
227
|
+
if (success) resolve(response);
|
|
228
|
+
else reject({ code: "dsm_" + ((response && response.code) || "error") });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
confirmPassword: function () {
|
|
234
|
+
return new Promise(function (resolve) {
|
|
235
|
+
SYNO.SDS.Utils.PasswordConfirmDialog.openDialog(me, function (confirmation) {
|
|
236
|
+
resolve((confirmation && confirmation.SynoConfirmPWToken) || null);
|
|
237
|
+
}, [], function () { resolve(null); });
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
call: ${ns}.call,
|
|
241
|
+
wait: function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
242
|
+
});
|
|
243
|
+
var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
|
|
244
|
+
if (panels) {
|
|
245
|
+
var rootI18n = ${ns}.createI18n(lang, ${ns}.rootMessages);
|
|
246
|
+
panels.lang = rootI18n.locale;
|
|
247
|
+
${ns}.mountRootPanel(panels, access, rootI18n, ${ns}.rootTasks, session().admin);
|
|
248
|
+
}
|
|
249
|
+
`,
|
|
250
|
+
};
|
|
251
|
+
}
|