@synopackageland/cli 0.25.2 → 0.27.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 +13 -0
- package/README.md +23 -3
- package/dist/error-registry.js +16 -1
- package/dist/runtime/executor-digests.json +12 -1
- package/dist/runtime/sdk-source.js +2 -2
- package/dist/standalone/daemon-restart.d.ts +15 -0
- package/dist/standalone/daemon-restart.js +55 -0
- package/dist/standalone/daemon.d.ts +4 -0
- package/dist/standalone/daemon.js +13 -3
- package/dist/standalone/dsm-window.d.ts +2 -0
- package/dist/standalone/dsm-window.js +6 -2
- package/dist/standalone/inspect-spk.js +59 -5
- package/dist/standalone/manifest.d.ts +6 -0
- package/dist/standalone/manifest.js +33 -5
- package/dist/standalone/spk.js +14 -13
- package/dist/template-paths.d.ts +1 -1
- package/dist/template-paths.js +1 -1
- package/package.json +2 -2
- package/schema/daemon-restart.v1.schema.json +16 -0
- package/skills/SKILL.md +16 -0
- package/skills/reference/compose.md +1 -1
- package/skills/reference/portal.md +6 -0
- package/skills/reference/ui-vue.md +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
- Depend on templates `~0.27.0` with SDK/App 0.27.0 and UI 0.14.9. Templates patches can ship independently and resolve during npm installation/update; runtime uses the installed templates without network fetching. SDK minor upgrades still require a CLI release for executor mappings/digests and `SDK_UNSUPPORTED` checks. New scaffolds use browser same-origin DSM file operations and the idle-storage fixes.
|
|
6
|
+
- Resolve SDK 0.27.0 to the new executor/v0.27.0 and ship all six verified executor/lifecycle digests. Preserve the previous 0.26.0 inventory and exact-version refusal for unrecorded patches. Protected Apps must rebuild and follow normal administrator approval.
|
|
7
|
+
- Keep Go 1.26.10 and the existing runtime/toolchain requirements. Rust crate publication and NAS/App upgrade certification are outside this release.
|
|
8
|
+
|
|
9
|
+
## 0.26.0 (2026-10-22)
|
|
10
|
+
|
|
11
|
+
- Support SDK/App 0.26.x and pin templates 0.26.0, retaining UI 0.14.3 and executor/v0.24.0 with unchanged protected payload digests.
|
|
12
|
+
- Add optional bounded `daemon.restart` crash recovery for native Apps; require SDK 0.26.0 or later, validate the policy and record it for inspection. Initial startup and intentional stops retain their existing behavior.
|
|
13
|
+
|
|
14
|
+
- Add `ui: false` for native Apps with `identity_gateway` and no SDK window. The main menu gets one url launcher to `sso/open.cgi` (new tab) and Package Center's Open goes to the same entry through INFO `adminurl`, as for Compose Apps. check refuses it without a gateway, with `console` or `portal`, or with capabilities whose approval or sign-in needs the SDK window (`STANDALONE_UI_NO_WINDOW_INVALID`); inspect verifies the single launcher and `adminurl`. Apps with `ui.dir` build unchanged.
|
|
15
|
+
|
|
3
16
|
## 0.25.2 (2026-10-22)
|
|
4
17
|
|
|
5
18
|
- Use templates 0.25.1 with the native Rust lockfile pinned to the public SDK 0.25.0 commit. SDK/App support and executor/lifecycle digests are unchanged.
|
package/README.md
CHANGED
|
@@ -64,8 +64,18 @@ a failed fetch reports `SDK_DOWNLOAD_FAILED`.
|
|
|
64
64
|
CLI-only patch releases can differ from the SDK patch. Compose import and SDK defaults
|
|
65
65
|
read the SDK pin from the installed templates; explicit App SDK declarations take precedence.
|
|
66
66
|
|
|
67
|
-
`init` and `add` read their templates from
|
|
68
|
-
|
|
67
|
+
`init` and `add` read their templates from the locally installed
|
|
68
|
+
`@synopackageland/templates`. CLI 0.27.0 depends on `~0.27.0`: npm installation or
|
|
69
|
+
update can select the latest templates patch in the 0.27 minor line. Templates
|
|
70
|
+
patches need only a templates release; update the npm installation to receive them.
|
|
71
|
+
The CLI never fetches templates at runtime, so offline use works after installation;
|
|
72
|
+
commit a lockfile and use `npm ci` to reproduce a chosen dependency tree.
|
|
73
|
+
The templates' SDK pin must stay in the CLI-supported minor line. An SDK minor
|
|
74
|
+
upgrade still needs a CLI release for its executor mapping/digests and
|
|
75
|
+
`SDK_UNSUPPORTED` checks. Runtime SDK defaults continue to come from the actual
|
|
76
|
+
installed templates, independently of the CLI patch version.
|
|
77
|
+
|
|
78
|
+
For development against local checkouts, set `SYNOPKGLAND_TEMPLATES_DIR` to a
|
|
69
79
|
templates checkout and `SYNOPKGLAND_SDK_DIR` to an SDK checkout; SPKs built with
|
|
70
80
|
`SYNOPKGLAND_SDK_DIR` warn `EXECUTOR_DIGEST_UNVERIFIED` and must not be published. A Vue project
|
|
71
81
|
packs the local SDK into its frontend (`npm install --install-links`) without building it: run
|
|
@@ -81,7 +91,7 @@ This compares `OFF_GLOSSARY` and the Italian sentence rule, including regex sour
|
|
|
81
91
|
flags. It supports declaration-only rules with read-only use in `glossaryProblems`;
|
|
82
92
|
post-declaration mutations, aliases and other unsupported rule uses fail the check instead of
|
|
83
93
|
comparing a partial snapshot. The command also fails on drift or missing source. It needs a checkout because templates
|
|
84
|
-
|
|
94
|
+
does not publish `scripts/test/i18n.test.mjs`. The npm gate runs this check when
|
|
85
95
|
`SYNOPKGLAND_TEMPLATES_DIR` is set; otherwise it explicitly reports that the drift check was skipped.
|
|
86
96
|
|
|
87
97
|
## Capabilities
|
|
@@ -278,6 +288,16 @@ sets it for every language. Omit `console` to keep one icon. check refuses
|
|
|
278
288
|
`console` on Compose, without `identity_gateway`, or with `ui.launch` or
|
|
279
289
|
`portal`. A cold DSM sign-in resumes through the url launcher.
|
|
280
290
|
|
|
291
|
+
A native App with `identity_gateway` whose only UI is the wrapped web app
|
|
292
|
+
declares `ui: false` instead of `ui.dir`. It gets no SDK window: the main menu
|
|
293
|
+
has one icon (`displayname`, a `type: "url"` launcher to `sso/open.cgi` that
|
|
294
|
+
opens a new tab), and Package Center's Open goes to the same entry through
|
|
295
|
+
INFO `adminurl`, as for a Compose App. The backend and ctl commands are
|
|
296
|
+
unchanged. check refuses `ui: false` without `identity_gateway`, with `console`
|
|
297
|
+
or `portal`, or with `capabilities.rootOps`, `delegate`, `webapi` or
|
|
298
|
+
`identity`, whose approval or sign-in is shown in the SDK window
|
|
299
|
+
(`STANDALONE_UI_NO_WINDOW_INVALID`). A missing `ui` is still an error.
|
|
300
|
+
|
|
281
301
|
Compose uses schema-2 `dsm-package.yaml` and `ui.launch`; the default is new-page.
|
|
282
302
|
Classless gateways use type url via the shared contract/app-launch resolver;
|
|
283
303
|
AppWindow wrappers use standalone. Headless projects have no launcher.
|
package/dist/error-registry.js
CHANGED
|
@@ -282,6 +282,16 @@ export const ERROR_REGISTRY = {
|
|
|
282
282
|
description: "The package daemon declaration does not match the fixed argv, environment and lifecycle contract.",
|
|
283
283
|
suggestion: "Check daemon.exec, the per-architecture targets, args, env, uninstall_args and integer-second timeouts; remove shell, user and custom conf.",
|
|
284
284
|
},
|
|
285
|
+
STANDALONE_DAEMON_RESTART_INVALID: {
|
|
286
|
+
code: "STANDALONE_DAEMON_RESTART_INVALID",
|
|
287
|
+
description: "daemon.restart does not match the bounded native crash-recovery policy.",
|
|
288
|
+
suggestion: "Use mode off or on-failure, integer delay/attempt/window limits, and max_seconds at least initial_seconds; omit or set limits to zero for SDK defaults and remove unknown fields.",
|
|
289
|
+
},
|
|
290
|
+
STANDALONE_DAEMON_RESTART_SDK_REQUIRED: {
|
|
291
|
+
code: "STANDALONE_DAEMON_RESTART_SDK_REQUIRED",
|
|
292
|
+
description: "The selected controller SDK version or source does not support daemon.restart.",
|
|
293
|
+
suggestion: "Upgrade the selected controller SDK to 0.26.0 or later and use matching sources, including a local SDK override; update the App SDK requirement, identity_gateway.sdk or the templates that supply the default.",
|
|
294
|
+
},
|
|
285
295
|
UNINSTALL_INVALID: {
|
|
286
296
|
code: "UNINSTALL_INVALID",
|
|
287
297
|
description: "The uninstall declaration does not match the fixed exec, argv, environment and timeout contract.",
|
|
@@ -485,7 +495,12 @@ export const ERROR_REGISTRY = {
|
|
|
485
495
|
STANDALONE_UI_INVALID: {
|
|
486
496
|
code: "STANDALONE_UI_INVALID",
|
|
487
497
|
description: "The ui configuration is invalid.",
|
|
488
|
-
suggestion: "Provide ui.dir.",
|
|
498
|
+
suggestion: "Provide ui.dir, or ui: false for a native identity_gateway App without an SDK window.",
|
|
499
|
+
},
|
|
500
|
+
STANDALONE_UI_NO_WINDOW_INVALID: {
|
|
501
|
+
code: "STANDALONE_UI_NO_WINDOW_INVALID",
|
|
502
|
+
description: "ui: false is declared where the App needs an SDK window.",
|
|
503
|
+
suggestion: "ui: false needs identity_gateway and no console, portal, capabilities.rootOps, delegate, webapi or identity; otherwise declare ui.dir.",
|
|
489
504
|
},
|
|
490
505
|
STANDALONE_UI_BUILD_INVALID: {
|
|
491
506
|
code: "STANDALONE_UI_BUILD_INVALID",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"0.
|
|
2
|
+
"0.26.0": {
|
|
3
3
|
"x86_64": "de78684faac211c24a7cda8a4541ab1b138d37f6d580936fe354bb9bd0d78a0f",
|
|
4
4
|
"aarch64": "8ef7c736203f6c111816b76f970b7204fa6f8d871ff3222fd7c0a27d0075b847",
|
|
5
5
|
"armv7": "a38703119554b5f2a8bf4810ec2a6c8f1d793bfbb1ca96803eb1b39a26ffd96c",
|
|
@@ -9,5 +9,16 @@
|
|
|
9
9
|
"armv7": "6d25fc972eba12e3ac68f8b6a95a45e9e5706ebe19ffc1ff774fc318acbeb90f"
|
|
10
10
|
},
|
|
11
11
|
"goToolchain": "go1.26.10"
|
|
12
|
+
},
|
|
13
|
+
"0.27.0": {
|
|
14
|
+
"x86_64": "35f760533c133f7ed99d62c11dc4696d06a56fd2d125d53291aa21743d8327cc",
|
|
15
|
+
"aarch64": "04dff10b52ca4c54e3e6a825c27bac876f296031d8f572f667e6902e18464e31",
|
|
16
|
+
"armv7": "8789b2663b683ae0a5a457b41ccfcff4d54be4c98dcd3af338622051ad24f943",
|
|
17
|
+
"lifecycle": {
|
|
18
|
+
"x86_64": "c6fbb377ecd5ec7ee5b96439f22091d9fa5965dfd616d510ca9445722f436721",
|
|
19
|
+
"aarch64": "a9ca1750d4d5cff4727dc29789e6eaf0a365cb0e674f1bb796971d82f2d4fa27",
|
|
20
|
+
"armv7": "6d25fc972eba12e3ac68f8b6a95a45e9e5706ebe19ffc1ff774fc318acbeb90f"
|
|
21
|
+
},
|
|
22
|
+
"goToolchain": "go1.26.10"
|
|
12
23
|
}
|
|
13
24
|
}
|
|
@@ -75,8 +75,8 @@ function goMod(backendDir) {
|
|
|
75
75
|
return '';
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
/** SDK
|
|
79
|
-
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0', '0.24.1': '0.24.0', '0.25.0': '0.24.0' };
|
|
78
|
+
/** Explicit executor release pins; older SDK versions can reuse an immutable release with identical payload bytes. */
|
|
79
|
+
const EXECUTOR_RELEASES = { '0.21.0': '0.20.0', '0.21.1': '0.20.0', '0.22.0': '0.20.0', '0.23.0': '0.20.0', '0.23.1': '0.20.0', '0.23.2': '0.20.0', '0.24.1': '0.24.0', '0.25.0': '0.24.0', '0.26.0': '0.24.0', '0.27.0': '0.27.0' };
|
|
80
80
|
/**
|
|
81
81
|
* The module directories of one SDK version. SYNOPKGLAND_SDK_DIR names a
|
|
82
82
|
* local sdk checkout (development and gates only); otherwise modules come from
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StandaloneError } from "./manifest.js";
|
|
2
|
+
export declare const DAEMON_RESTART_MIN_SDK = "0.26.0";
|
|
3
|
+
export interface DaemonRestart {
|
|
4
|
+
mode: "off" | "on-failure";
|
|
5
|
+
initial_seconds: number;
|
|
6
|
+
max_seconds: number;
|
|
7
|
+
max_attempts: number;
|
|
8
|
+
window_seconds: number;
|
|
9
|
+
}
|
|
10
|
+
/** Normalize exactly the SDK's zero/omitted defaults; omission itself stays off. */
|
|
11
|
+
export declare function readDaemonRestart(value: unknown, fail: (error: StandaloneError) => void): DaemonRestart | undefined;
|
|
12
|
+
export declare function daemonRestartSdkSupported(sdk: string): boolean;
|
|
13
|
+
export declare function daemonRestartSdkProblem(sdk: string): string | undefined;
|
|
14
|
+
/** Development overrides must contain the decoder as well as the restart code. */
|
|
15
|
+
export declare function requireDaemonRestartSource(dir: string, sdk: string, policy?: DaemonRestart): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { StandaloneBuildError } from "./spk.js";
|
|
5
|
+
export const DAEMON_RESTART_MIN_SDK = "0.26.0";
|
|
6
|
+
let validator;
|
|
7
|
+
function restartValidator() {
|
|
8
|
+
return validator ??= new Ajv().compile(JSON.parse(readFileSync(new URL("../../schema/daemon-restart.v1.schema.json", import.meta.url), "utf8")));
|
|
9
|
+
}
|
|
10
|
+
/** Normalize exactly the SDK's zero/omitted defaults; omission itself stays off. */
|
|
11
|
+
export function readDaemonRestart(value, fail) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (restartValidator()(value)) {
|
|
15
|
+
const raw = value;
|
|
16
|
+
const policy = {
|
|
17
|
+
mode: raw.mode,
|
|
18
|
+
initial_seconds: raw.initial_seconds || 1,
|
|
19
|
+
max_seconds: raw.max_seconds || 30,
|
|
20
|
+
max_attempts: raw.max_attempts || 5,
|
|
21
|
+
window_seconds: raw.window_seconds || 300,
|
|
22
|
+
};
|
|
23
|
+
if (policy.max_seconds >= policy.initial_seconds)
|
|
24
|
+
return policy;
|
|
25
|
+
}
|
|
26
|
+
fail({
|
|
27
|
+
code: "STANDALONE_DAEMON_RESTART_INVALID",
|
|
28
|
+
message: "daemon.restart requires mode off or on-failure; delays 1–3600s (max >= initial), attempts 1–100 and window 1–86400s. Omitted or zero limits use SDK defaults; unknown fields are rejected.",
|
|
29
|
+
});
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
export function daemonRestartSdkSupported(sdk) {
|
|
33
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(sdk);
|
|
34
|
+
return Boolean(match && (Number(match[1]) > 0 || Number(match[2]) >= 26));
|
|
35
|
+
}
|
|
36
|
+
export function daemonRestartSdkProblem(sdk) {
|
|
37
|
+
if (!daemonRestartSdkSupported(sdk))
|
|
38
|
+
return `daemon.restart requires SDK ${DAEMON_RESTART_MIN_SDK} or later; selected SDK ${sdk}. Upgrade the App's SDK pin, identity_gateway.sdk or the templates supplying the default controller SDK before rebuilding.`;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Development overrides must contain the decoder as well as the restart code. */
|
|
42
|
+
export function requireDaemonRestartSource(dir, sdk, policy) {
|
|
43
|
+
if (!policy)
|
|
44
|
+
return;
|
|
45
|
+
let main = "", restart = "";
|
|
46
|
+
try {
|
|
47
|
+
main = readFileSync(join(dir, "daemonctl/main.go"), "utf8");
|
|
48
|
+
restart = readFileSync(join(dir, "daemonctl/restart.go"), "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch { /* Fail below before compiling or packaging a mismatched runtime. */ }
|
|
51
|
+
const problem = daemonRestartSdkProblem(sdk);
|
|
52
|
+
if (problem || !/\bRestart\s+\*restartPolicy\s+`json:"restart,omitempty"`/.test(main) ||
|
|
53
|
+
!/^func normalizeRestart\(/m.test(restart) || !/^type restartBudget struct\s*\{/m.test(restart))
|
|
54
|
+
throw new StandaloneBuildError("STANDALONE_DAEMON_RESTART_SDK_REQUIRED", problem ?? `Selected SDK ${sdk} source does not support daemon.restart; use matching SDK ${DAEMON_RESTART_MIN_SDK} or later sources, including for a local development override.`);
|
|
55
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
2
2
|
import { NODE_PACKAGES } from "./bridge-backend.js";
|
|
3
3
|
import { type DaemonIngress } from "./daemon-ingress.js";
|
|
4
|
+
import { type DaemonRestart } from "./daemon-restart.js";
|
|
4
5
|
import type { CommandRunner } from "./build.js";
|
|
5
6
|
import type { StandaloneError, StandaloneManifest } from "./manifest.js";
|
|
6
7
|
export interface Daemon {
|
|
8
|
+
/** Optional bounded recovery; omitted means off. SDK 0.26.0 or later. */
|
|
9
|
+
restart?: DaemonRestart;
|
|
7
10
|
/** Fixed foreground executable relative to daemon/payload. */
|
|
8
11
|
exec: string;
|
|
9
12
|
/** Architecture-specific project directories, copied without links. */
|
|
@@ -46,6 +49,7 @@ export declare const envKey: (k: string) => boolean;
|
|
|
46
49
|
/** A relative path to a fixed executable, without `.` or `..` parts. */
|
|
47
50
|
export declare const relativeExec: (v: unknown) => v is string;
|
|
48
51
|
export declare function readDaemon(value: unknown, root: string, arches: NativeArchFamily[], fail: (error: StandaloneError) => void): Daemon | undefined;
|
|
52
|
+
export declare function daemonSdkVersion(manifest: Pick<StandaloneManifest, "backend" | "identityGateway">): string;
|
|
49
53
|
export declare function daemonConfig(manifest: StandaloneManifest): string;
|
|
50
54
|
/**
|
|
51
55
|
* The argv and environment daemon/control runs: a Node daemon execs the
|
|
@@ -8,6 +8,7 @@ import { templateSdkVersion } from "../template-paths.js";
|
|
|
8
8
|
import { checkCgiBinary } from "./binary.js";
|
|
9
9
|
import { NODE_PACKAGES } from "./bridge-backend.js";
|
|
10
10
|
import { readDaemonIngress, ingressEnv } from "./daemon-ingress.js";
|
|
11
|
+
import { readDaemonRestart, requireDaemonRestartSource } from "./daemon-restart.js";
|
|
11
12
|
import { composeDaemon, composeNetwork } from "./compose.js";
|
|
12
13
|
import { gatewayDaemon } from "./identity-gateway.js";
|
|
13
14
|
import { StandaloneBuildError } from "./spk.js";
|
|
@@ -41,6 +42,7 @@ const fields = new Set([
|
|
|
41
42
|
"uninstall_args",
|
|
42
43
|
"node",
|
|
43
44
|
"ingress",
|
|
45
|
+
"restart",
|
|
44
46
|
]);
|
|
45
47
|
const record = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
46
48
|
/** A fixed argv or env value: only the package's own directories expand. */
|
|
@@ -110,6 +112,9 @@ export function readDaemon(value, root, arches, fail) {
|
|
|
110
112
|
const timeouts = readLifecycleTimeouts(value.timeouts, fail);
|
|
111
113
|
if (!timeouts)
|
|
112
114
|
return undefined;
|
|
115
|
+
const restart = readDaemonRestart(value.restart, fail);
|
|
116
|
+
if (value.restart !== undefined && !restart)
|
|
117
|
+
return undefined;
|
|
113
118
|
const node = readDaemonNode(value.node, fail);
|
|
114
119
|
if (value.node !== undefined && !node)
|
|
115
120
|
return undefined;
|
|
@@ -129,6 +134,7 @@ export function readDaemon(value, root, arches, fail) {
|
|
|
129
134
|
timeouts: timeouts,
|
|
130
135
|
...(node ? { node } : {}),
|
|
131
136
|
...(ingress ? { ingress } : {}),
|
|
137
|
+
...(restart ? { restart } : {}),
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
function readDaemonNode(value, fail) {
|
|
@@ -152,13 +158,13 @@ function readDaemonNode(value, fail) {
|
|
|
152
158
|
}
|
|
153
159
|
return { package: pkg, ...(min ? { min_version: min } : {}) };
|
|
154
160
|
}
|
|
155
|
-
function daemonSdkVersion(manifest) {
|
|
161
|
+
export function daemonSdkVersion(manifest) {
|
|
156
162
|
return manifest.identityGateway?.sdk ??
|
|
157
163
|
(manifest.backend.kind === "go" ? appSdkVersion(manifest.backend) : undefined) ??
|
|
158
164
|
templateSdkVersion();
|
|
159
165
|
}
|
|
160
166
|
export function daemonConfig(manifest) {
|
|
161
|
-
const { exec, args, uninstall_args, env, timeouts } = manifest.compose
|
|
167
|
+
const { exec, args, uninstall_args, env, timeouts, restart } = manifest.compose
|
|
162
168
|
? composeDaemon(manifest)
|
|
163
169
|
: manifest.identityGateway
|
|
164
170
|
? gatewayDaemon(manifest.daemon)
|
|
@@ -168,6 +174,7 @@ export function daemonConfig(manifest) {
|
|
|
168
174
|
exec,
|
|
169
175
|
args,
|
|
170
176
|
...(uninstall_args ? { uninstall_args } : {}),
|
|
177
|
+
...(restart ? { restart } : {}),
|
|
171
178
|
env,
|
|
172
179
|
timeouts,
|
|
173
180
|
...(runtimeBudgetSdkSupported(daemonSdkVersion(manifest))
|
|
@@ -193,6 +200,9 @@ export async function buildDaemonControl(manifest, arch, work, run) {
|
|
|
193
200
|
if (!manifest.daemon && !(manifest.compose && manifest.identityGateway))
|
|
194
201
|
return undefined;
|
|
195
202
|
const out = join(work, `daemon-control-${arch}`);
|
|
203
|
+
const sdk = daemonSdkVersion(manifest);
|
|
204
|
+
const source = runtimeBudgetSource(sdkSource(sdk).goDir, manifest);
|
|
205
|
+
requireDaemonRestartSource(source, sdk, manifest.daemon?.restart);
|
|
196
206
|
const result = await run("go", [
|
|
197
207
|
"build",
|
|
198
208
|
"-trimpath",
|
|
@@ -203,7 +213,7 @@ export async function buildDaemonControl(manifest, arch, work, run) {
|
|
|
203
213
|
out,
|
|
204
214
|
"./daemonctl",
|
|
205
215
|
], {
|
|
206
|
-
cwd:
|
|
216
|
+
cwd: source,
|
|
207
217
|
env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
|
|
208
218
|
});
|
|
209
219
|
if (result.missing || result.status !== 0)
|
|
@@ -25,6 +25,8 @@ export declare function windowScriptName(manifest: Pick<StandaloneManifest, "bui
|
|
|
25
25
|
/** The launcher fields an entry mode fixes; inspect checks a built SPK against them. */
|
|
26
26
|
export declare function entryLauncherAccess(entry: EntryMode): Record<string, unknown>;
|
|
27
27
|
export declare function appTitle(manifest: StandaloneManifest): AppTitle;
|
|
28
|
+
/** Whether the App ships the SDK window; Compose and `ui: false` Apps open the gateway instead. */
|
|
29
|
+
export declare function hasSdkWindow(manifest: Pick<StandaloneManifest, "compose" | "sdkWindow">): boolean;
|
|
28
30
|
/** The DSM app class whose launch opens the SDK window: the console's when the App declares one. */
|
|
29
31
|
export declare function windowAppId(manifest: Pick<StandaloneManifest, "package" | "console">): string;
|
|
30
32
|
/**
|
|
@@ -74,7 +74,7 @@ function launcherAccess(manifest) {
|
|
|
74
74
|
return manifest.entry
|
|
75
75
|
? entryLauncherAccess(manifest.entry)
|
|
76
76
|
: {
|
|
77
|
-
allUsers: manifest.compose || manifest.console
|
|
77
|
+
allUsers: manifest.compose || manifest.console || !hasSdkWindow(manifest)
|
|
78
78
|
? manifest.identityGateway?.access !== "dsm-admins"
|
|
79
79
|
: permitsOrdinaryUsers(manifest.apiPermissions),
|
|
80
80
|
};
|
|
@@ -107,6 +107,10 @@ function displayNameCatalogs(manifest) {
|
|
|
107
107
|
}
|
|
108
108
|
return catalogs;
|
|
109
109
|
}
|
|
110
|
+
/** Whether the App ships the SDK window; Compose and `ui: false` Apps open the gateway instead. */
|
|
111
|
+
export function hasSdkWindow(manifest) {
|
|
112
|
+
return !manifest.compose && manifest.sdkWindow !== false;
|
|
113
|
+
}
|
|
110
114
|
/** The DSM app class whose launch opens the SDK window: the console's when the App declares one. */
|
|
111
115
|
export function windowAppId(manifest) {
|
|
112
116
|
return `SYNO.SDS.${manifest.package}.${manifest.console ? "Console" : "Instance"}`;
|
|
@@ -128,7 +132,7 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon), lo
|
|
|
128
132
|
const icon = windowIconTemplate(icons);
|
|
129
133
|
const ns = `SYNO.SDS.${manifest.package}`;
|
|
130
134
|
const titleFields = uiTitleFields(appTitle(manifest));
|
|
131
|
-
if (manifest
|
|
135
|
+
if (!hasSdkWindow(manifest)) {
|
|
132
136
|
if (!manifest.identityGateway)
|
|
133
137
|
return "{}\n";
|
|
134
138
|
return (JSON.stringify({
|
|
@@ -23,6 +23,7 @@ import { ELF_MACHINE } from "./binary.js";
|
|
|
23
23
|
import { CGI_PAYLOAD_PATH, entryLauncherAccess } from "./dsm-window.js";
|
|
24
24
|
import { daemonConfig, readDaemon, readLifecycleTimeouts, renderNodeLauncher, nodeDependency, NODE_LAUNCHER } from "./daemon.js";
|
|
25
25
|
import { DAEMON_INGRESS_CONFIG, daemonIngressMinDsmProblem, readDaemonIngress, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
26
|
+
import { daemonRestartSdkProblem, readDaemonRestart } from "./daemon-restart.js";
|
|
26
27
|
import { ROOT_SCRIPT_HINTS } from "./root-ops.js";
|
|
27
28
|
import { DELETE_STATE_KEY } from "../build-spk/uninstall-wizard.js";
|
|
28
29
|
import { readUninstallRecord, stateItemTexts } from "./uninstall.js";
|
|
@@ -114,7 +115,9 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
114
115
|
new Set(raw.capabilities).size !== raw.capabilities.length ||
|
|
115
116
|
(raw.entry !== undefined &&
|
|
116
117
|
!["admin-only", "app-privilege"].includes(raw.entry)) ||
|
|
117
|
-
(raw.console !== undefined && raw.console !== true)
|
|
118
|
+
(raw.console !== undefined && raw.console !== true) ||
|
|
119
|
+
(raw.window !== undefined &&
|
|
120
|
+
(raw.window !== false || raw.console !== undefined)))
|
|
118
121
|
throw new Error();
|
|
119
122
|
if (raw.lifecycleTimeouts !== undefined && (!readLifecycleTimeouts(raw.lifecycleTimeouts, () => { }) || Object.keys(raw.lifecycleTimeouts).length !== 4))
|
|
120
123
|
throw new Error();
|
|
@@ -126,6 +129,9 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
126
129
|
findings.push("sdk-package.json does not match the schema 2 capability inventory.");
|
|
127
130
|
}
|
|
128
131
|
}
|
|
132
|
+
const daemonRestart = recoverDaemonRestart(metadata.daemon_restart, metadata.daemon_sdk, findings);
|
|
133
|
+
if (metadata.daemon_restart !== undefined && (metadata.compose !== undefined || !paths.has("daemon/control")))
|
|
134
|
+
findings.push("DAEMON_RESTART_INVALID daemon restart requires a native daemon controller, without Compose supervision");
|
|
129
135
|
const identityGateway = await inspectProviderOnlyAdmission(dir, paths, metadata.provider_only_oidc, findings);
|
|
130
136
|
const installSettings = await inspectInstallSettings(dir, payload, metadata, findings);
|
|
131
137
|
if (metadata.compose !== undefined) {
|
|
@@ -210,7 +216,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
210
216
|
if (standalone && portal)
|
|
211
217
|
findings.push(...inspectPortalInfo(info, hasExecutor));
|
|
212
218
|
const daemon = standalone
|
|
213
|
-
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode })
|
|
219
|
+
? await checkDaemon(dir, info, paths, payload, findings, hasExecutor, background, combined, nativeGateway, uninstallHook?.uninstall, { ingress: daemonIngress, node: daemonNode, restart: daemonRestart })
|
|
214
220
|
: undefined;
|
|
215
221
|
if (background)
|
|
216
222
|
checkBackground(dir, info, payload, findings, hasExecutor, daemon, kind !== "native-go", metadata.lifecycleTimeouts, nativeGateway);
|
|
@@ -276,7 +282,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
276
282
|
}
|
|
277
283
|
if (metadata.entry)
|
|
278
284
|
await checkEntryLauncher(dir, info.package, metadata.entry, findings);
|
|
279
|
-
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings, metadata.console === true);
|
|
285
|
+
const launch = await checkLaunchConfiguration(dir, info, standalone, metadata.launch, portal?.alias ?? executor?.policy.portal?.alias, findings, metadata.console === true, metadata.window === false);
|
|
280
286
|
const loginStyle = standalone
|
|
281
287
|
? await inspectLoginStyle(dir, info.package, Boolean(portal || executor?.policy.portal), findings)
|
|
282
288
|
: undefined;
|
|
@@ -340,7 +346,7 @@ export async function inspectSpk(spkPath, expectedKind) {
|
|
|
340
346
|
const LAUNCHER_TYPE_MISMATCH = "LAUNCHER_TYPE_MISMATCH";
|
|
341
347
|
const LAUNCHER_PORTAL_MISMATCH = "LAUNCHER_PORTAL_MISMATCH";
|
|
342
348
|
/** Compare the actual launcher and Portal fields to the packaged declaration. */
|
|
343
|
-
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings, console = false) {
|
|
349
|
+
async function checkLaunchConfiguration(dir, info, native, declared, alias, findings, console = false, windowless = false) {
|
|
344
350
|
const target = join(dir, "launch-inspection");
|
|
345
351
|
mkdirSync(target);
|
|
346
352
|
await tar.x({
|
|
@@ -395,6 +401,10 @@ async function checkLaunchConfiguration(dir, info, native, declared, alias, find
|
|
|
395
401
|
checkConsoleLaunchers(target, info, native, findings);
|
|
396
402
|
return launch;
|
|
397
403
|
}
|
|
404
|
+
if (windowless) {
|
|
405
|
+
checkGatewayOnlyLauncher(target, info, native, findings);
|
|
406
|
+
return launch;
|
|
407
|
+
}
|
|
398
408
|
const type = dsmLauncherType(launch, native);
|
|
399
409
|
if (!launcher ||
|
|
400
410
|
launcher.type !== type ||
|
|
@@ -443,6 +453,36 @@ function checkConsoleLaunchers(target, info, native, findings) {
|
|
|
443
453
|
findings.push(`${LAUNCHER_TYPE_MISMATCH} console requires a url launcher at ${ns}.Instance to sso/open.cgi, an administrator-only app launcher at ${ns}.Console and INFO dsmapplaunchname=${ns}.Console; rebuild with synopkgland build`);
|
|
444
454
|
}
|
|
445
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* A `ui: false` App's ui/config: one url launcher at `.Instance` to the
|
|
458
|
+
* gateway, which INFO's adminurl also opens for Package Center's Open, and
|
|
459
|
+
* no SDK window.
|
|
460
|
+
*/
|
|
461
|
+
function checkGatewayOnlyLauncher(target, info, native, findings) {
|
|
462
|
+
const ns = `SYNO.SDS.${info.package}`;
|
|
463
|
+
const entry = `/webman/3rdparty/${info.package}/sso/open.cgi`;
|
|
464
|
+
let config = {};
|
|
465
|
+
try {
|
|
466
|
+
config = JSON.parse(readFileSync(join(target, "ui/config"), "utf8"));
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
/* reported below as a missing launcher */
|
|
470
|
+
}
|
|
471
|
+
const sections = Object.keys(config);
|
|
472
|
+
const instance = config[".url"]?.[`${ns}.Instance`];
|
|
473
|
+
if (!native ||
|
|
474
|
+
sections.length !== 1 ||
|
|
475
|
+
Object.keys(config[".url"] ?? {}).length !== 1 ||
|
|
476
|
+
instance?.type !== "url" ||
|
|
477
|
+
instance.url !== entry ||
|
|
478
|
+
instance.appWindow !== undefined ||
|
|
479
|
+
info.dsmuidir !== "ui" ||
|
|
480
|
+
info.dsmappname !== `${ns}.Instance` ||
|
|
481
|
+
info.dsmapplaunchname !== undefined ||
|
|
482
|
+
info.adminurl !== entry) {
|
|
483
|
+
findings.push(`${LAUNCHER_TYPE_MISMATCH} ui: false requires only a url launcher at ${ns}.Instance to sso/open.cgi and INFO adminurl to the same entry; rebuild with synopkgland build`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
446
486
|
/** The launcher in ui/config must carry exactly the access the declared entry mode fixes. */
|
|
447
487
|
async function checkEntryLauncher(dir, pkg, entry, findings) {
|
|
448
488
|
const target = join(dir, "entry-inspection");
|
|
@@ -877,15 +917,19 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
877
917
|
"timeouts",
|
|
878
918
|
"operation_seconds",
|
|
879
919
|
"uninstall_args",
|
|
920
|
+
"restart",
|
|
880
921
|
...(combined ? ["network"] : []),
|
|
881
922
|
].includes(key)))
|
|
882
923
|
throw new Error("package/config mismatch");
|
|
924
|
+
if (!isDeepStrictEqual(config.restart, extras.restart))
|
|
925
|
+
throw new Error("daemon restart differs from its build declaration");
|
|
883
926
|
const daemon = readDaemon({
|
|
884
927
|
exec: config.exec,
|
|
885
928
|
args: config.args,
|
|
886
929
|
uninstall_args: config.uninstall_args,
|
|
887
930
|
env: config.env,
|
|
888
931
|
timeouts: config.timeouts,
|
|
932
|
+
restart: config.restart,
|
|
889
933
|
targets: { x86_64: "payload" },
|
|
890
934
|
}, dir, ["x86_64"], () => { });
|
|
891
935
|
if (!daemon)
|
|
@@ -947,8 +991,18 @@ async function checkDaemon(dir, info, paths, payload, findings, executor, backgr
|
|
|
947
991
|
return daemon;
|
|
948
992
|
}
|
|
949
993
|
catch {
|
|
950
|
-
findings.push("daemon/config.json or the payload does not match the fixed daemon contract");
|
|
994
|
+
findings.push("daemon/config.json (including restart) or the payload does not match the fixed daemon contract");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function recoverDaemonRestart(value, sdk, findings) {
|
|
998
|
+
if (value === undefined && sdk === undefined)
|
|
999
|
+
return undefined;
|
|
1000
|
+
const policy = readDaemonRestart(value, () => { });
|
|
1001
|
+
if (!policy || !isDeepStrictEqual(policy, value) || typeof sdk !== "string" || daemonRestartSdkProblem(sdk)) {
|
|
1002
|
+
findings.push("DAEMON_RESTART_INVALID sdk-package.json must record a normalized daemon restart policy and SDK 0.26.0 or later");
|
|
1003
|
+
return undefined;
|
|
951
1004
|
}
|
|
1005
|
+
return policy;
|
|
952
1006
|
}
|
|
953
1007
|
/** INFO retains the localized title inputs used by build's ui/config renderer. */
|
|
954
1008
|
function archivedInfoTranslations(info) {
|
|
@@ -100,6 +100,12 @@ export interface StandaloneManifest {
|
|
|
100
100
|
entry?: EntryMode;
|
|
101
101
|
/** `portal`: registers the DSM Application Portal entry; an administrator activates it. */
|
|
102
102
|
portal?: PortalDeclaration;
|
|
103
|
+
/**
|
|
104
|
+
* `ui: false`: a native identity_gateway App with no SDK window. Its one
|
|
105
|
+
* main-menu icon and Package Center's Open both go to the gateway's web UI,
|
|
106
|
+
* as for a Compose App. Absent when the App has a `ui` directory.
|
|
107
|
+
*/
|
|
108
|
+
sdkWindow?: false;
|
|
103
109
|
/**
|
|
104
110
|
* `console`: the main menu gets two icons, the gateway's native UI in a new
|
|
105
111
|
* tab and the SDK UI in an administrator-only DSM window titled `title`
|
|
@@ -10,7 +10,8 @@ import { isNativeArchFamily, } from "../build-spk/dsm-arch.js";
|
|
|
10
10
|
import { checkGlossary, loadCatalogs, readInfoTranslations, } from "./i18n.js";
|
|
11
11
|
import { loadRootOps, readRootSettings, rootBinaryProblem, ROOT_OPS_DIR, } from "./root-ops.js";
|
|
12
12
|
import { readComposeApp } from "./compose.js";
|
|
13
|
-
import { readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
13
|
+
import { daemonSdkVersion, readDaemon, readLifecycleTimeouts } from "./daemon.js";
|
|
14
|
+
import { daemonRestartSdkProblem } from "./daemon-restart.js";
|
|
14
15
|
import { daemonIngressMinDsmProblem } from "./daemon-ingress.js";
|
|
15
16
|
import { readIdentityGateway, } from "./identity-gateway.js";
|
|
16
17
|
import { readUninstall } from "./uninstall.js";
|
|
@@ -283,6 +284,11 @@ function readManifest(raw, c) {
|
|
|
283
284
|
if (problem)
|
|
284
285
|
c.fail("STANDALONE_DAEMON_INGRESS_MIN_DSM", problem);
|
|
285
286
|
}
|
|
287
|
+
if (daemon?.restart && backend) {
|
|
288
|
+
const problem = daemonRestartSdkProblem(daemonSdkVersion({ backend, identityGateway }));
|
|
289
|
+
if (problem)
|
|
290
|
+
c.fail("STANDALONE_DAEMON_RESTART_SDK_REQUIRED", problem);
|
|
291
|
+
}
|
|
286
292
|
if ((daemon?.ingress || daemon?.node) && raw.identity_gateway !== undefined)
|
|
287
293
|
c.fail("STANDALONE_DAEMON_INVALID", "daemon.node and daemon.ingress run the App's own daemon; identity_gateway runs its gateway as the daemon. Use one or the other.");
|
|
288
294
|
if (compose && identityGateway && compose.sdk !== identityGateway.sdk)
|
|
@@ -338,7 +344,10 @@ function readManifest(raw, c) {
|
|
|
338
344
|
}
|
|
339
345
|
permissions.policy = floored.policy;
|
|
340
346
|
}
|
|
341
|
-
const
|
|
347
|
+
const noWindow = raw.compose === undefined && raw.ui === false;
|
|
348
|
+
if (noWindow)
|
|
349
|
+
readNoWindow(raw, capabilities, c);
|
|
350
|
+
const ui = raw.compose !== undefined || noWindow
|
|
342
351
|
? {
|
|
343
352
|
dir: c.root,
|
|
344
353
|
...(isRecord(raw.ui) && raw.ui.launch !== undefined
|
|
@@ -347,8 +356,8 @@ function readManifest(raw, c) {
|
|
|
347
356
|
}
|
|
348
357
|
: readUi(raw.ui, c);
|
|
349
358
|
if (raw.compose !== undefined &&
|
|
350
|
-
|
|
351
|
-
Object.keys(raw.ui).some((k) => k !== "launch"))
|
|
359
|
+
raw.ui !== undefined &&
|
|
360
|
+
(!isRecord(raw.ui) || Object.keys(raw.ui).some((k) => k !== "launch")))
|
|
352
361
|
c.fail("STANDALONE_UI_INVALID", "Compose uses the SDK gateway; ui accepts only launch.");
|
|
353
362
|
if (raw.compose !== undefined &&
|
|
354
363
|
ui?.launch !== undefined &&
|
|
@@ -466,6 +475,7 @@ function readManifest(raw, c) {
|
|
|
466
475
|
...(entry ? { entry } : {}),
|
|
467
476
|
...(portal ? { portal } : {}),
|
|
468
477
|
...(dsmConsole ? { console: dsmConsole } : {}),
|
|
478
|
+
...(noWindow ? { sdkWindow: false } : {}),
|
|
469
479
|
...(icon ? { icon } : {}),
|
|
470
480
|
...(loginStyle ? { loginStyle } : {}),
|
|
471
481
|
...(appScripts ? { appScripts } : {}),
|
|
@@ -539,6 +549,24 @@ function readConsole(raw, launch, c) {
|
|
|
539
549
|
c.fail("STANDALONE_CONSOLE_PORTAL", "console and portal both claim the App's SDK UI; declare one of them.");
|
|
540
550
|
return value.title === undefined ? {} : { title: value.title };
|
|
541
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* `ui: false`: the gateway's web UI is the App's only UI, so the App needs
|
|
554
|
+
* identity_gateway, and nothing may need the SDK window: a console or portal
|
|
555
|
+
* is that window, and root, delegate and WebAPI approvals and the standalone
|
|
556
|
+
* sign-in page are shown in it.
|
|
557
|
+
*/
|
|
558
|
+
function readNoWindow(raw, capabilities, c) {
|
|
559
|
+
const fail = (why) => c.fail("STANDALONE_UI_NO_WINDOW_INVALID", `ui: false ${why}`);
|
|
560
|
+
if (raw.identity_gateway === undefined)
|
|
561
|
+
fail("needs identity_gateway: its web UI is what the main-menu icon and Package Center's Open start.");
|
|
562
|
+
if (raw.console !== undefined)
|
|
563
|
+
fail("leaves no SDK window for the console; remove console, or declare ui.dir.");
|
|
564
|
+
if (raw.portal !== undefined)
|
|
565
|
+
fail("leaves no SDK window for the portal; remove portal, or declare ui.dir.");
|
|
566
|
+
for (const name of ["rootOps", "delegate", "webapi", "identity"])
|
|
567
|
+
if (capabilities[name])
|
|
568
|
+
fail(`cannot use capabilities.${name}: its approval or sign-in is shown in the SDK window; declare ui.dir.`);
|
|
569
|
+
}
|
|
542
570
|
/** `login_style`: a portal's colour, wallpaper (with its thumbnail) and title image, each optional. */
|
|
543
571
|
function readLoginStyle(value, portal, c) {
|
|
544
572
|
const invalid = (message) => c.fail("STANDALONE_LOGIN_STYLE_INVALID", message);
|
|
@@ -736,7 +764,7 @@ function goReadsTimeout(backend, c) {
|
|
|
736
764
|
}
|
|
737
765
|
function readUi(value, c) {
|
|
738
766
|
if (!isRecord(value)) {
|
|
739
|
-
return c.fail("STANDALONE_UI_INVALID", "ui must contain dir.");
|
|
767
|
+
return c.fail("STANDALONE_UI_INVALID", "ui must contain dir; a native identity_gateway App without an SDK window declares ui: false.");
|
|
740
768
|
}
|
|
741
769
|
if (value.launch !== undefined && !isWebLaunchMode(value.launch))
|
|
742
770
|
return c.fail("STANDALONE_UI_LAUNCH_INVALID", "ui.launch must be dsm-window or new-page. Application Portal is a separate opt-in.");
|
package/dist/standalone/spk.js
CHANGED
|
@@ -21,7 +21,7 @@ import { renderStandaloneScript } from "./lifecycle.js";
|
|
|
21
21
|
import { APP_SCRIPTS_DIR, APP_SCRIPTS_SPK_DIR } from "./app-scripts.js";
|
|
22
22
|
import { codedMessage } from "../errors.js";
|
|
23
23
|
import { DSM_LANGS, LOCALES } from "./i18n.js";
|
|
24
|
-
import { appTitle, CGI_PAYLOAD_PATH, renderUiConfig, renderWindowScript, windowScriptName, } from "./dsm-window.js";
|
|
24
|
+
import { appTitle, CGI_PAYLOAD_PATH, hasSdkWindow, renderUiConfig, renderWindowScript, windowScriptName, } from "./dsm-window.js";
|
|
25
25
|
import { writeUiTexts } from "../build-spk/ui-texts.js";
|
|
26
26
|
import { loginStyleAssets } from "../build-spk/login-style.js";
|
|
27
27
|
import { copyStandaloneNotices, copyNoticeTree } from "../build-spk/notices.js";
|
|
@@ -29,7 +29,7 @@ import { copyAppTree } from "./bridge-app.js";
|
|
|
29
29
|
import { isBridgeBackend } from "./bridge-backend.js";
|
|
30
30
|
import { hostScriptName, renderHostAdapterScript } from "./host-adapter.js";
|
|
31
31
|
import { writeComposePayload } from "./compose.js";
|
|
32
|
-
import { copyDaemonPayload, nodeDependency } from "./daemon.js";
|
|
32
|
+
import { copyDaemonPayload, daemonSdkVersion, nodeDependency } from "./daemon.js";
|
|
33
33
|
import { DAEMON_INGRESS_CONFIG, renderDaemonIngressConfig } from "./daemon-ingress.js";
|
|
34
34
|
import { GATEWAY_CGI_PATH, GATEWAY_CONFIG_PATH, GATEWAY_EXEC, identityGatewayConfig, identityGatewayDeclaration, } from "./identity-gateway.js";
|
|
35
35
|
import { BACKGROUND_UNIT_DIR, BACKGROUND_WORKER_PAYLOAD, backgroundUnitName, renderBackgroundUnit, BRIDGE_WORKER_ARGS, } from "./background.js";
|
|
@@ -77,7 +77,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
77
77
|
const icons = packageIcons(manifest.icon);
|
|
78
78
|
const uiRoot = join(staging, "package", "ui");
|
|
79
79
|
const appDir = join(uiRoot, "app");
|
|
80
|
-
if (
|
|
80
|
+
if (hasSdkWindow(manifest))
|
|
81
81
|
copyUiTree(manifest.ui.dir, appDir);
|
|
82
82
|
else
|
|
83
83
|
mkdirSync(appDir, { recursive: true });
|
|
@@ -88,7 +88,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
88
88
|
writeFile(join(uiRoot, "config"), renderUiConfig(manifest, icons, login?.fields));
|
|
89
89
|
for (const [path, bytes] of login?.files ?? [])
|
|
90
90
|
writeFile(join(uiRoot, path), bytes);
|
|
91
|
-
if (
|
|
91
|
+
if (hasSdkWindow(manifest))
|
|
92
92
|
writeFile(join(uiRoot, windowScriptName(manifest)), renderWindowScript(manifest, assetVersion, icons, options.portalText));
|
|
93
93
|
writeFile(join(uiRoot, hostScriptName(manifest.build)), renderHostAdapterScript(manifest.package));
|
|
94
94
|
writeDsmWindowIcons(uiRoot, icons);
|
|
@@ -235,6 +235,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
235
235
|
...(nativeGatewaySupported(manifest) ? { native_gateway: { sdk: manifest.identityGateway.sdk, listen: manifest.identityGateway.listen } } : {}),
|
|
236
236
|
...(manifest.daemon?.ingress ? { daemon_ingress: manifest.daemon.ingress } : {}),
|
|
237
237
|
...(manifest.daemon?.node ? { daemon_node: manifest.daemon.node } : {}),
|
|
238
|
+
...(manifest.daemon?.restart ? { daemon_restart: manifest.daemon.restart, daemon_sdk: daemonSdkVersion(manifest) } : {}),
|
|
238
239
|
launch: resolvedSdkLaunch(manifest),
|
|
239
240
|
capabilities: Object.keys(manifest.capabilities).sort(),
|
|
240
241
|
...(manifest.compose
|
|
@@ -248,6 +249,7 @@ export async function assembleStandaloneSpk(options) {
|
|
|
248
249
|
: {}),
|
|
249
250
|
...(manifest.entry ? { entry: manifest.entry } : {}),
|
|
250
251
|
...(manifest.console ? { console: true } : {}),
|
|
252
|
+
...(manifest.sdkWindow === false ? { window: false } : {}),
|
|
251
253
|
...(manifest.uninstall
|
|
252
254
|
? {
|
|
253
255
|
uninstall: uninstallRecord(manifest.uninstall, manifest.i18n?.catalogs),
|
|
@@ -353,15 +355,14 @@ export function renderInfo(manifest, arch, checksum, executorDigest) {
|
|
|
353
355
|
// Package Center writes INFO as root; root tasks accept only this executor.
|
|
354
356
|
if (executorDigest)
|
|
355
357
|
fields.push([EXECUTOR_ANCHOR, executorDigest]);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
358
|
+
// Package Center's Open goes to the gateway entry when there is no SDK window.
|
|
359
|
+
// A Compose App's entry lets the docker-project worker check availability;
|
|
360
|
+
// never start or repair Container Manager here.
|
|
361
|
+
if (!hasSdkWindow(manifest) && manifest.identityGateway)
|
|
362
|
+
fields.push([
|
|
363
|
+
"adminurl",
|
|
364
|
+
gatewayEntryPath(manifest),
|
|
365
|
+
]);
|
|
365
366
|
const backend = manifest.backend;
|
|
366
367
|
if (isBridgeBackend(backend) && backend.node)
|
|
367
368
|
fields.push(["install_dep_packages", backend.node.package]);
|
package/dist/template-paths.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The scaffold templates come from the
|
|
2
|
+
* The scaffold templates come from the locally installed @synopackageland/templates
|
|
3
3
|
* package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
|
|
4
4
|
*/
|
|
5
5
|
export declare function templatesRoot(env?: NodeJS.ProcessEnv, from?: string): string;
|
package/dist/template-paths.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { sdkRequirement } from "./runtime/sdk-source.js";
|
|
4
4
|
/**
|
|
5
|
-
* The scaffold templates come from the
|
|
5
|
+
* The scaffold templates come from the locally installed @synopackageland/templates
|
|
6
6
|
* package; SYNOPKGLAND_TEMPLATES_DIR points at a templates checkout instead.
|
|
7
7
|
*/
|
|
8
8
|
export function templatesRoot(env = process.env, from = import.meta.url) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@napi-rs/keyring": "^2.1.1",
|
|
30
|
-
"@synopackageland/templates": "0.
|
|
30
|
+
"@synopackageland/templates": "~0.27.0",
|
|
31
31
|
"ajv": "^8.17.1",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"json-canonicalize": "^3.0.1",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://synopkgland.com/schemas/daemon-restart/1",
|
|
4
|
+
"title": "Native daemon restart policy",
|
|
5
|
+
"description": "The daemon.restart declaration in schema-2 dsm-package.yaml. Requires SDK 0.26.0 or later. Omitted or zero numeric limits use SDK defaults; normalized max_seconds must be at least initial_seconds.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["mode"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"mode": { "type": "string", "enum": ["off", "on-failure"] },
|
|
11
|
+
"initial_seconds": { "type": "integer", "minimum": 0, "maximum": 3600, "default": 1 },
|
|
12
|
+
"max_seconds": { "type": "integer", "minimum": 0, "maximum": 3600, "default": 30 },
|
|
13
|
+
"max_attempts": { "type": "integer", "minimum": 0, "maximum": 100, "default": 5 },
|
|
14
|
+
"window_seconds": { "type": "integer", "minimum": 0, "maximum": 86400, "default": 300 }
|
|
15
|
+
}
|
|
16
|
+
}
|
package/skills/SKILL.md
CHANGED
|
@@ -5,6 +5,14 @@ description: Initialize, extend, check, build, inspect, and deploy Synology appl
|
|
|
5
5
|
|
|
6
6
|
# Independent Synology SPKs
|
|
7
7
|
|
|
8
|
+
CLI 0.27.x installs templates through `~0.27.0`. A templates patch needs only a
|
|
9
|
+
templates release; npm install/update selects a patch in that minor line. The CLI
|
|
10
|
+
uses the locally installed templates and reads their SDK pin for defaults; it
|
|
11
|
+
never downloads templates at runtime. Preserve the lockfile and use `npm ci` for
|
|
12
|
+
reproducible offline-ready installations. An SDK minor upgrade still requires a
|
|
13
|
+
CLI release with executor mappings/digests and `SDK_UNSUPPORTED` validation; a
|
|
14
|
+
templates patch must keep its SDK pin in the CLI-supported minor line.
|
|
15
|
+
|
|
8
16
|
Before releasing an executor App, read [reference/approval-upgrades.md](reference/approval-upgrades.md). Run build/inspect with the previous released SPK as `--baseline` and include the approval difference in release notes. Without a comparable baseline, retention is unknown.
|
|
9
17
|
|
|
10
18
|
Use this guide to initialize, build and inspect own applications or upstream OSS as independent packages. 依已實作能力區分 DSM session 檔案、私有 jobs 與 App 登入;delegate 已實作並隨 CLI 提供固定操作的離線委託(預設 `files.moveFromStaging`);共用 delegate/rootOps executor 的完整裝置生命週期驗收仍待完成,不得宣稱已通過;支援邊界依 delegate guide/support matrix,禁止發明新的身分平台。
|
|
@@ -25,6 +33,8 @@ Read [reference/lifecycle.md](reference/lifecycle.md) before editing `dsm-script
|
|
|
25
33
|
|
|
26
34
|
Read [reference/ui-vue.md](reference/ui-vue.md) when editing Vue components in the native-go Vue template; use the installed SDK contracts, keep DSM embedding boundaries, and verify the actual app.
|
|
27
35
|
|
|
36
|
+
Buttons follow DSM's policy ([reference/ui-vue.md](reference/ui-vue.md#buttons), the UI package's `docs/button-policy.md`): only the bar that closes a dialog, wizard or form draws capsules, with Cancel/Reset first and one primary (or one danger to confirm a deletion) last; toolbar, page, section and row actions are grey tool buttons, Delete included; links go to `Link`. Choose the variant, never the shape.
|
|
37
|
+
|
|
28
38
|
Read [reference/ui-json.md](reference/ui-json.md) when generating or editing JSON UI; discover the installed catalog, validate the spec, then render and fix layout issues.
|
|
29
39
|
|
|
30
40
|
## UI copywriting
|
|
@@ -176,6 +186,12 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
176
186
|
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
177
187
|
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
178
188
|
|
|
189
|
+
原生 App 搭配 `identity_gateway`、而且唯一的 UI 就是包裝的網頁時,宣告 `ui: false` 取代 `ui.dir`。
|
|
190
|
+
沒有 SDK 視窗:主選單只有一個 `displayname` 圖示,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI;
|
|
191
|
+
套件中心「開啟」經 INFO `adminurl` 開同一個入口,與 compose App 相同。backend 與 ctl 指令不受影響。
|
|
192
|
+
沒有 `identity_gateway`、搭配 `console` 或 `portal`、或使用 `capabilities.rootOps`、`delegate`、`webapi`、`identity`
|
|
193
|
+
(這些的核准或登入畫面在 SDK 視窗裡)都會被 check 以 `STANDALONE_UI_NO_WINDOW_INVALID` 拒絕。省略 `ui` 仍是錯誤。
|
|
194
|
+
|
|
179
195
|
Schema 2 Compose 使用 `dsm-package.yaml` 的 `compose.files`、`compose.web` 與
|
|
180
196
|
`identity_gateway`;不使用 file/targets/address_env 或手寫 upstream。
|
|
181
197
|
省略 `ui.launch` 時預設 `new-page`,使用 SDK standalone launcher,不帶 Application Portal。
|
|
@@ -232,7 +232,7 @@ declared availability readiness, LAN isolation/egress, protected grant lifecycle
|
|
|
232
232
|
ACLs, two-user visibility and clean install/upgrade/reboot/stop/uninstall before
|
|
233
233
|
calling the package supported on a NAS. No NAS is accessed by the build gate.
|
|
234
234
|
|
|
235
|
-
The unified Compose contract uses SDK 0.
|
|
235
|
+
The unified Compose contract uses SDK 0.27.x with CLI 0.27.x and locally installed templates from `~0.27.0`. Templates patches can ship independently; npm install/update resolves them, and SDK defaults come from the installed templates. SDK minor upgrades still require a CLI release with executor mappings/digests and `SDK_UNSUPPORTED` checks. The CLI does not fetch templates at runtime. The legacy packaging route and bridge publish mode are removed.
|
|
236
236
|
|
|
237
237
|
Primary override files, and each included override set, must share a directory. Cross-directory override bases are rejected before loader I/O. Included implicit .env files are confined, inventoried and preserved; symlinks are rejected.
|
|
238
238
|
|
|
@@ -66,6 +66,12 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
66
66
|
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
67
67
|
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
68
68
|
|
|
69
|
+
原生 App 搭配 `identity_gateway`、而且唯一的 UI 就是包裝的網頁時,宣告 `ui: false` 取代 `ui.dir`。
|
|
70
|
+
沒有 SDK 視窗:主選單只有一個 `displayname` 圖示,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI;
|
|
71
|
+
套件中心「開啟」經 INFO `adminurl` 開同一個入口,與 compose App 相同。backend 與 ctl 指令不受影響。
|
|
72
|
+
沒有 `identity_gateway`、搭配 `console` 或 `portal`、或使用 `capabilities.rootOps`、`delegate`、`webapi`、`identity`
|
|
73
|
+
(這些的核准或登入畫面在 SDK 視窗裡)都會被 check 以 `STANDALONE_UI_NO_WINDOW_INVALID` 拒絕。省略 `ui` 仍是錯誤。
|
|
74
|
+
|
|
69
75
|
Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
|
|
70
76
|
省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
|
|
71
77
|
省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
|
|
@@ -61,6 +61,31 @@ Reuse the embedded theme's typography, spacing and public controls. Preserve
|
|
|
61
61
|
loading, empty, error, disabled and permission states. Backend policies and DSM
|
|
62
62
|
checks decide authority; component visibility cannot grant access.
|
|
63
63
|
|
|
64
|
+
## Buttons
|
|
65
|
+
|
|
66
|
+
Follow DSM's button policy, in the installed
|
|
67
|
+
`node_modules/@synopackageland/ui/docs/button-policy.md`. Choose only the
|
|
68
|
+
`variant`; the container chooses the shape, so do not set `kind="action"`.
|
|
69
|
+
|
|
70
|
+
| Place | Use |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| Dialog, Sheet, Wizard footer; `FormActions`, `AppActionBar`, `Footbar`, Panel `#fbar` | capsule: Cancel or Reset `secondary`, then one `primary` last |
|
|
73
|
+
| Confirming a deletion | Dialog footer: Cancel, then one `danger` last |
|
|
74
|
+
| Toolbar, PageHeader, section or card body and `#footer`, beside a field | grey tool `Button` (Delete too) |
|
|
75
|
+
| List or grid row | `IconButton` with a `label` |
|
|
76
|
+
| Help, docs, external pages | `Link`, not a button |
|
|
77
|
+
|
|
78
|
+
- A capsule only ends something: it closes a dialog, applies a form or moves a
|
|
79
|
+
wizard. Disable this connection, Open or Check again stay tool buttons, even
|
|
80
|
+
in a `SettingsSection` `#footer`; put a section's Save / Cancel in `FormActions`.
|
|
81
|
+
- One main action per footer, placed last. Inside a page, `primary` only for
|
|
82
|
+
the one step that unblocks it (Sign in), at most one per section.
|
|
83
|
+
- Keep the default size; neighbouring buttons share one size. One refresh per
|
|
84
|
+
page, in the PageHeader.
|
|
85
|
+
- In development the UI warns in the console about a capsule outside a footer,
|
|
86
|
+
two main actions in a footer, or a main action that is not last. Clear these
|
|
87
|
+
warnings before handing over the app.
|
|
88
|
+
|
|
64
89
|
## Start fast
|
|
65
90
|
|
|
66
91
|
Every api.cgi call pays DSM's session check (about 60–240 ms on a NAS), so
|
|
@@ -165,6 +190,12 @@ Application Portal 是獨立 opt-in:不想要別名就省略 `portal`,不生
|
|
|
165
190
|
compose App、沒有 gateway 的 App、`ui.launch` 或 `portal` 都會被 check 拒絕。
|
|
166
191
|
冷登入由 url 圖示經 `open.cgi` 續接,`oidc.browser` 需要支援此續接的 SDK。
|
|
167
192
|
|
|
193
|
+
原生 App 搭配 `identity_gateway`、而且唯一的 UI 就是包裝的網頁時,宣告 `ui: false` 取代 `ui.dir`。
|
|
194
|
+
沒有 SDK 視窗:主選單只有一個 `displayname` 圖示,是 `type: "url"`,經 `sso/open.cgi` 在新分頁開原生 UI;
|
|
195
|
+
套件中心「開啟」經 INFO `adminurl` 開同一個入口,與 compose App 相同。backend 與 ctl 指令不受影響。
|
|
196
|
+
沒有 `identity_gateway`、搭配 `console` 或 `portal`、或使用 `capabilities.rootOps`、`delegate`、`webapi`、`identity`
|
|
197
|
+
(這些的核准或登入畫面在 SDK 視窗裡)都會被 check 以 `STANDALONE_UI_NO_WINDOW_INVALID` 拒絕。省略 `ui` 仍是錯誤。
|
|
198
|
+
|
|
168
199
|
Docker/web service App 設定 `integration.web.launch`,內建預設是 `new-page`;
|
|
169
200
|
省略開啟方式時直接開新分頁,不宣告 Application Portal。SDK 原生 App
|
|
170
201
|
省略時使用 `dsm-window`;非 UI SDK 原生 App 必須明確寫 `ui.launch: new-page`。
|