@synopackageland/cli 0.9.7 → 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 +121 -0
- 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 -96
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,126 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
Builds with SDK 0.10.0 and templates 0.10.0: `init` writes SDK v0.10.0 (Go
|
|
6
|
+
module, Rust crate tag and `backend.sdk`), `@synopackageland/sdk` 0.10.0 and
|
|
7
|
+
`@synopackageland/ui` 0.9.4, and the Vue templates use the SDK's `AppShell`.
|
|
8
|
+
CLI 0.10 builds only SDK 0.10.x: the executor digest table records SDK
|
|
9
|
+
0.10.0 and no longer lists 0.9.x, so an App on SDK 0.9.x is refused
|
|
10
|
+
(`SDK_UNSUPPORTED`) and stays on CLI 0.9.7. SDK 0.10.0's executor changed, so
|
|
11
|
+
a rebuilt App carries new executor and lifecycle digests and its pins are
|
|
12
|
+
approved again; the policy digest changes only for a policy that uses root
|
|
13
|
+
program settings, jobs, `stop` or `state`.
|
|
14
|
+
|
|
15
|
+
繁中摘要:使用 SDK 0.10.0 與 templates 0.10.0(UI 0.9.4),Vue 範本改用 SDK 的
|
|
16
|
+
`AppShell`;新增 Python、Node、Rust App(bridge runtime)、root programs 設定、
|
|
17
|
+
`ui.window`、`backend.timeout_seconds`(預設 60 秒),CLI 輸出全改英文。CLI 0.10
|
|
18
|
+
只接受 SDK 0.10.x,digest 表移除 0.9.x;executor 變更,重建的 App 要重新核准
|
|
19
|
+
executor 與 lifecycle digest。
|
|
20
|
+
|
|
21
|
+
- Breaking: every CLI message is English. Error descriptions and suggestions,
|
|
22
|
+
`--help` text, and check, inspect, build and deploy output no longer carry
|
|
23
|
+
Chinese. Only the multilingual data shipped to DSM end users keeps its
|
|
24
|
+
translations: the install/upgrade/uninstall wizard copy, the App UI catalogs
|
|
25
|
+
and the INFO `displayname`/`description` translations. A test now fails on
|
|
26
|
+
CJK in any other `src/` file.
|
|
27
|
+
- Behavior change: every App's backend request deadline is 60 seconds instead
|
|
28
|
+
of the SDK's 20. `bridge.json` always carries `timeout_seconds`, so the
|
|
29
|
+
runtime's deadline and the `deadline_ms` it sends the App follow it; a Go
|
|
30
|
+
CGI gets it as `-X main.requestTimeout` and keeps the SDK's 20 seconds
|
|
31
|
+
until its main reads that variable into `api.Timeout`. The DSM window sets
|
|
32
|
+
`ctx.requestTimeout` to the deadline and gives up two seconds past it (62
|
|
33
|
+
seconds instead of 22), also for a prebuilt binary CGI.
|
|
34
|
+
- `backend.timeout_seconds` (1–3600) sets another deadline for go, python,
|
|
35
|
+
node and rust backends. A Go main that does not read `requestTimeout` into
|
|
36
|
+
`api.Timeout` is refused (`STANDALONE_TIMEOUT_UNSUPPORTED`); a binary
|
|
37
|
+
backend refuses it.
|
|
38
|
+
- `ui.window: {width, height, minWidth, minHeight}` sets the DSM window's
|
|
39
|
+
size; omitted fields keep 960×640, at least 700×420, and a standalone
|
|
40
|
+
launch keeps its 320 px minimum width. Sizes outside 320–3840 × 240–2160,
|
|
41
|
+
or a minimum larger than the size, are refused
|
|
42
|
+
(`STANDALONE_UI_WINDOW_INVALID`).
|
|
43
|
+
- The generated DSM window knows a standalone (Portal) launch,
|
|
44
|
+
`_S("standalone") === true`: its minimum width drops from 700 to 320 px so
|
|
45
|
+
a phone can show it, and `mount` gets `ctx.fullPage` and `ctx.appIcon` (the
|
|
46
|
+
package's 64 px window icon) for a full-page layout and brand.
|
|
47
|
+
- `init -t native-python | native-node | native-rust` creates the native-go
|
|
48
|
+
App with a Python, Node.js (TypeScript) or Rust backend: the Go template's
|
|
49
|
+
UI, frontend and root program, the language's capability overlays for
|
|
50
|
+
`--with jobs,root-ops,delegate` (jobs also declares `files: session`, which
|
|
51
|
+
its example reads; delegate opens `app.status` and `report.save` to
|
|
52
|
+
signed-in users; Rust lists the added methods in `backend.methods`), and the
|
|
53
|
+
App SDK its tests import in `backend/vendor`. `--shared-folder` stays
|
|
54
|
+
native-go only. Capability overlays carry their permission rules.
|
|
55
|
+
- Breaking: `backend.kind: rust` (the Rust full SDK) is removed, and
|
|
56
|
+
`init -t native-rust` now creates the thin Rust App behind the bridge
|
|
57
|
+
runtime instead.
|
|
58
|
+
- `backend.runtime: python | node | rust` builds an App in another language
|
|
59
|
+
behind the SDK's Go runtime (spec/bridge/v1). `build` compiles
|
|
60
|
+
`bridge/cmd/runtime` of the App's SDK release as `ui/cgi/api.cgi` with the
|
|
61
|
+
sha256 of `bridge.json` and `permissions.json` bound, and ships the App in
|
|
62
|
+
`target/app`: Python with the SDK in `vendor/`, Node with the SDK in
|
|
63
|
+
`node_modules/@synopackageland/app` and an `install_dep_packages` on
|
|
64
|
+
Package Center's Node.js, Rust cross-compiled with `cargo zigbuild`.
|
|
65
|
+
Methods come from `backend.methods` or the App's `--describe`. `inspect`
|
|
66
|
+
checks the manifest schema, the digests, the entry, that the App is not
|
|
67
|
+
under `ui/`, and the Node dependencies. The `bridge` gate builds all three
|
|
68
|
+
for every architecture.
|
|
69
|
+
- Bridge Apps may declare `jobs`, `rootOps`, `portal` and an
|
|
70
|
+
`until-revoked` `delegate` for `files.moveFromStaging`, plus
|
|
71
|
+
`backend.transfers` and `backend.max_upload_bytes`; `bridge.json` carries
|
|
72
|
+
them. With jobs or delegate the runtime is also installed as
|
|
73
|
+
`target/background/worker` and DSM runs it with `--worker` as the package
|
|
74
|
+
account. `inspect` refuses delegate digests that are not the SPK
|
|
75
|
+
executor's. The Node SDK is copied as its `package.json` and `dist/`, so
|
|
76
|
+
the SDK checkout must be built.
|
|
77
|
+
- `root` in dsm-package.yaml configures root programs for the executor's
|
|
78
|
+
root programs, root jobs, package stop step and root state directory:
|
|
79
|
+
`programs.<name>` gives a params `schema` and `run` or `job` limits,
|
|
80
|
+
`kind: binary` takes `root/<arch>/<name>` (an ELF for each architecture,
|
|
81
|
+
so the policy digest differs per architecture), `stop` names the program
|
|
82
|
+
the executor runs at package stop and upgrade, and `state: true` asks for
|
|
83
|
+
the root state directory. Programs without settings keep the bare digest,
|
|
84
|
+
so existing policies and their digests do not change. Jobs add
|
|
85
|
+
`root.start` and the `root.jobs` operations. A build whose SDK executor
|
|
86
|
+
predates these refuses them (`EXECUTOR_ROOT_PROGRAMS_UNSUPPORTED`).
|
|
87
|
+
- `preupgrade` stops the executor with `executor/lifecycle stop --upgrade`;
|
|
88
|
+
an installed helper without the flag gets a plain stop.
|
|
89
|
+
|
|
90
|
+
- The DSM window icons are named after their content,
|
|
91
|
+
`images/app_<hash>_<size>.png`, and `ui/config` points at that name. DSM and
|
|
92
|
+
the browser cache an App icon by its URL, so after an icon change DSM kept
|
|
93
|
+
showing the old one (the blue square in t-0391) until the cache expired; a
|
|
94
|
+
new icon now has a new URL, and an unchanged icon keeps its cached one. DSM
|
|
95
|
+
7.4 passes the config's icon path, with `{0}` for the size, to
|
|
96
|
+
`SYNO.Core.Synohdpack getHDIcon` for every `webman/3rdparty/` App, so any
|
|
97
|
+
file name works.
|
|
98
|
+
- `ctx.call(method, params, {signal})` aborts its fetch when the signal does,
|
|
99
|
+
so a page the user leaves really stops its request, and rejects
|
|
100
|
+
`{code: "request_canceled"}`. It also gives up after the SDK's
|
|
101
|
+
`clientTimeout(ctx.requestTimeout)`, two seconds past the backend deadline
|
|
102
|
+
(the backend deadline unless the App sets `ctx.requestTimeout` in ms, read
|
|
103
|
+
on every call), aborts the fetch and rejects `{code: "request_timeout"}`.
|
|
104
|
+
Before, a call waited as long as the browser did. The portal and root
|
|
105
|
+
panels use the same call.
|
|
106
|
+
- preuninst waits for the App's uninstall hook by looking it up in `/proc`
|
|
107
|
+
instead of `kill -0`. DSM's bash logs a failed builtin to syslog even with
|
|
108
|
+
`2>/dev/null`, so every uninstall with a hook logged `BASH_ERR ... kill: No
|
|
109
|
+
such process` twice (t-0383 B8). The wait, the timeout and the result are
|
|
110
|
+
unchanged; a host without `/proc` still uses `kill -0`.
|
|
111
|
+
- preuninst stops an uninstall hook past its timeout, and prestart waits for
|
|
112
|
+
a stale package user manager to exit, without a `kill` that can fail: the
|
|
113
|
+
hook's process group and the manager are looked up in `/proc` first, so
|
|
114
|
+
neither logs `BASH_ERR ... kill: No such process` once the process has
|
|
115
|
+
exited.
|
|
116
|
+
- The Application Portal panel's Italian instructions use the infinitive like
|
|
117
|
+
DSM ("Riprovare o usare il Pannello di controllo."), the same text as the
|
|
118
|
+
SDK's `i18n/portal` catalogs.
|
|
119
|
+
- `ctx.portal.status()` waits for a register or unregister in progress to
|
|
120
|
+
end, whatever its result, like the SDK's `createPortalAccess`. Before, a
|
|
121
|
+
status read during a slow change could show "The Application Portal entry
|
|
122
|
+
was not fully removed".
|
|
123
|
+
|
|
3
124
|
## 0.9.7 (2026-10-13)
|
|
4
125
|
|
|
5
126
|
Builds with SDK 0.9.8 and templates 0.9.7: `init` writes SDK v0.9.8 (Go
|
package/README.md
CHANGED
|
@@ -34,12 +34,17 @@ the SPK with Package Center's Manual Install.
|
|
|
34
34
|
| Template | Use it for | Manifest |
|
|
35
35
|
| --- | --- | --- |
|
|
36
36
|
| `native-go` | A Go backend with a Vue UI (or `--ui plain`) | `dsm-package.yaml` |
|
|
37
|
+
| `native-python`, `native-node`, `native-rust` | The `native-go` App with a Python, Node.js (TypeScript) or Rust backend | `dsm-package.yaml` |
|
|
37
38
|
| `binary` | A static Linux program you build yourself | `dsm-package.yaml` |
|
|
38
39
|
| `compose` | An existing Docker Compose app (`--from compose.yaml`) | `synology-app.yaml` |
|
|
39
40
|
|
|
40
41
|
Existing native products with their own payload layout use `backend: {kind: native}` in
|
|
41
42
|
`dsm-package.yaml`.
|
|
42
43
|
|
|
44
|
+
A backend in Python, Node or Rust uses `backend: {runtime: python|node|rust, ...}`: the CGI is
|
|
45
|
+
the SDK's Go runtime, which checks the DSM session and `permissions.json` and starts the App
|
|
46
|
+
from `target/app` as the package account for each request (see `skills/reference/create.md`).
|
|
47
|
+
|
|
43
48
|
## SDK and templates
|
|
44
49
|
|
|
45
50
|
The CLI packages, deploys and checks. The app-side code is the SDK
|
package/dist/add.js
CHANGED
|
@@ -11,7 +11,6 @@ import { API_PERMISSIONS_FILE } from './standalone/api-permissions.js';
|
|
|
11
11
|
import { DEFAULT_MIN_DSM, isDsmAtLeast, PORTAL_EXECUTOR_MIN_DSM } from './build-spk/dsm-version.js';
|
|
12
12
|
import { loadStandaloneManifest } from './standalone/manifest.js';
|
|
13
13
|
import { PORTAL_REQUIRES_EXECUTOR } from './portal/alias.js';
|
|
14
|
-
const NATIVE_TEMPLATE = { go: 'native-go', rust: 'native-rust' };
|
|
15
14
|
function files(root, dir = root) {
|
|
16
15
|
if (!existsSync(dir))
|
|
17
16
|
return [];
|
|
@@ -61,7 +60,7 @@ function addPortal(root, result) {
|
|
|
61
60
|
if (loaded.manifest.portal)
|
|
62
61
|
return { ...result, unchanged: [...result.unchanged, 'portal'] };
|
|
63
62
|
if (!safePath(root, 'dsm-package.yaml'))
|
|
64
|
-
throw new Error('[ADD_FILE_CONFLICT]
|
|
63
|
+
throw new Error('[ADD_FILE_CONFLICT] The existing file differs from the SDK overlay: dsm-package.yaml; move or merge the conflict first, nothing is overwritten.');
|
|
65
64
|
const manifestPath = join(root, 'dsm-package.yaml');
|
|
66
65
|
const { values, notes } = raiseOsMinVer(loaded.manifest.os_min_ver, PORTAL_EXECUTOR_MIN_DSM, 'portal');
|
|
67
66
|
writeFileSync(manifestPath, setTopLevel(readFileSync(manifestPath, 'utf8'), { ...values, portal: {} }));
|
|
@@ -89,18 +88,18 @@ export function addCapabilities(root, requested) {
|
|
|
89
88
|
if (!loaded.ok)
|
|
90
89
|
throw new Error(loaded.errors.map(e => `[${e.code}] ${e.message}`).join('\n'));
|
|
91
90
|
const m = loaded.manifest;
|
|
92
|
-
if (m.backend.kind
|
|
93
|
-
throw new Error('[CAPABILITY_UNSUPPORTED] add
|
|
94
|
-
const template =
|
|
91
|
+
if (m.backend.kind !== 'go')
|
|
92
|
+
throw new Error('[CAPABILITY_UNSUPPORTED] add needs backend.kind: go.');
|
|
93
|
+
const template = 'native-go';
|
|
95
94
|
const existing = Object.entries(capabilitySettings(names)).filter(([key]) => key in m.capabilities).map(([key]) => key);
|
|
96
95
|
const unchanged = names.filter(name => existing.includes(Object.keys(capabilitySettings([name]))[0]));
|
|
97
96
|
const added = names.filter(name => !unchanged.includes(name));
|
|
98
97
|
if (!added.length)
|
|
99
98
|
return { added, unchanged };
|
|
100
99
|
if (m.daemon && added.includes('jobs') && !added.includes('delegate') && m.capabilities.delegate?.lifetime !== 'until-revoked')
|
|
101
|
-
throw new Error('[CAPABILITY_COMBINATION_INVALID] daemon
|
|
100
|
+
throw new Error('[CAPABILITY_COMBINATION_INVALID] daemon can only coexist with an until-revoked delegate fixed worker.');
|
|
102
101
|
if (m.backend.dir !== join(root, 'backend'))
|
|
103
|
-
throw new Error('[ADD_LAYOUT_UNSUPPORTED] SDK overlay
|
|
102
|
+
throw new Error('[ADD_LAYOUT_UNSUPPORTED] The SDK overlay needs backend.dir: backend.');
|
|
104
103
|
const work = mkdtempSync(join(tmpdir(), 'synopkgland-add-'));
|
|
105
104
|
try {
|
|
106
105
|
// Generate SDK files using init's identical substitutions, without touching app-owned files.
|
|
@@ -130,7 +129,7 @@ export function addCapabilities(root, requested) {
|
|
|
130
129
|
if (!safePath(root, 'dsm-package.yaml'))
|
|
131
130
|
conflicts.push('dsm-package.yaml');
|
|
132
131
|
if (conflicts.length)
|
|
133
|
-
throw new Error(`[ADD_FILE_CONFLICT]
|
|
132
|
+
throw new Error(`[ADD_FILE_CONFLICT] Existing files differ from the SDK overlay: ${conflicts.join(', ')}; move or merge the conflict first, nothing is overwritten.`);
|
|
134
133
|
const text = readFileSync(manifestPath, 'utf8');
|
|
135
134
|
const capabilities = { ...parse(text).capabilities, ...nativeCapabilitySettings(added) };
|
|
136
135
|
// rootOps needs DSM 7.1-42661 or later.
|
|
@@ -32,7 +32,7 @@ export function parseArchList(value) {
|
|
|
32
32
|
const families = [];
|
|
33
33
|
for (const item of value.split(",").map((part) => part.trim())) {
|
|
34
34
|
if (!isNativeArchFamily(item)) {
|
|
35
|
-
throw new Error(`--arch
|
|
35
|
+
throw new Error(`--arch accepts only ${NATIVE_ARCH_FAMILIES.join(", ")} (list several with commas), got "${value}".`);
|
|
36
36
|
}
|
|
37
37
|
if (!families.includes(item)) {
|
|
38
38
|
families.push(item);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Sizes DSM loads for
|
|
1
|
+
/** Sizes DSM loads for the window icon template (main menu, desktop, window, taskbar). */
|
|
2
2
|
export declare const WINDOW_ICON_SIZES: readonly [16, 24, 32, 48, 64, 72, 256];
|
|
3
3
|
/** The package source drops a release whose PACKAGE_ICON is larger than this. */
|
|
4
4
|
export declare const MAX_ICON_BYTES: number;
|
|
@@ -27,5 +27,12 @@ export interface IconReport {
|
|
|
27
27
|
solid: boolean;
|
|
28
28
|
}
|
|
29
29
|
export declare function iconReport(root: string, icon: string | undefined): IconReport;
|
|
30
|
+
/**
|
|
31
|
+
* `ui/config`'s icon, relative to ui/: DSM puts the size in for {0}. DSM and
|
|
32
|
+
* the browser cache an icon by its URL, so the name carries the icons' content
|
|
33
|
+
* and a changed icon is loaded again.
|
|
34
|
+
*/
|
|
35
|
+
export declare function windowIconTemplate(icons: PackageIcons): string;
|
|
36
|
+
export declare function windowIconFile(icons: PackageIcons, size: number): string;
|
|
30
37
|
export declare function writeDsmWindowIcons(uiDir: string, icons: PackageIcons): void;
|
|
31
38
|
export declare function writePackageCenterIcons(spkRoot: string, icons: PackageIcons): void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { decodePng, encodePng } from "./png.js";
|
|
5
|
-
/** Sizes DSM loads for
|
|
6
|
+
/** Sizes DSM loads for the window icon template (main menu, desktop, window, taskbar). */
|
|
6
7
|
export const WINDOW_ICON_SIZES = [16, 24, 32, 48, 64, 72, 256];
|
|
7
8
|
/** The package source drops a release whose PACKAGE_ICON is larger than this. */
|
|
8
9
|
export const MAX_ICON_BYTES = 256 * 1024;
|
|
@@ -14,7 +15,7 @@ function readPng(path, name) {
|
|
|
14
15
|
return decodePng(readFileSync(path));
|
|
15
16
|
}
|
|
16
17
|
catch (error) {
|
|
17
|
-
throw new Error(`${name}
|
|
18
|
+
throw new Error(`${name}: ${error.message}`);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
/** Area-average downscale in premultiplied alpha, so transparent edges stay clean. */
|
|
@@ -78,24 +79,24 @@ function readIconDirectory(dir) {
|
|
|
78
79
|
continue;
|
|
79
80
|
const size = Number(match[1]);
|
|
80
81
|
if (!WINDOW_ICON_SIZES.includes(size)) {
|
|
81
|
-
throw new Error(`${name}
|
|
82
|
+
throw new Error(`${name}: unsupported size; only ${WINDOW_ICON_SIZES.map((s) => `icon_${s}.png`).join(", ")} are accepted.`);
|
|
82
83
|
}
|
|
83
84
|
const image = readPng(join(dir, name), name);
|
|
84
85
|
if (image.width !== size || image.height !== size) {
|
|
85
|
-
throw new Error(`${name}
|
|
86
|
+
throw new Error(`${name}: the size is ${image.width}x${image.height}; it should be ${size}x${size}.`);
|
|
86
87
|
}
|
|
87
88
|
drawn.set(size, image);
|
|
88
89
|
}
|
|
89
90
|
if (!drawn.has(256))
|
|
90
|
-
throw new Error(
|
|
91
|
+
throw new Error(`Icon directory ${basename(dir)}/ is missing icon_256.png (a 256x256 PNG; other sizes may be omitted and are generated by scaling it down).`);
|
|
91
92
|
return drawn;
|
|
92
93
|
}
|
|
93
94
|
function readIconFile(path) {
|
|
94
95
|
const image = readPng(path, basename(path));
|
|
95
96
|
if (image.width !== image.height)
|
|
96
|
-
throw new Error(`${basename(path)}
|
|
97
|
+
throw new Error(`${basename(path)}: the size ${image.width}x${image.height} is not square.`);
|
|
97
98
|
if (image.width < 256)
|
|
98
|
-
throw new Error(`${basename(path)}
|
|
99
|
+
throw new Error(`${basename(path)}: the size ${image.width}x${image.height} must be at least 256x256.`);
|
|
99
100
|
return new Map([[256, scale(image, 256)]]);
|
|
100
101
|
}
|
|
101
102
|
function render(drawn, source) {
|
|
@@ -104,7 +105,7 @@ function render(drawn, source) {
|
|
|
104
105
|
for (const size of WINDOW_ICON_SIZES) {
|
|
105
106
|
const png = encodePng(size, size, scale(drawn.get(size) ?? big, size).rgba);
|
|
106
107
|
if (png.length > MAX_ICON_BYTES) {
|
|
107
|
-
throw new Error(
|
|
108
|
+
throw new Error(`The encoded ${size}x${size} icon is ${png.length} bytes, over the package source limit of 256 KiB; reduce noise or detail.`);
|
|
108
109
|
}
|
|
109
110
|
window.set(size, png);
|
|
110
111
|
}
|
|
@@ -113,7 +114,7 @@ function render(drawn, source) {
|
|
|
113
114
|
/** A project icon: one square PNG of at least 256 px, or a directory of icon_<size>.png. */
|
|
114
115
|
export function renderPackageIcons(path) {
|
|
115
116
|
if (!existsSync(path))
|
|
116
|
-
throw new Error(
|
|
117
|
+
throw new Error(`Icon ${basename(path)} was not found.`);
|
|
117
118
|
return render(statSync(path).isDirectory() ? readIconDirectory(path) : readIconFile(path), "custom");
|
|
118
119
|
}
|
|
119
120
|
let defaults;
|
|
@@ -134,11 +135,24 @@ function writeIcon(path, png) {
|
|
|
134
135
|
writeFileSync(path, png);
|
|
135
136
|
chmodSync(path, 0o644);
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* `ui/config`'s icon, relative to ui/: DSM puts the size in for {0}. DSM and
|
|
140
|
+
* the browser cache an icon by its URL, so the name carries the icons' content
|
|
141
|
+
* and a changed icon is loaded again.
|
|
142
|
+
*/
|
|
143
|
+
export function windowIconTemplate(icons) {
|
|
144
|
+
const hash = createHash("sha256");
|
|
145
|
+
for (const [size, png] of icons.window)
|
|
146
|
+
hash.update(`${size}:${png.length}:`).update(png);
|
|
147
|
+
return `images/app_${hash.digest("hex").slice(0, 12)}_{0}.png`;
|
|
148
|
+
}
|
|
149
|
+
export function windowIconFile(icons, size) {
|
|
150
|
+
return windowIconTemplate(icons).replace("{0}", String(size));
|
|
151
|
+
}
|
|
137
152
|
export function writeDsmWindowIcons(uiDir, icons) {
|
|
138
|
-
|
|
139
|
-
mkdirSync(imagesDir, { recursive: true });
|
|
153
|
+
mkdirSync(join(uiDir, "images"), { recursive: true });
|
|
140
154
|
for (const [size, png] of icons.window)
|
|
141
|
-
writeIcon(join(
|
|
155
|
+
writeIcon(join(uiDir, windowIconFile(icons, size)), png);
|
|
142
156
|
}
|
|
143
157
|
export function writePackageCenterIcons(spkRoot, icons) {
|
|
144
158
|
writeIcon(join(spkRoot, "PACKAGE_ICON.PNG"), icons.packageIcon);
|
|
@@ -15,7 +15,7 @@ export declare const CONTAINER_MIN_DSM: string;
|
|
|
15
15
|
*/
|
|
16
16
|
export declare const PORTAL_MIN_DSM: string;
|
|
17
17
|
/** Why DSM below PORTAL_MIN_DSM refuses a package that declares the web-config portal resource. */
|
|
18
|
-
export declare const PORTAL_MIN_DSM_REASON = "DSM 7.1
|
|
18
|
+
export declare const PORTAL_MIN_DSM_REASON = "DSM 7.1 lists web-config as a Synology-only worker and rejects unsigned packages with a portal (web-config app-portal) with error 320; the 7.2 onward allowance is inferred from the synopkg source and is not yet verified on hardware";
|
|
19
19
|
/**
|
|
20
20
|
* Below {@link PORTAL_MIN_DSM} the root executor registers the portal entry in
|
|
21
21
|
* DSM's app.d instead; that file format is verified from DSM 7.1 on.
|
|
@@ -22,7 +22,7 @@ export const CONTAINER_MIN_DSM = FIRST_BUILD["7.2.1"];
|
|
|
22
22
|
*/
|
|
23
23
|
export const PORTAL_MIN_DSM = FIRST_BUILD["7.2"];
|
|
24
24
|
/** Why DSM below PORTAL_MIN_DSM refuses a package that declares the web-config portal resource. */
|
|
25
|
-
export const PORTAL_MIN_DSM_REASON = "DSM 7.1
|
|
25
|
+
export const PORTAL_MIN_DSM_REASON = "DSM 7.1 lists web-config as a Synology-only worker and rejects unsigned packages with a portal (web-config app-portal) with error 320; the 7.2 onward allowance is inferred from the synopkg source and is not yet verified on hardware";
|
|
26
26
|
/**
|
|
27
27
|
* Below {@link PORTAL_MIN_DSM} the root executor registers the portal entry in
|
|
28
28
|
* DSM's app.d instead; that file format is verified from DSM 7.1 on.
|
|
@@ -34,7 +34,7 @@ export function portalByExecutor(osMinVer) {
|
|
|
34
34
|
}
|
|
35
35
|
/** Why a portal package cannot declare `value`, and how to fix it; `field` names the portal key. */
|
|
36
36
|
export function portalMinDsmMessage(value, field) {
|
|
37
|
-
return `${field}
|
|
37
|
+
return `${field} needs DSM ${PORTAL_EXECUTOR_MIN_DSM} or later; os_min_ver is "${value}". Before DSM ${PORTAL_MIN_DSM} the executor writes the entry into DSM's app.d, and that format is only verified from DSM 7.1. Raise os_min_ver to ${PORTAL_EXECUTOR_MIN_DSM} or later, or remove ${field}.`;
|
|
38
38
|
}
|
|
39
39
|
/** `true` when `value` is not older than `floor`, comparing version parts then build. */
|
|
40
40
|
export function isDsmAtLeast(value, floor) {
|
|
@@ -53,7 +53,7 @@ export function isDsmAtLeast(value, floor) {
|
|
|
53
53
|
/** Container Apps cannot declare an os_min_ver below {@link CONTAINER_MIN_DSM}. */
|
|
54
54
|
export function assertContainerMinDsm(value) {
|
|
55
55
|
if (!isDsmAtLeast(value, CONTAINER_MIN_DSM)) {
|
|
56
|
-
throw new Error(`[MIN_DSM_CONTAINER_FLOOR]
|
|
56
|
+
throw new Error(`[MIN_DSM_CONTAINER_FLOOR] A container App needs DSM ${CONTAINER_MIN_DSM} or later (docker-project/Container Manager starts at DSM 7.2.1); got "${value}".`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
const FULL_VERSION = /^7\.\d+(\.\d+)?-\d+$/;
|
|
@@ -64,7 +64,7 @@ export function resolveMinDsm(value) {
|
|
|
64
64
|
}
|
|
65
65
|
const build = FIRST_BUILD[value];
|
|
66
66
|
if (!build) {
|
|
67
|
-
throw new Error(`[MIN_DSM_INVALID] --min-dsm
|
|
67
|
+
throw new Error(`[MIN_DSM_INVALID] --min-dsm accepts only ${Object.keys(FIRST_BUILD).join(", ")}, or a full <version>-<build> (for example 7.2-64570); got "${value}".`);
|
|
68
68
|
}
|
|
69
69
|
return build;
|
|
70
70
|
}
|
|
@@ -32,10 +32,10 @@ export async function buildGatewayBinary(input) {
|
|
|
32
32
|
env: { ...process.env, ...pinnedGoBuildEnv(input.arch) },
|
|
33
33
|
});
|
|
34
34
|
if (result.missing) {
|
|
35
|
-
return { error: "GATEWAY_GO_MISSING", detail:
|
|
35
|
+
return { error: "GATEWAY_GO_MISSING", detail: `The go command was not found. Building the DSM verification gateway needs Go 1.21 or later (https://go.dev/dl/). ${PINNED_GO_HINT}` };
|
|
36
36
|
}
|
|
37
37
|
if (result.status !== 0) {
|
|
38
|
-
return { error: "GATEWAY_BUILD_FAILED", detail: `go build
|
|
38
|
+
return { error: "GATEWAY_BUILD_FAILED", detail: `go build (${input.arch}) failed. ${PINNED_GO_HINT}\n${result.stderr}` };
|
|
39
39
|
}
|
|
40
40
|
const problem = checkCgiBinary(input.work, out, input.arch);
|
|
41
41
|
if (problem) {
|
|
@@ -178,7 +178,7 @@ export async function buildSpk(options) {
|
|
|
178
178
|
if (contract.adminport !== undefined && resolvedWebLaunch(contract) === "dsm-window") {
|
|
179
179
|
const uiDir = join(staging, "package", "ui");
|
|
180
180
|
mkdirSync(uiDir, { recursive: true });
|
|
181
|
-
writeFileSync(join(uiDir, "config"), renderDsmWindowConfig(contract), "utf8");
|
|
181
|
+
writeFileSync(join(uiDir, "config"), renderDsmWindowConfig(contract, icons), "utf8");
|
|
182
182
|
writeDsmWindowIcons(uiDir, icons);
|
|
183
183
|
writeUiTexts(uiDir, contractAppTitle(contract));
|
|
184
184
|
}
|
package/dist/build-spk/png.js
CHANGED
|
@@ -4,7 +4,7 @@ const MAX_SIDE = 4096;
|
|
|
4
4
|
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
5
5
|
const DEPTHS = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
|
|
6
6
|
function corrupt(why) {
|
|
7
|
-
throw new Error(`PNG
|
|
7
|
+
throw new Error(`Corrupt PNG: ${why}`);
|
|
8
8
|
}
|
|
9
9
|
function chunk(type, data) {
|
|
10
10
|
const head = Buffer.alloc(8);
|
|
@@ -21,34 +21,34 @@ function paeth(a, b, c) {
|
|
|
21
21
|
}
|
|
22
22
|
export function decodePng(bytes) {
|
|
23
23
|
if (bytes.length < 8 || !bytes.subarray(0, 8).equals(SIGNATURE))
|
|
24
|
-
throw new Error("
|
|
24
|
+
throw new Error("Not a PNG file (header signature mismatch).");
|
|
25
25
|
let offset = 8;
|
|
26
26
|
let header;
|
|
27
27
|
let palette, alpha, ended = false;
|
|
28
28
|
const data = [];
|
|
29
29
|
while (!ended) {
|
|
30
30
|
if (offset + 12 > bytes.length)
|
|
31
|
-
corrupt("
|
|
31
|
+
corrupt("file is truncated");
|
|
32
32
|
const length = bytes.readUInt32BE(offset);
|
|
33
33
|
const type = bytes.toString("latin1", offset + 4, offset + 8);
|
|
34
34
|
const end = offset + 12 + length;
|
|
35
35
|
if (end > bytes.length)
|
|
36
|
-
corrupt("
|
|
36
|
+
corrupt("file is truncated");
|
|
37
37
|
const body = bytes.subarray(offset + 8, offset + 8 + length);
|
|
38
38
|
if (crc32(body, crc32(bytes.subarray(offset + 4, offset + 8))) !== bytes.readUInt32BE(end - 4))
|
|
39
|
-
corrupt(`${type} CRC
|
|
39
|
+
corrupt(`${type} CRC mismatch`);
|
|
40
40
|
if (!header && type !== "IHDR")
|
|
41
|
-
corrupt("
|
|
41
|
+
corrupt("the first chunk must be IHDR");
|
|
42
42
|
if (type === "IHDR") {
|
|
43
43
|
if (header || length !== 13)
|
|
44
|
-
corrupt("IHDR
|
|
44
|
+
corrupt("invalid IHDR");
|
|
45
45
|
header = { width: body.readUInt32BE(0), height: body.readUInt32BE(4), depth: body[8], type: body[9] };
|
|
46
46
|
if (!DEPTHS[header.type]?.includes(header.depth) || body[10] !== 0 || body[11] !== 0)
|
|
47
|
-
corrupt("
|
|
47
|
+
corrupt("unsupported colour type or bit depth");
|
|
48
48
|
if (body[12] !== 0)
|
|
49
|
-
throw new Error("
|
|
49
|
+
throw new Error("Interlaced PNG is not supported; save it as a non-interlaced PNG.");
|
|
50
50
|
if (header.width < 1 || header.height < 1 || header.width > MAX_SIDE || header.height > MAX_SIDE) {
|
|
51
|
-
throw new Error(`PNG
|
|
51
|
+
throw new Error(`PNG size ${header.width}x${header.height} is out of range (1-${MAX_SIDE} px per side).`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
else if (type === "PLTE")
|
|
@@ -62,10 +62,10 @@ export function decodePng(bytes) {
|
|
|
62
62
|
offset = end;
|
|
63
63
|
}
|
|
64
64
|
if (!header || !data.length)
|
|
65
|
-
corrupt("
|
|
65
|
+
corrupt("missing image data");
|
|
66
66
|
const { width, height, depth, type } = header;
|
|
67
67
|
if (type === 3 && (!palette || palette.length % 3 !== 0))
|
|
68
|
-
corrupt("
|
|
68
|
+
corrupt("palette image is missing PLTE");
|
|
69
69
|
const bits = CHANNELS[type] * depth;
|
|
70
70
|
const bpp = Math.max(1, bits >> 3);
|
|
71
71
|
const stride = Math.ceil((width * bits) / 8);
|
|
@@ -74,10 +74,10 @@ export function decodePng(bytes) {
|
|
|
74
74
|
raw = inflateSync(Buffer.concat(data), { maxOutputLength: (stride + 1) * height + 1 });
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
77
|
-
corrupt("
|
|
77
|
+
corrupt("image data cannot be decompressed");
|
|
78
78
|
}
|
|
79
79
|
if (raw.length !== (stride + 1) * height)
|
|
80
|
-
corrupt("
|
|
80
|
+
corrupt("image data length mismatch");
|
|
81
81
|
const prev = Buffer.alloc(stride), line = Buffer.alloc(stride);
|
|
82
82
|
const rgba = Buffer.alloc(width * height * 4);
|
|
83
83
|
const sample = (x, channel) => {
|
|
@@ -94,7 +94,7 @@ export function decodePng(bytes) {
|
|
|
94
94
|
const filter = raw[y * (stride + 1)];
|
|
95
95
|
for (let i = 0; i < stride; i++) {
|
|
96
96
|
const a = i >= bpp ? line[i - bpp] : 0, b = prev[i], c = i >= bpp ? prev[i - bpp] : 0;
|
|
97
|
-
const predictor = filter === 0 ? 0 : filter === 1 ? a : filter === 2 ? b : filter === 3 ? (a + b) >> 1 : filter === 4 ? paeth(a, b, c) : corrupt("
|
|
97
|
+
const predictor = filter === 0 ? 0 : filter === 1 ? a : filter === 2 ? b : filter === 3 ? (a + b) >> 1 : filter === 4 ? paeth(a, b, c) : corrupt("unknown row filter");
|
|
98
98
|
line[i] = (row[i] + predictor) & 0xff;
|
|
99
99
|
}
|
|
100
100
|
for (let x = 0; x < width; x++) {
|
|
@@ -102,7 +102,7 @@ export function decodePng(bytes) {
|
|
|
102
102
|
if (type === 3) {
|
|
103
103
|
const index = sample(x, 0);
|
|
104
104
|
if (index * 3 + 2 >= palette.length)
|
|
105
|
-
corrupt("
|
|
105
|
+
corrupt("palette index out of range");
|
|
106
106
|
palette.copy(rgba, out, index * 3, index * 3 + 3);
|
|
107
107
|
rgba[out + 3] = alpha && index < alpha.length ? alpha[index] : 255;
|
|
108
108
|
}
|
package/dist/capabilities.js
CHANGED
|
@@ -3,7 +3,7 @@ export function capabilityNames(values) {
|
|
|
3
3
|
const names = [...new Set(values.flatMap(value => value.split(',')).map(value => value.trim()))];
|
|
4
4
|
for (const name of names) {
|
|
5
5
|
if (!CAPABILITY_NAMES.includes(name))
|
|
6
|
-
throw new Error(`[CAPABILITY_INVALID]
|
|
6
|
+
throw new Error(`[CAPABILITY_INVALID] Unsupported capability "${name}"; choose from ${CAPABILITY_NAMES.join(', ')}.`);
|
|
7
7
|
}
|
|
8
8
|
return names;
|
|
9
9
|
}
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
+
import type { CapabilityName } from './capabilities.js';
|
|
2
|
+
import type { ApiPermission } from './standalone/api-permissions.js';
|
|
1
3
|
/**
|
|
2
4
|
* How each capability of a native template is laid over its base template: the
|
|
3
|
-
* template directories copied on top, files borrowed from another overlay,
|
|
4
|
-
*
|
|
5
|
+
* template directories copied on top, files borrowed from another overlay, the
|
|
6
|
+
* base files the overlay replaces, and the rules it adds to the App's
|
|
7
|
+
* permissions. A bridge App's example may also need another capability, and a
|
|
8
|
+
* Rust App lists the methods it adds in its manifest. init and add both read
|
|
9
|
+
* this table.
|
|
5
10
|
*/
|
|
6
11
|
export interface CapabilityOverlay {
|
|
7
12
|
overlays: string[];
|
|
8
13
|
borrowed: string[];
|
|
9
14
|
removed: string[];
|
|
15
|
+
permissions: Record<string, ApiPermission>;
|
|
16
|
+
implied?: CapabilityName[];
|
|
17
|
+
methods?: string[];
|
|
10
18
|
}
|
|
11
|
-
export type
|
|
19
|
+
export type BridgeTemplate = 'native-python' | 'native-node' | 'native-rust';
|
|
20
|
+
export type NativeTemplate = 'native-go' | BridgeTemplate;
|
|
21
|
+
export declare const BRIDGE_TEMPLATES: BridgeTemplate[];
|
|
12
22
|
export declare const CAPABILITY_OVERLAYS: Record<NativeTemplate, Record<string, CapabilityOverlay>>;
|
|
13
|
-
/**
|
|
14
|
-
export declare const
|
|
23
|
+
/** What a native template's base and Vue frontend add before the capabilities: a bridge App borrows the Go template's UI. */
|
|
24
|
+
export declare const BASE_BORROWED: Record<NativeTemplate, string[]>;
|
|
25
|
+
export declare const VUE_OVERLAYS: Record<NativeTemplate, Pick<CapabilityOverlay, 'overlays' | 'borrowed'>>;
|
|
15
26
|
/** The overlays of these capabilities, in table order, each once; a capability without one adds nothing. */
|
|
16
27
|
export declare function capabilityOverlays(template: NativeTemplate, names: readonly string[]): CapabilityOverlay;
|
|
17
28
|
/** Each file of the borrowed paths, a borrowed folder listed file by file, under `templates`. */
|
|
@@ -1,38 +1,52 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const EXECUTOR_SHELL = ['backend/internal/app/executor.go', 'backend/internal/app/executor_test.go'];
|
|
4
|
+
export const BRIDGE_TEMPLATES = ['native-python', 'native-node', 'native-rust'];
|
|
4
5
|
export const CAPABILITY_OVERLAYS = { 'native-go': {
|
|
5
|
-
'root-ops': { overlays: ['native-go-root-ops'], borrowed: [], removed: ['backend/internal/app/root_none.go'] },
|
|
6
|
-
jobs: { overlays: ['native-go-jobs'], borrowed: [], removed: ['backend/internal/app/background_none.go'] },
|
|
6
|
+
'root-ops': { overlays: ['native-go-root-ops'], borrowed: [], removed: ['backend/internal/app/root_none.go'], permissions: {} },
|
|
7
|
+
jobs: { overlays: ['native-go-jobs'], borrowed: [], removed: ['backend/internal/app/background_none.go'], permissions: {} },
|
|
7
8
|
delegate: {
|
|
8
9
|
overlays: ['native-go-jobs', 'native-go-delegate'],
|
|
9
10
|
borrowed: EXECUTOR_SHELL.map(file => `native-go-root-ops/${file}`),
|
|
10
11
|
removed: ['backend/internal/app/background_none.go', 'backend/internal/app/root_none.go', 'backend/internal/app/delegate_none.go'],
|
|
12
|
+
// Every signed-in user opens a delegate App to pick where reports go.
|
|
13
|
+
permissions: { 'app.status': 'authenticated' },
|
|
11
14
|
},
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
}, ...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, bridgeOverlays(template)])) };
|
|
16
|
+
/**
|
|
17
|
+
* A bridge App runs the Go template's example in its own language: each
|
|
18
|
+
* overlay replaces a stub module, and the frontend and root programs come
|
|
19
|
+
* from the Go overlays. In this order, a Rust manifest lists the methods.
|
|
20
|
+
*/
|
|
21
|
+
function bridgeOverlays(template) {
|
|
22
|
+
return {
|
|
23
|
+
// The example hashes a file the caller picks, which the runtime reads only for an App with files.
|
|
24
|
+
jobs: { overlays: [`${template}-jobs`], borrowed: ['native-go-jobs/frontend'], removed: [], permissions: {}, implied: ['files'], methods: ['hash.submit', 'hash.get', 'hash.cancel'] },
|
|
25
|
+
'root-ops': { overlays: [`${template}-root-ops`], borrowed: ['native-go-root-ops/root'], removed: [], permissions: {}, methods: ['whoami.run'] },
|
|
18
26
|
delegate: {
|
|
19
|
-
overlays: ['native-
|
|
20
|
-
|
|
21
|
-
removed: [],
|
|
27
|
+
overlays: [`${template}-delegate`], borrowed: ['native-go-jobs/frontend', 'native-go-delegate/frontend'], removed: [],
|
|
28
|
+
permissions: { 'app.status': 'authenticated', 'report.save': 'authenticated' }, methods: ['report.save'],
|
|
22
29
|
},
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** What a native template's base and Vue frontend add before the capabilities: a bridge App borrows the Go template's UI. */
|
|
33
|
+
export const BASE_BORROWED = { 'native-go': [],
|
|
34
|
+
...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, ['native-go/ui', 'native-go/i18n']])) };
|
|
35
|
+
export const VUE_OVERLAYS = { 'native-go': { overlays: ['native-go-vue'], borrowed: [] },
|
|
36
|
+
...Object.fromEntries(BRIDGE_TEMPLATES.map(template => [template, { overlays: [`${template}-vue`], borrowed: ['native-go-vue/frontend'] }])) };
|
|
26
37
|
/** The overlays of these capabilities, in table order, each once; a capability without one adds nothing. */
|
|
27
38
|
export function capabilityOverlays(template, names) {
|
|
28
39
|
const chosen = Object.entries(CAPABILITY_OVERLAYS[template]).filter(([name]) => names.includes(name)).map(([, overlay]) => overlay);
|
|
29
40
|
const unique = (pick) => [...new Set(chosen.flatMap(pick))];
|
|
30
|
-
return { overlays: unique(o => o.overlays), borrowed: unique(o => o.borrowed), removed: unique(o => o.removed)
|
|
41
|
+
return { overlays: unique(o => o.overlays), borrowed: unique(o => o.borrowed), removed: unique(o => o.removed),
|
|
42
|
+
permissions: Object.assign({}, ...chosen.map(o => o.permissions)),
|
|
43
|
+
implied: [...new Set(chosen.flatMap(o => o.implied ?? []))], methods: unique(o => o.methods ?? []) };
|
|
31
44
|
}
|
|
32
45
|
/** Each file of the borrowed paths, a borrowed folder listed file by file, under `templates`. */
|
|
33
46
|
export function borrowedFiles(templates, borrowed) {
|
|
47
|
+
// A template folder may carry what developing it installs or builds.
|
|
34
48
|
const walk = (path) => statSync(join(templates, path)).isDirectory()
|
|
35
|
-
? readdirSync(join(templates, path)).sort().flatMap(name => walk(`${path}/${name}`))
|
|
49
|
+
? readdirSync(join(templates, path)).sort().filter(name => name !== 'node_modules' && name !== 'dist').flatMap(name => walk(`${path}/${name}`))
|
|
36
50
|
: [path];
|
|
37
51
|
return [...new Set(borrowed.flatMap(walk))];
|
|
38
52
|
}
|