@synopackageland/cli 0.9.6 → 0.10.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/CHANGELOG.md +144 -5
- package/README.md +5 -0
- package/dist/add.js +7 -8
- package/dist/build-spk/dsm-arch.js +1 -1
- package/dist/build-spk/dsm-icons.d.ts +8 -1
- package/dist/build-spk/dsm-icons.js +26 -12
- package/dist/build-spk/dsm-version.d.ts +1 -1
- package/dist/build-spk/dsm-version.js +4 -4
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.js +1 -1
- package/dist/build-spk/png.js +16 -16
- package/dist/capabilities.js +1 -1
- package/dist/capability-overlays.d.ts +16 -5
- package/dist/capability-overlays.js +30 -16
- package/dist/cli.js +107 -107
- package/dist/compose/analyze.js +23 -23
- package/dist/compose/privilege.js +12 -12
- package/dist/contract/i18n.js +3 -3
- package/dist/contract/icon.js +2 -2
- package/dist/contract/posix-all-shares.js +2 -2
- package/dist/contract/schema-validate.js +3 -3
- package/dist/contract/validate.js +16 -16
- package/dist/contract/web-launch.d.ts +2 -1
- package/dist/contract/web-launch.js +3 -2
- package/dist/deploy/deploy.js +8 -8
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/identity-shared.js +1 -1
- package/dist/deploy/native-install-values.js +1 -1
- package/dist/deploy/param-values.js +4 -4
- package/dist/deploy/plan.d.ts +1 -1
- package/dist/deploy/port-preflight.js +3 -3
- package/dist/deploy/run-as-values.js +2 -2
- package/dist/deploy/share-permission-preflight.js +6 -6
- package/dist/deploy/share-values.js +3 -3
- package/dist/error-registry.js +369 -346
- package/dist/errors.js +1 -1
- package/dist/host/dsm-port-profile.d.ts +1 -1
- package/dist/host/dsm-port-profile.js +1 -1
- package/dist/info.js +11 -11
- package/dist/init.d.ts +2 -0
- package/dist/init.js +85 -38
- package/dist/inspect.js +5 -5
- package/dist/portal/alias.d.ts +1 -1
- package/dist/portal/alias.js +4 -4
- package/dist/runtime/executor-build.js +12 -1
- package/dist/runtime/executor-digests.json +8 -85
- package/dist/runtime/executor-inspect.js +11 -4
- package/dist/runtime/executor-policy.d.ts +9 -2
- package/dist/runtime/executor-policy.js +27 -8
- package/dist/runtime/root-programs.d.ts +47 -0
- package/dist/runtime/root-programs.js +153 -0
- package/dist/runtime/sdk-source.d.ts +5 -4
- package/dist/runtime/sdk-source.js +8 -4
- package/dist/skill.js +14 -14
- package/dist/spk/check-info.js +17 -17
- package/dist/spk/check-metadata.js +18 -18
- package/dist/spk/check-payload.js +10 -10
- package/dist/spk/rules.d.ts +57 -57
- package/dist/spk/rules.js +50 -50
- package/dist/spk/validate-spk.js +5 -5
- package/dist/standalone/api-permissions.js +1 -1
- package/dist/standalone/background.d.ts +3 -1
- package/dist/standalone/background.js +8 -4
- package/dist/standalone/binary.js +7 -7
- package/dist/standalone/bridge-app.d.ts +23 -0
- package/dist/standalone/bridge-app.js +141 -0
- package/dist/standalone/bridge-backend.d.ts +40 -0
- package/dist/standalone/bridge-backend.js +95 -0
- package/dist/standalone/bridge-build.d.ts +21 -0
- package/dist/standalone/bridge-build.js +62 -0
- package/dist/standalone/bridge-json.d.ts +16 -0
- package/dist/standalone/bridge-json.js +59 -0
- package/dist/standalone/bridge-runtime.d.ts +16 -0
- package/dist/standalone/bridge-runtime.js +46 -0
- package/dist/standalone/bridge-rust.d.ts +16 -0
- package/dist/standalone/bridge-rust.js +89 -0
- package/dist/standalone/build.js +18 -14
- package/dist/standalone/daemon.js +6 -6
- package/dist/standalone/dsm-window.d.ts +10 -5
- package/dist/standalone/dsm-window.js +64 -16
- package/dist/standalone/i18n.js +18 -18
- package/dist/standalone/inspect-bridge.d.ts +31 -0
- package/dist/standalone/inspect-bridge.js +111 -0
- package/dist/standalone/inspect-spk.d.ts +1 -1
- package/dist/standalone/inspect-spk.js +77 -69
- package/dist/standalone/lifecycle.js +22 -3
- package/dist/standalone/manifest.d.ts +17 -9
- package/dist/standalone/manifest.js +143 -65
- package/dist/standalone/portal-window.js +14 -7
- package/dist/standalone/request-timeout.d.ts +9 -0
- package/dist/standalone/request-timeout.js +13 -0
- package/dist/standalone/root-ops.d.ts +23 -2
- package/dist/standalone/root-ops.js +118 -13
- package/dist/standalone/sdk-check.js +4 -4
- package/dist/standalone/shared-folder.js +7 -7
- package/dist/standalone/spk.d.ts +2 -0
- package/dist/standalone/spk.js +30 -11
- package/dist/standalone/uninstall-hook.js +22 -4
- package/dist/standalone/uninstall.js +7 -7
- package/dist/standalone/window-size.d.ts +17 -0
- package/dist/standalone/window-size.js +31 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +13 -13
- package/package.json +2 -2
- package/schema/bridge-manifest.v1.schema.json +86 -0
- package/skills/reference/create.md +76 -2
- package/skills/reference/ui-json.md +2 -1
- package/skills/reference/ui-vue.md +9 -3
- package/skills/reference/wrap.md +1 -1
- package/dist/standalone/rust-build.d.ts +0 -35
- package/dist/standalone/rust-build.js +0 -127
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { SDK_GIT_URL } from "../runtime/sdk-source.js";
|
|
5
|
+
import { checkCgiBinary } from "./binary.js";
|
|
6
|
+
import { RUST_BIN_DIR } from "./bridge-json.js";
|
|
7
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
8
|
+
/** The glibc loader of DSM's ARMv7 models, the one interpreter an armv7 Rust App may name. */
|
|
9
|
+
export const ARMV7_INTERPRETER = "/lib/ld-linux-armhf.so.3";
|
|
10
|
+
/**
|
|
11
|
+
* x86_64 and aarch64 are static musl binaries. armv7 links against glibc
|
|
12
|
+
* (2.17, older than any DSM 7), because musl's 64-bit time calls fail on
|
|
13
|
+
* DSM's ARMv7 kernels; zig provides both, so any host can build all three.
|
|
14
|
+
*/
|
|
15
|
+
export const RUST_TARGETS = {
|
|
16
|
+
x86_64: { triple: "x86_64-unknown-linux-musl" },
|
|
17
|
+
aarch64: { triple: "aarch64-unknown-linux-musl" },
|
|
18
|
+
armv7: { triple: "armv7-unknown-linux-gnueabihf.2.17", interpreter: ARMV7_INTERPRETER },
|
|
19
|
+
};
|
|
20
|
+
const RUSTUP_HINT = "Install Rust with rustup (https://rustup.rs), run rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl armv7-unknown-linux-gnueabihf, and make sure the cargo on PATH is rustup's (not Homebrew's rust).";
|
|
21
|
+
/** Cross-compiles the App's binary for one architecture into `work/app-<arch>/bin`; returns that App directory. */
|
|
22
|
+
export async function buildRustApp(m, arch, work, run) {
|
|
23
|
+
const backend = m.backend;
|
|
24
|
+
const target = RUST_TARGETS[arch];
|
|
25
|
+
const result = await run("cargo", [
|
|
26
|
+
"zigbuild", "--release", "--locked", "--target", target.triple, "--bin", backend.entry, "--message-format=json-render-diagnostics",
|
|
27
|
+
], { cwd: backend.dir, env: cargoEnv(backend.dir), capture: true });
|
|
28
|
+
if (result.missing)
|
|
29
|
+
fail(`cargo was not found. ${RUSTUP_HINT}`);
|
|
30
|
+
if (result.status !== 0) {
|
|
31
|
+
const hint = /no such command: `?zigbuild/.test(result.stderr) ? "Run cargo install cargo-zigbuild and install zig (https://ziglang.org/download/)."
|
|
32
|
+
: /can't find crate for `(core|std)`/.test(result.stderr) ? RUSTUP_HINT : "";
|
|
33
|
+
fail(`cargo zigbuild (${arch}) failed. ${hint}\n${result.stderr}`);
|
|
34
|
+
}
|
|
35
|
+
const app = join(work, `app-${arch}`);
|
|
36
|
+
rmSync(app, { recursive: true, force: true });
|
|
37
|
+
mkdirSync(join(app, RUST_BIN_DIR), { recursive: true });
|
|
38
|
+
const out = join(app, RUST_BIN_DIR, backend.entry);
|
|
39
|
+
copyFileSync(artifact(result.stdout ?? "", backend.entry), out);
|
|
40
|
+
chmodSync(out, 0o755);
|
|
41
|
+
const problem = checkCgiBinary(work, out, arch, target.interpreter);
|
|
42
|
+
if (problem)
|
|
43
|
+
throw new StandaloneBuildError(problem.code, problem.message);
|
|
44
|
+
return app;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The environment of one reproducible cargo build: the CLI's flags replace any
|
|
48
|
+
* inherited RUSTFLAGS, paths are remapped, symbols and build IDs dropped, and
|
|
49
|
+
* panics unwind so the SDK answers `internal` instead of the process ending.
|
|
50
|
+
* Cargo fetches the private SDK repository through git, with the user's credentials.
|
|
51
|
+
*/
|
|
52
|
+
function cargoEnv(dir) {
|
|
53
|
+
const env = { ...process.env };
|
|
54
|
+
delete env.RUSTFLAGS;
|
|
55
|
+
const cargoHome = env.CARGO_HOME || join(homedir(), ".cargo");
|
|
56
|
+
env.CARGO_ENCODED_RUSTFLAGS = [
|
|
57
|
+
`--remap-path-prefix=${dir}=/build`,
|
|
58
|
+
`--remap-path-prefix=${cargoHome}=/cargo`,
|
|
59
|
+
"-Cstrip=symbols",
|
|
60
|
+
"-Clink-arg=-Wl,--build-id=none",
|
|
61
|
+
// zig notes that it ignores rustc's own -Wl,-O1; such linker output is no warning of the App. Link errors still fail.
|
|
62
|
+
"-Alinker_messages",
|
|
63
|
+
].join("\x1f");
|
|
64
|
+
env.CARGO_PROFILE_RELEASE_PANIC = "unwind";
|
|
65
|
+
env.SOURCE_DATE_EPOCH = "0";
|
|
66
|
+
env.CARGO_NET_GIT_FETCH_WITH_CLI = "true";
|
|
67
|
+
if (env.SYNOPKGLAND_SDK_DIR) {
|
|
68
|
+
// Development only: git reads the SDK repository from this checkout, which must hold the locked tag.
|
|
69
|
+
const n = Number(env.GIT_CONFIG_COUNT ?? 0);
|
|
70
|
+
env.GIT_CONFIG_COUNT = String(n + 1);
|
|
71
|
+
env[`GIT_CONFIG_KEY_${n}`] = `url.file://${env.SYNOPKGLAND_SDK_DIR}.insteadOf`;
|
|
72
|
+
env[`GIT_CONFIG_VALUE_${n}`] = SDK_GIT_URL;
|
|
73
|
+
}
|
|
74
|
+
return env;
|
|
75
|
+
}
|
|
76
|
+
/** The executable Cargo reported for `bin`. */
|
|
77
|
+
function artifact(messages, bin) {
|
|
78
|
+
for (const line of messages.split("\n")) {
|
|
79
|
+
if (!line.startsWith("{"))
|
|
80
|
+
continue;
|
|
81
|
+
const message = JSON.parse(line);
|
|
82
|
+
if (message.reason === "compiler-artifact" && message.target?.kind?.includes("bin") && message.target.name === bin && message.executable)
|
|
83
|
+
return message.executable;
|
|
84
|
+
}
|
|
85
|
+
return fail(`cargo zigbuild produced no bin ${bin}; backend.entry must be the Cargo project's binary name.`);
|
|
86
|
+
}
|
|
87
|
+
function fail(message) {
|
|
88
|
+
throw new StandaloneBuildError("STANDALONE_RUST_BUILD_FAILED", message);
|
|
89
|
+
}
|
package/dist/standalone/build.js
CHANGED
|
@@ -7,13 +7,15 @@ import { goBuildEnv } from "../build-spk/dsm-arch.js";
|
|
|
7
7
|
import { resolveMinDsm, DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM, portalByExecutor, portalMinDsmMessage } from "../build-spk/dsm-version.js";
|
|
8
8
|
import { checkCgiBinary } from "./binary.js";
|
|
9
9
|
import { loadStandaloneManifest } from "./manifest.js";
|
|
10
|
-
import { buildRustBackend } from "./rust-build.js";
|
|
11
10
|
import { assembleStandaloneSpk, StandaloneBuildError } from "./spk.js";
|
|
12
11
|
import { buildDaemonControl } from "./daemon.js";
|
|
13
12
|
import { buildExecutor } from "../runtime/executor-build.js";
|
|
14
13
|
import { BACKGROUND_WORKER_MAIN } from "./background.js";
|
|
15
14
|
import { readApiPermissions } from "./api-permissions.js";
|
|
16
15
|
import { checkJobs, commitOutputs, forEachArch } from "./arch-jobs.js";
|
|
16
|
+
import { isBridgeBackend } from "./bridge-backend.js";
|
|
17
|
+
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
18
|
+
import { buildBridge, prepareBridge } from "./bridge-build.js";
|
|
17
19
|
/** Argv only, never a shell; output streams to the terminal except stderr, which is also kept, and captured stdout. */
|
|
18
20
|
export const runCommand = (command, args, options) => new Promise((done) => {
|
|
19
21
|
const child = spawn(command, args, {
|
|
@@ -88,7 +90,7 @@ export async function buildStandalone(options) {
|
|
|
88
90
|
const arches = options.arches ?? manifest.arches;
|
|
89
91
|
for (const arch of arches) {
|
|
90
92
|
if (!manifest.arches.includes(arch)) {
|
|
91
|
-
throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch}
|
|
93
|
+
throw new StandaloneBuildError("STANDALONE_ARCH_NOT_DECLARED", `${arch} is not in the arches of dsm-package.yaml; to build it, add ${arch} to arches first.`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
await buildUi(manifest, options.run ?? runCommand);
|
|
@@ -96,6 +98,7 @@ export async function buildStandalone(options) {
|
|
|
96
98
|
const work = mkdtempSync(join(tmpdir(), "syno-standalone-build-"));
|
|
97
99
|
const warnings = new Set();
|
|
98
100
|
try {
|
|
101
|
+
const bridge = await prepareBridge(manifest, work);
|
|
99
102
|
const outputs = await commitOutputs(outDir, (stage) => forEachArch(arches, jobs, async (arch) => {
|
|
100
103
|
const run = runnerFor(options.run, jobs, arch);
|
|
101
104
|
const archWork = join(work, arch);
|
|
@@ -103,10 +106,10 @@ export async function buildStandalone(options) {
|
|
|
103
106
|
const executor = await buildExecutor(manifest, arch, archWork, run);
|
|
104
107
|
executor?.warnings.forEach(warning => warnings.add(warning));
|
|
105
108
|
const builtManifest = executor ? { ...manifest, executor } : manifest;
|
|
106
|
-
const { cgi: cgiPath, worker: backgroundWorker } = await backendFor(builtManifest, arch, archWork, run);
|
|
109
|
+
const { cgi: cgiPath, worker: backgroundWorker, app } = await backendFor(builtManifest, arch, archWork, run, bridge);
|
|
107
110
|
const outputPath = join(stage, `${manifest.package}-${manifest.spkVersion}-${arch}.spk`);
|
|
108
111
|
const daemonControl = await buildDaemonControl(manifest, arch, archWork, run);
|
|
109
|
-
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor });
|
|
112
|
+
await assembleStandaloneSpk({ manifest: builtManifest, arch, cgiPath, outputPath, daemonControl, backgroundWorker, executor, app });
|
|
110
113
|
return outputPath;
|
|
111
114
|
}));
|
|
112
115
|
return { outputs, warnings: [...warnings] };
|
|
@@ -124,16 +127,16 @@ async function buildUi(manifest, run) {
|
|
|
124
127
|
const result = await run(command, args, { cwd: build.cwd, env: { ...process.env } });
|
|
125
128
|
if (result.missing || result.status !== 0) {
|
|
126
129
|
const installHint = command === "npm" && existsSync(join(build.cwd, "package.json")) && !existsSync(join(build.cwd, "node_modules"))
|
|
127
|
-
? `\
|
|
130
|
+
? `\nThe UI dependencies are not installed. Run npm ${existsSync(join(build.cwd, "package-lock.json")) ? "ci" : "install"} in ${build.cwd}, then run build again.`
|
|
128
131
|
: "";
|
|
129
|
-
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build
|
|
132
|
+
throw new StandaloneBuildError("STANDALONE_UI_BUILD_FAILED", `UI build (${build.command.join(" ")}) failed. ${installHint}\n${result.stderr}`);
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
|
-
/** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker. */
|
|
133
|
-
async function backendFor(manifest, arch, work, run) {
|
|
135
|
+
/** The checked CGI of one architecture and, with capabilities.jobs, its fixed worker; or the runtime and App of a bridge App. */
|
|
136
|
+
async function backendFor(manifest, arch, work, run, bridge) {
|
|
134
137
|
const backend = manifest.backend;
|
|
135
|
-
if (backend
|
|
136
|
-
return
|
|
138
|
+
if (isBridgeBackend(backend))
|
|
139
|
+
return buildBridge(manifest, bridge, arch, work, run);
|
|
137
140
|
const cgi = backend.kind === "binary" ? backend.files[arch] : await buildGoCgi(manifest, backend, arch, work, run);
|
|
138
141
|
const problem = checkCgiBinary(backend.kind === "go" ? work : manifest.root, cgi, arch);
|
|
139
142
|
if (problem) {
|
|
@@ -149,7 +152,7 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
|
149
152
|
}
|
|
150
153
|
const module = goModulePath(backend.dir);
|
|
151
154
|
if (!module)
|
|
152
|
-
throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `${join(backend.dir, "go.mod")}
|
|
155
|
+
throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `${join(backend.dir, "go.mod")} has no module line; backend must be a Go module.`);
|
|
153
156
|
const out = join(work, `api-${arch}.cgi`);
|
|
154
157
|
const ldflags = [
|
|
155
158
|
"-s",
|
|
@@ -157,6 +160,7 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
|
157
160
|
"-buildid=",
|
|
158
161
|
`-X main.packageID=${manifest.package}`,
|
|
159
162
|
`-X main.version=${manifest.spkVersion}`,
|
|
163
|
+
`-X main.requestTimeout=${requestTimeoutSeconds(backend)}`,
|
|
160
164
|
`-X ${module}/internal/api.expectedPermissionsDigest=${manifest.apiPermissionsDigest}`,
|
|
161
165
|
`-X ${SDK_MODULE}/api.expectedSDKPermissionsDigest=${manifest.apiSDKPermissionsDigest}`,
|
|
162
166
|
...delegateBuildFlags(manifest),
|
|
@@ -171,11 +175,11 @@ async function buildGoCgi(manifest, backend, arch, work, run) {
|
|
|
171
175
|
env: { ...process.env, ...goBuildEnv(arch) },
|
|
172
176
|
});
|
|
173
177
|
if (result.missing) {
|
|
174
|
-
throw new StandaloneBuildError("STANDALONE_GO_MISSING", "
|
|
178
|
+
throw new StandaloneBuildError("STANDALONE_GO_MISSING", "The go command was not found. Install Go 1.25 or later (https://go.dev/dl/), or package an existing executable with backend.binary.");
|
|
175
179
|
}
|
|
176
180
|
if (result.status !== 0) {
|
|
177
181
|
const fetchingSdk = result.stderr.includes(`${SDK_MODULE}@`) ? privateSdkHint() : "";
|
|
178
|
-
throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build
|
|
182
|
+
throw new StandaloneBuildError("STANDALONE_GO_BUILD_FAILED", `go build (${arch}) failed.\n${result.stderr}${fetchingSdk}`);
|
|
179
183
|
}
|
|
180
184
|
return out;
|
|
181
185
|
}
|
|
@@ -192,7 +196,7 @@ async function buildBackgroundWorker(manifest, arch, work, run) {
|
|
|
192
196
|
env: { ...process.env, ...goBuildEnv(arch) },
|
|
193
197
|
});
|
|
194
198
|
if (result.missing || result.status !== 0) {
|
|
195
|
-
throw new StandaloneBuildError("STANDALONE_BACKGROUND_BUILD_FAILED", `background worker
|
|
199
|
+
throw new StandaloneBuildError("STANDALONE_BACKGROUND_BUILD_FAILED", `background worker (${BACKGROUND_WORKER_MAIN}) build failed; this project's backend must contain that package.\n${result.stderr}`);
|
|
196
200
|
}
|
|
197
201
|
const problem = checkCgiBinary(work, out, arch);
|
|
198
202
|
if (problem)
|
|
@@ -18,7 +18,7 @@ export const relativeExec = (v) => typeof v === "string" && v.length <= 200
|
|
|
18
18
|
export function readDaemon(value, root, arches, fail) {
|
|
19
19
|
if (value === undefined)
|
|
20
20
|
return undefined;
|
|
21
|
-
const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon
|
|
21
|
+
const invalid = () => { fail({ code: "STANDALONE_DAEMON_INVALID", message: "daemon accepts only fixed exec, targets, args, env and timeouts; paths must be inside the project, and the environment does not accept loader/shell/system variables." }); return undefined; };
|
|
22
22
|
if (!record(value) || Object.keys(value).some(k => !fields.has(k)))
|
|
23
23
|
return invalid();
|
|
24
24
|
if (!relativeExec(value.exec))
|
|
@@ -85,7 +85,7 @@ export function copyDaemonPayload(manifest, arch, target, controller) {
|
|
|
85
85
|
const stat = lstatSync(from);
|
|
86
86
|
const rel = relative(realpathSync(manifest.root), realpathSync(from));
|
|
87
87
|
if (rel.startsWith("..") || isAbsolute(rel) || stat.isSymbolicLink())
|
|
88
|
-
invalid(`daemon payload
|
|
88
|
+
invalid(`A daemon payload path must not contain a symlink or escape the project: ${from}`);
|
|
89
89
|
if (stat.isDirectory()) {
|
|
90
90
|
mkdirSync(to, { recursive: true });
|
|
91
91
|
chmodSync(to, 0o755);
|
|
@@ -95,22 +95,22 @@ export function copyDaemonPayload(manifest, arch, target, controller) {
|
|
|
95
95
|
else if (stat.isFile()) {
|
|
96
96
|
total += stat.size;
|
|
97
97
|
if (total > 512 * 1024 * 1024 || (stat.mode & 0o6000))
|
|
98
|
-
invalid(`daemon payload
|
|
98
|
+
invalid(`The daemon payload does not accept setid bits or more than 512 MiB: ${from}`);
|
|
99
99
|
copyFileSync(from, to);
|
|
100
100
|
chmodSync(to, stat.mode & 0o111 ? 0o755 : 0o644);
|
|
101
101
|
}
|
|
102
102
|
else
|
|
103
|
-
invalid(`daemon payload
|
|
103
|
+
invalid(`The daemon payload accepts only regular files and directories: ${from}`);
|
|
104
104
|
};
|
|
105
105
|
try {
|
|
106
106
|
if (!lstatSync(source).isDirectory())
|
|
107
|
-
invalid("daemon.targets
|
|
107
|
+
invalid("daemon.targets must point at a directory.");
|
|
108
108
|
walk(source, join(target, "payload"));
|
|
109
109
|
}
|
|
110
110
|
catch (error) {
|
|
111
111
|
if (error instanceof StandaloneBuildError)
|
|
112
112
|
throw error;
|
|
113
|
-
invalid(
|
|
113
|
+
invalid(`Cannot read the daemon payload: ${String(error)}`);
|
|
114
114
|
}
|
|
115
115
|
const exec = join(source, daemon.exec);
|
|
116
116
|
const problem = checkCgiBinary(manifest.root, exec, arch);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EntryMode, StandaloneManifest } from "./manifest.js";
|
|
2
2
|
import { type AppTitle } from "../build-spk/ui-texts.js";
|
|
3
|
+
import { type PackageIcons } from "../build-spk/dsm-icons.js";
|
|
3
4
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
4
5
|
export declare const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
5
6
|
export declare function cgiUrl(packageId: string): string;
|
|
@@ -19,12 +20,16 @@ export declare function windowScriptName(manifest: Pick<StandaloneManifest, "bui
|
|
|
19
20
|
export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
|
|
20
21
|
export declare function appTitle(manifest: StandaloneManifest): AppTitle;
|
|
21
22
|
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
22
|
-
export declare function renderUiConfig(manifest: StandaloneManifest): string;
|
|
23
|
+
export declare function renderUiConfig(manifest: StandaloneManifest, icons?: PackageIcons): string;
|
|
23
24
|
/**
|
|
24
25
|
* The DSM AppWindow that hosts the project's UI. It loads app/app.js and
|
|
25
|
-
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call
|
|
26
|
-
* only transport: same-origin POST with DSM's session
|
|
26
|
+
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call(method,
|
|
27
|
+
* params, {signal})` is the only transport: same-origin POST with DSM's session
|
|
28
|
+
* cookie and SynoToken, given up past `ctx.requestTimeout` like the SDK's.
|
|
27
29
|
* `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
|
|
28
|
-
* `assetVersion` busts browser caches when the UI changes.
|
|
30
|
+
* `assetVersion` busts browser caches when the UI changes. A Portal/standalone
|
|
31
|
+
* launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
|
|
32
|
+
* (and add a device-width viewport), and `ctx.appIcon` is the package icon a
|
|
33
|
+
* full-page shell shows beside the name, where a DSM window has a title bar.
|
|
29
34
|
*/
|
|
30
|
-
export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string): string;
|
|
35
|
+
export declare function renderWindowScript(manifest: StandaloneManifest, assetVersion: string, icons?: PackageIcons): string;
|
|
@@ -6,8 +6,13 @@ import { permitsOrdinaryUsers } from "./api-permissions.js";
|
|
|
6
6
|
import { PORTAL_RUNTIME } from "./portal-window.js";
|
|
7
7
|
import { portalByExecutor } from "../build-spk/dsm-version.js";
|
|
8
8
|
import { uiTitleFields } from "../build-spk/ui-texts.js";
|
|
9
|
+
import { packageIcons, windowIconFile, windowIconTemplate } from "../build-spk/dsm-icons.js";
|
|
10
|
+
import { DEFAULT_WINDOW, FULL_PAGE_MIN_WIDTH } from "./window-size.js";
|
|
11
|
+
import { requestTimeoutSeconds } from "./request-timeout.js";
|
|
9
12
|
/** Same path the generated wrapper POSTs to, and the payload location of the CGI. */
|
|
10
13
|
export const CGI_PAYLOAD_PATH = "ui/cgi/api.cgi";
|
|
14
|
+
/** The SDK's clientTimeout(requestTimeout) waits this much past the backend deadline. */
|
|
15
|
+
const CLIENT_TIMEOUT_MARGIN = 2000;
|
|
11
16
|
export function cgiUrl(packageId) {
|
|
12
17
|
return `/webman/3rdparty/${packageId}/cgi/api.cgi`;
|
|
13
18
|
}
|
|
@@ -68,7 +73,8 @@ export function appTitle(manifest) {
|
|
|
68
73
|
return { displayname: manifest.displayname, description: manifest.description, info: manifest.i18n?.info };
|
|
69
74
|
}
|
|
70
75
|
/** `ui/config`: registers the DSM main-menu app and its window class. */
|
|
71
|
-
export function renderUiConfig(manifest) {
|
|
76
|
+
export function renderUiConfig(manifest, icons = packageIcons(manifest.icon)) {
|
|
77
|
+
const icon = windowIconTemplate(icons);
|
|
72
78
|
const ns = `SYNO.SDS.${manifest.package}`;
|
|
73
79
|
const titleFields = uiTitleFields(appTitle(manifest));
|
|
74
80
|
return `${JSON.stringify({
|
|
@@ -76,7 +82,7 @@ export function renderUiConfig(manifest) {
|
|
|
76
82
|
[`${ns}.Instance`]: {
|
|
77
83
|
type: "app",
|
|
78
84
|
...titleFields,
|
|
79
|
-
icon
|
|
85
|
+
icon,
|
|
80
86
|
appWindow: `${ns}.MainWindow`,
|
|
81
87
|
allowMultiInstance: false,
|
|
82
88
|
...launcherAccess(manifest),
|
|
@@ -86,7 +92,7 @@ export function renderUiConfig(manifest) {
|
|
|
86
92
|
type: "lib",
|
|
87
93
|
title: titleFields.title,
|
|
88
94
|
texts: titleFields.texts,
|
|
89
|
-
icon
|
|
95
|
+
icon,
|
|
90
96
|
depend: [],
|
|
91
97
|
},
|
|
92
98
|
},
|
|
@@ -94,37 +100,68 @@ export function renderUiConfig(manifest) {
|
|
|
94
100
|
}
|
|
95
101
|
/**
|
|
96
102
|
* The DSM AppWindow that hosts the project's UI. It loads app/app.js and
|
|
97
|
-
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call
|
|
98
|
-
* only transport: same-origin POST with DSM's session
|
|
103
|
+
* app/app.css once, then calls the app's mount(root, ctx). `ctx.call(method,
|
|
104
|
+
* params, {signal})` is the only transport: same-origin POST with DSM's session
|
|
105
|
+
* cookie and SynoToken, given up past `ctx.requestTimeout` like the SDK's.
|
|
99
106
|
* `ctx.t` / `ctx.locale` translate with the catalogs embedded at build time.
|
|
100
|
-
* `assetVersion` busts browser caches when the UI changes.
|
|
107
|
+
* `assetVersion` busts browser caches when the UI changes. A Portal/standalone
|
|
108
|
+
* launch owns the whole page: `ctx.fullPage` tells the app to lay out full-page
|
|
109
|
+
* (and add a device-width viewport), and `ctx.appIcon` is the package icon a
|
|
110
|
+
* full-page shell shows beside the name, where a DSM window has a title bar.
|
|
101
111
|
*/
|
|
102
|
-
export function renderWindowScript(manifest, assetVersion) {
|
|
112
|
+
export function renderWindowScript(manifest, assetVersion, icons = packageIcons(manifest.icon)) {
|
|
103
113
|
const pkg = manifest.package;
|
|
104
114
|
const ns = `SYNO.SDS.${pkg}`;
|
|
105
115
|
const p = JSON.stringify(pkg);
|
|
106
116
|
const root = manifest.executor ? renderRootParts(ns, manifest) : undefined;
|
|
117
|
+
const size = manifest.ui.window ?? DEFAULT_WINDOW;
|
|
118
|
+
const requestTimeout = requestTimeoutSeconds(manifest.backend) * 1000;
|
|
107
119
|
return `/* Generated by synopkgland build. Edit your UI in the project's ui folder. */
|
|
108
120
|
Ext.ns(${JSON.stringify(ns)});
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
// The SDK's clientTimeout: past the backend's request deadline
|
|
123
|
+
// (ctx.requestTimeout, from backend.timeout_seconds), so its request_timeout
|
|
124
|
+
// reply arrives first. options.signal aborts the fetch, for a page the user leaves.
|
|
125
|
+
${ns}.call = function (method, params, options, requestTimeout) {
|
|
126
|
+
var signal = options && options.signal;
|
|
127
|
+
var canceled = { code: "request_canceled", message: "The request was canceled" };
|
|
128
|
+
if (signal && signal.aborted) {
|
|
129
|
+
return Promise.reject(canceled);
|
|
130
|
+
}
|
|
111
131
|
var headers = { "Content-Type": "application/json" };
|
|
112
132
|
var token = SYNO.SDS.Session && SYNO.SDS.Session.SynoToken;
|
|
113
133
|
if (token) {
|
|
114
134
|
headers["X-SYNO-TOKEN"] = token;
|
|
115
135
|
}
|
|
136
|
+
var controller = new AbortController();
|
|
137
|
+
var failure = null;
|
|
138
|
+
var stop = function (error) {
|
|
139
|
+
failure = failure || error;
|
|
140
|
+
controller.abort();
|
|
141
|
+
};
|
|
142
|
+
var leave = function () { stop(canceled); };
|
|
143
|
+
var timer = setTimeout(function () {
|
|
144
|
+
stop({ code: "request_timeout", message: "The NAS did not answer in time" });
|
|
145
|
+
}, (requestTimeout || ${requestTimeout}) + ${CLIENT_TIMEOUT_MARGIN});
|
|
146
|
+
if (signal) signal.addEventListener("abort", leave);
|
|
147
|
+
var done = function () {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
if (signal) signal.removeEventListener("abort", leave);
|
|
150
|
+
};
|
|
116
151
|
return fetch(${JSON.stringify(cgiUrl(pkg))}, {
|
|
117
152
|
method: "POST",
|
|
118
153
|
credentials: "same-origin",
|
|
119
154
|
headers: headers,
|
|
120
|
-
body: JSON.stringify({ method: method, params: params || {} })
|
|
155
|
+
body: JSON.stringify({ method: method, params: params || {} }),
|
|
156
|
+
signal: controller.signal
|
|
121
157
|
}).then(function (response) {
|
|
122
158
|
return response.json().catch(function () {
|
|
123
159
|
return { success: false, error: { code: "bad_response", message: "HTTP " + response.status } };
|
|
124
160
|
});
|
|
125
161
|
}, function () {
|
|
126
|
-
return { success: false, error: { code: "network", message: "The NAS did not answer" } };
|
|
162
|
+
return { success: false, error: failure || { code: "network", message: "The NAS did not answer" } };
|
|
127
163
|
}).then(function (body) {
|
|
164
|
+
done();
|
|
128
165
|
if (body && body.success === true) {
|
|
129
166
|
return body.data;
|
|
130
167
|
}
|
|
@@ -173,6 +210,13 @@ ${root ? root.definitions : ""}${manifest.portal ? renderPortalDefinition(ns) :
|
|
|
173
210
|
document.head.appendChild(js);
|
|
174
211
|
};
|
|
175
212
|
|
|
213
|
+
// A Portal/standalone launch is the whole page, on a phone as well: no desktop
|
|
214
|
+
// minimum width, and the app lays itself out full-page.
|
|
215
|
+
${ns}.fullPage = function () {
|
|
216
|
+
try { return typeof _S === "function" && _S("standalone") === true; } catch (error) { return false; }
|
|
217
|
+
};
|
|
218
|
+
${ns}.appIcon = ${JSON.stringify(`webman/3rdparty/${pkg}/${windowIconFile(icons, 64)}`)};
|
|
219
|
+
|
|
176
220
|
// DSM reopens a window at the size it was last left at, even one saved on a
|
|
177
221
|
// larger screen, and only moves a window that does not fit: keep it inside.
|
|
178
222
|
${ns}.fitDesktop = function (size) {
|
|
@@ -191,7 +235,7 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
|
191
235
|
extend: "SYNO.SDS.AppWindow",
|
|
192
236
|
constructor: function (config) {
|
|
193
237
|
this.callParent([${ns}.fitDesktop(Ext.apply({
|
|
194
|
-
width:
|
|
238
|
+
width: ${size.width}, height: ${size.height}, minWidth: ${ns}.fullPage() ? ${FULL_PAGE_MIN_WIDTH} : ${size.minWidth}, minHeight: ${size.minHeight}, layout: "fit",
|
|
195
239
|
${root ? root.html : `html: '<div class="dsmapp-root" style="height:100%"></div>'`}
|
|
196
240
|
}, config))]);
|
|
197
241
|
},
|
|
@@ -212,6 +256,8 @@ Ext.define(${JSON.stringify(`${ns}.MainWindow`)}, {
|
|
|
212
256
|
me.reopenHandlers = me.reopenEvents.handlers;
|
|
213
257
|
var lang = "enu";
|
|
214
258
|
try { if (typeof _S === "function") lang = _S("lang"); } catch (error) {}
|
|
259
|
+
var ctx = { requestTimeout: ${requestTimeout} };
|
|
260
|
+
var call = function (method, params, options) { return ${ns}.call(method, params, options, ctx.requestTimeout); };
|
|
215
261
|
${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
216
262
|
if (me.mountToken !== mountToken) return;
|
|
217
263
|
me.mountPending = false;
|
|
@@ -221,18 +267,20 @@ ${root ? root.mount : ""} ${ns}.loadApp(function (app) {
|
|
|
221
267
|
me.hostAccess = ${ns}.createHost({ appWindow: me, locale: i18n.locale });
|
|
222
268
|
root.lang = i18n.locale;
|
|
223
269
|
try {
|
|
224
|
-
me.mounted = app.mount(root, {
|
|
270
|
+
me.mounted = app.mount(root, Ext.apply(ctx, {
|
|
225
271
|
displayName: ${JSON.stringify(manifest.displayname)},
|
|
226
|
-
call:
|
|
272
|
+
call: call,
|
|
227
273
|
files: {endpoint: ${JSON.stringify(cgiUrl(pkg))}, token: function () { return SYNO.SDS.Session && SYNO.SDS.Session.SynoToken; }},
|
|
228
274
|
reopen: me.reopenEvents.subscribe,
|
|
229
275
|
user: ${ns}.sessionUser(),${manifest.portal ? `\n portal: ${renderPortalAccess(ns, manifest)},` : ""}
|
|
230
276
|
host: me.hostAccess,
|
|
231
277
|
appWindow: me,
|
|
278
|
+
fullPage: ${ns}.fullPage(),
|
|
279
|
+
appIcon: ${ns}.appIcon,
|
|
232
280
|
lang: lang,
|
|
233
281
|
locale: i18n.locale,
|
|
234
282
|
t: i18n.t${root ? `,\n executor: access` : ""}
|
|
235
|
-
}) || {};
|
|
283
|
+
})) || {};
|
|
236
284
|
} catch (error) {
|
|
237
285
|
me.hostAccess.dispose();
|
|
238
286
|
me.hostAccess = null;
|
|
@@ -283,7 +331,7 @@ function renderPortalAccess(ns, manifest) {
|
|
|
283
331
|
defaultAlias: ${JSON.stringify(manifest.portal.alias)},${portalByExecutor(manifest.os_min_ver) ? "\n entry: true," : ""}
|
|
284
332
|
isAdmin: function () { return ${ns}.sessionUser().isAdmin; },
|
|
285
333
|
send: ${ns}.webApi(me),
|
|
286
|
-
call:
|
|
334
|
+
call: call,
|
|
287
335
|
launch: function (app, params) { SYNO.SDS.AppLaunch(app, params); },
|
|
288
336
|
location: window.location,
|
|
289
337
|
locale: ${ns}.createI18n(lang, ${JSON.stringify(Object.fromEntries(Object.keys(DSM_LANGS).map((locale) => [locale, {}])))}).locale
|
|
@@ -357,7 +405,7 @@ ${PASSWORD_CONFIRM}
|
|
|
357
405
|
user: session,
|
|
358
406
|
send: ${ns}.webApi(me),
|
|
359
407
|
confirmPassword: ${ns}.createPasswordConfirm(me),
|
|
360
|
-
call:
|
|
408
|
+
call: call,
|
|
361
409
|
wait: function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
362
410
|
});
|
|
363
411
|
${settingsOnlyApproval(manifest) ? "" : ` var panels = me.body && me.body.dom.querySelector(".dsmapp-rootpanels");
|
package/dist/standalone/i18n.js
CHANGED
|
@@ -23,15 +23,15 @@ export function readInfoTranslations(value, check) {
|
|
|
23
23
|
return {};
|
|
24
24
|
}
|
|
25
25
|
if (!isRecord(value)) {
|
|
26
|
-
return check.invalid("
|
|
26
|
+
return check.invalid("It must be keyed by language, for example zh-TW: { displayname, description }.");
|
|
27
27
|
}
|
|
28
28
|
const info = {};
|
|
29
29
|
for (const [locale, text] of Object.entries(value)) {
|
|
30
30
|
if (!isLocale(locale) || locale === "en") {
|
|
31
|
-
return check.invalid(
|
|
31
|
+
return check.invalid(`"${locale}" is not a supported translation language; English goes in the top-level displayname / description.`);
|
|
32
32
|
}
|
|
33
33
|
if (!isRecord(text) || Object.keys(text).some((key) => !INFO_TEXT_FIELDS.has(key))) {
|
|
34
|
-
return check.invalid(`.${locale}
|
|
34
|
+
return check.invalid(`.${locale} accepts only displayname and description.`);
|
|
35
35
|
}
|
|
36
36
|
const entry = {};
|
|
37
37
|
for (const field of INFO_TEXT_FIELDS) {
|
|
@@ -66,14 +66,14 @@ function readCatalog(file, locale) {
|
|
|
66
66
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
|
-
return invalid(
|
|
69
|
+
return invalid(`is not valid JSON: ${String(error)}`);
|
|
70
70
|
}
|
|
71
71
|
if (!isRecord(raw)) {
|
|
72
|
-
return invalid("
|
|
72
|
+
return invalid("It must be a { \"key\": \"text\" } JSON object.");
|
|
73
73
|
}
|
|
74
74
|
for (const [key, text] of Object.entries(raw)) {
|
|
75
75
|
if (!MESSAGE_KEY.test(key) || typeof text !== "string") {
|
|
76
|
-
return invalid(
|
|
76
|
+
return invalid(`"${key}" must be a dot-separated key (for example greeting.save) mapped to a text.`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
return { catalog: raw };
|
|
@@ -88,7 +88,7 @@ export function loadCatalogs(dir) {
|
|
|
88
88
|
if (!isLocale(locale)) {
|
|
89
89
|
errors.push({
|
|
90
90
|
code: "STANDALONE_I18N_LOCALE_UNSUPPORTED",
|
|
91
|
-
message: `${name}
|
|
91
|
+
message: `${name}: only ${LOCALES.join(", ")} are supported; every other DSM language shows English.`,
|
|
92
92
|
});
|
|
93
93
|
continue;
|
|
94
94
|
}
|
|
@@ -103,7 +103,7 @@ export function loadCatalogs(dir) {
|
|
|
103
103
|
const en = catalogs.en;
|
|
104
104
|
if (!en) {
|
|
105
105
|
if (!errors.some((e) => e.code === "STANDALONE_I18N_CATALOG_INVALID")) {
|
|
106
|
-
errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir}
|
|
106
|
+
errors.push({ code: "STANDALONE_I18N_MESSAGES_MISSING", message: `${dir} is missing en.json (the fallback text for every language).` });
|
|
107
107
|
}
|
|
108
108
|
return { ok: false, errors };
|
|
109
109
|
}
|
|
@@ -117,30 +117,30 @@ function compareWithEnglish(locale, catalog, en) {
|
|
|
117
117
|
const extra = Object.keys(catalog).filter((key) => !(key in en));
|
|
118
118
|
if (missing.length > 0 || extra.length > 0) {
|
|
119
119
|
const parts = [
|
|
120
|
-
missing.length > 0 ?
|
|
121
|
-
extra.length > 0 ?
|
|
120
|
+
missing.length > 0 ? `missing ${missing.join(", ")}` : "",
|
|
121
|
+
extra.length > 0 ? `extra ${extra.join(", ")}` : "",
|
|
122
122
|
].filter(Boolean);
|
|
123
|
-
return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json
|
|
123
|
+
return [{ code: "STANDALONE_I18N_KEYS_MISMATCH", message: `${locale}.json keys differ from en.json: ${parts.join("; ")}.` }];
|
|
124
124
|
}
|
|
125
125
|
const drifted = Object.keys(en).filter((key) => placeholders(catalog[key]) !== placeholders(en[key]));
|
|
126
126
|
return drifted.length === 0
|
|
127
127
|
? []
|
|
128
128
|
: [{
|
|
129
129
|
code: "STANDALONE_I18N_PLACEHOLDER_MISMATCH",
|
|
130
|
-
message: `${locale}.json
|
|
130
|
+
message: `${locale}.json has ${drifted.join(", ")} with placeholders that differ from en.json.`,
|
|
131
131
|
}];
|
|
132
132
|
}
|
|
133
133
|
function readGlossary(file) {
|
|
134
|
-
const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}
|
|
134
|
+
const invalid = (why) => ({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `${file}: ${why}` });
|
|
135
135
|
let raw;
|
|
136
136
|
try {
|
|
137
137
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
138
138
|
}
|
|
139
139
|
catch (error) {
|
|
140
|
-
return invalid(
|
|
140
|
+
return invalid(`is not valid JSON: ${String(error)}`);
|
|
141
141
|
}
|
|
142
142
|
if (!isRecord(raw) || !Array.isArray(raw.terms)) {
|
|
143
|
-
return invalid("
|
|
143
|
+
return invalid("It must be { \"terms\": [...] }.");
|
|
144
144
|
}
|
|
145
145
|
const terms = [];
|
|
146
146
|
for (const term of raw.terms) {
|
|
@@ -150,7 +150,7 @@ function readGlossary(file) {
|
|
|
150
150
|
&& Object.entries(term.text).every(([locale, text]) => isLocale(locale) && typeof text === "string" && text !== "")
|
|
151
151
|
&& (term.keys === undefined || (Array.isArray(term.keys) && term.keys.every((key) => typeof key === "string")));
|
|
152
152
|
if (!ok) {
|
|
153
|
-
return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"}
|
|
153
|
+
return invalid(`term ${isRecord(term) ? JSON.stringify(term.id) : "?"} needs id, text keyed by a supported language, and an optional keys list.`);
|
|
154
154
|
}
|
|
155
155
|
terms.push({ id: term.id, text: term.text, keys: term.keys ?? [] });
|
|
156
156
|
}
|
|
@@ -173,7 +173,7 @@ export function checkGlossary(file, catalogs) {
|
|
|
173
173
|
for (const term of terms) {
|
|
174
174
|
for (const key of term.keys) {
|
|
175
175
|
if (!(key in catalogs.en)) {
|
|
176
|
-
errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id}
|
|
176
|
+
errors.push({ code: "STANDALONE_I18N_GLOSSARY_INVALID", message: `glossary term ${term.id} points at a key that does not exist: ${key}.` });
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
for (const [locale, catalog] of Object.entries(catalogs)) {
|
|
@@ -181,7 +181,7 @@ export function checkGlossary(file, catalogs) {
|
|
|
181
181
|
if (official !== undefined && !containsTerm(catalog[key], official, locale)) {
|
|
182
182
|
errors.push({
|
|
183
183
|
code: "STANDALONE_I18N_GLOSSARY_MISMATCH",
|
|
184
|
-
message: `${locale}.json
|
|
184
|
+
message: `${locale}.json ${key} "${catalog[key]}" does not use the Synology term "${official}" (glossary term ${term.id}).`,
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type Payload = Array<{
|
|
2
|
+
path: string;
|
|
3
|
+
mode: string;
|
|
4
|
+
}>;
|
|
5
|
+
export interface BridgeJson {
|
|
6
|
+
runtime?: string;
|
|
7
|
+
entry?: string;
|
|
8
|
+
methods?: string[];
|
|
9
|
+
node?: {
|
|
10
|
+
package?: string;
|
|
11
|
+
};
|
|
12
|
+
capabilities?: {
|
|
13
|
+
root?: boolean;
|
|
14
|
+
delegate?: {
|
|
15
|
+
executor_digest?: string;
|
|
16
|
+
policy_digest?: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A Python, Node or Rust App: the runtime CGI binds bridge.json and
|
|
22
|
+
* permissions.json by digest, the App lives in target/app (never under ui/),
|
|
23
|
+
* and its entry, SDK and dependencies are in the package.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkBridgeApp(dir: string, backend: string, info: Record<string, string>, payload: Payload, findings: string[]): Promise<BridgeJson | undefined>;
|
|
26
|
+
/** root and delegate reach the package's executor; delegate pins the one this SPK ships. */
|
|
27
|
+
export declare function checkBridgeExecutor(bridge: BridgeJson, executor: {
|
|
28
|
+
executorDigest: string;
|
|
29
|
+
policyDigest: string;
|
|
30
|
+
} | undefined, findings: string[]): void;
|
|
31
|
+
export {};
|