@synopackageland/cli 0.13.1 → 0.15.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 +18 -0
- package/README.md +44 -1
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -850
- package/dist/build-spk/resource-worker.d.ts +2 -14
- package/dist/build-spk/resource-worker.js +3 -166
- package/dist/cli.js +208 -56
- package/dist/compose/analyze.d.ts +3 -61
- package/dist/compose/analyze.js +35 -776
- package/dist/compose/env.d.ts +5 -3
- package/dist/compose/env.js +17 -25
- package/dist/compose/runner.js +23 -6
- package/dist/contract/app-launch.d.ts +11 -0
- package/dist/contract/app-launch.js +18 -0
- package/dist/contract/web-launch.d.ts +3 -13
- package/dist/contract/web-launch.js +14 -81
- package/dist/ctl/cli.d.ts +36 -0
- package/dist/ctl/cli.js +218 -0
- package/dist/ctl/descriptor.d.ts +81 -0
- package/dist/ctl/descriptor.js +93 -0
- package/dist/ctl/dsm-auth.d.ts +10 -0
- package/dist/ctl/dsm-auth.js +30 -0
- package/dist/ctl/errors.d.ts +17 -0
- package/dist/ctl/errors.js +32 -0
- package/dist/ctl/flows.d.ts +11 -0
- package/dist/ctl/flows.js +52 -0
- package/dist/ctl/input.d.ts +20 -0
- package/dist/ctl/input.js +128 -0
- package/dist/ctl/json.d.ts +2 -0
- package/dist/ctl/json.js +38 -0
- package/dist/ctl/redaction.d.ts +15 -0
- package/dist/ctl/redaction.js +198 -0
- package/dist/ctl/service.d.ts +38 -0
- package/dist/ctl/service.js +156 -0
- package/dist/ctl/store.d.ts +51 -0
- package/dist/ctl/store.js +93 -0
- package/dist/ctl/transfers.d.ts +14 -0
- package/dist/ctl/transfers.js +74 -0
- package/dist/ctl/transport.d.ts +11 -0
- package/dist/ctl/transport.js +79 -0
- package/dist/deploy/deploy.d.ts +4 -3
- package/dist/deploy/deploy.js +5 -210
- package/dist/deploy/plan.js +26 -7
- package/dist/dev.js +5 -4
- package/dist/error-registry.js +1020 -831
- package/dist/info.js +120 -28
- package/dist/init.js +188 -64
- package/dist/inspect.js +21 -6
- package/dist/runtime/executor-digests.json +1 -12
- package/dist/skill.js +51 -9
- package/dist/standalone/api-description-inspect.d.ts +4 -0
- package/dist/standalone/api-description-inspect.js +12 -0
- package/dist/standalone/api-description.d.ts +7 -0
- package/dist/standalone/api-description.js +73 -0
- package/dist/standalone/build.d.ts +3 -0
- package/dist/standalone/build.js +140 -33
- package/dist/standalone/compose-errors.d.ts +3 -0
- package/dist/standalone/compose-errors.js +243 -0
- package/dist/standalone/compose.d.ts +97 -10
- package/dist/standalone/compose.js +306 -75
- package/dist/standalone/daemon.js +111 -29
- package/dist/standalone/dsm-window.js +55 -9
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +15 -0
- package/dist/standalone/inspect-spk.js +714 -128
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +10 -1
- package/dist/standalone/manifest.js +308 -87
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +139 -39
- package/dist/template-paths.d.ts +0 -2
- package/dist/template-paths.js +0 -5
- package/dist/test-command.d.ts +1 -1
- package/dist/test-command.js +1 -1
- package/dist/validate.d.ts +3 -3
- package/dist/validate.js +32 -140
- package/package.json +5 -3
- package/schema/synology-app.schema.json +1 -1
- package/schema/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +34 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +26 -4
- package/skills/reference/ctl.md +41 -0
- package/skills/reference/deploy.md +1 -1
- package/skills/reference/files.md +4 -0
- package/skills/reference/identity-gateway.md +8 -138
- package/skills/reference/portal.md +31 -0
- package/skills/reference/ui-vue.md +31 -0
- package/skills/reference/wrap.md +10 -30
- package/assets/go/daemon/fixture_linux_test.go +0 -15
- package/assets/go/daemon/fixture_other_test.go +0 -7
- package/assets/go/daemon/go.mod +0 -5
- package/assets/go/daemon/main.go +0 -636
- package/assets/go/daemon/main_test.go +0 -315
- package/assets/go/daemon/network.go +0 -107
- package/assets/go/daemon/network_test.go +0 -86
- package/assets/go/daemon/process_linux.go +0 -55
- package/assets/go/daemon/process_linux_test.go +0 -161
- package/assets/go/daemon/process_other.go +0 -14
- package/assets/go/daemon/testdata/child.go +0 -44
- package/assets/go/gateway/auth.go +0 -101
- package/assets/go/gateway/auth_test.go +0 -216
- package/assets/go/gateway/cgi.go +0 -133
- package/assets/go/gateway/cgi_test.go +0 -156
- package/assets/go/gateway/csrf.go +0 -73
- package/assets/go/gateway/csrf_test.go +0 -155
- package/assets/go/gateway/directory.go +0 -108
- package/assets/go/gateway/directory_test.go +0 -202
- package/assets/go/gateway/gateway.go +0 -412
- package/assets/go/gateway/gateway_test.go +0 -676
- package/assets/go/gateway/go.mod +0 -5
- package/assets/go/gateway/groups.go +0 -66
- package/assets/go/gateway/main.go +0 -45
- package/dist/build-spk/gateway.d.ts +0 -31
- package/dist/build-spk/gateway.js +0 -60
- package/dist/compose/privilege.d.ts +0 -14
- package/dist/compose/privilege.js +0 -102
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import { type ResourceWorkerEligibility } from "../compose/analyze.js";
|
|
3
|
-
export { classifyComposeRoute, resourceWorkerEligibility, type ComposeRoute, type ComposeRouteClassification, type ResourceWorkerEligibility, type ResourceWorkerIneligibleReason, } from "../compose/analyze.js";
|
|
1
|
+
/** Static metadata for DSM's official Container Manager worker. No runtime code. */
|
|
4
2
|
export declare function composeProjectName(packageId: string): string;
|
|
5
3
|
/**
|
|
6
4
|
* `forcePull` re-resolves every image tag on install, which can move an image
|
|
7
5
|
* shared with other containers. Digest-pinned images cannot move, so it is only
|
|
8
6
|
* needed (and kept, to preserve upgrade behaviour) while a tag is unpinned.
|
|
9
7
|
*/
|
|
10
|
-
export declare function renderDockerProjectResource(packageId: string, projectPath?: string, forcePull?: boolean): Record<string, unknown>;
|
|
11
|
-
export declare function renderWebserviceResource(contract: AppContract, adminport: number): Record<string, unknown>;
|
|
12
|
-
export declare function renderResourceConfig(contract: AppContract, eligibility: ResourceWorkerEligibility, compose: ComposeDocument): string;
|
|
13
|
-
/**
|
|
14
|
-
* Renders the lifecycle control script for the Docker Project resource-worker
|
|
15
|
-
* route. ContainerManager owns the actual project start/stop operation through
|
|
16
|
-
* the worker timing hooks; the package script must wait for that operation to
|
|
17
|
-
* leave a reachable service and must not claim running unconditionally.
|
|
18
|
-
*/
|
|
19
|
-
export declare function renderResourceWorkerStartStopStatus(packageId: string, adminport?: number, headless?: boolean): string;
|
|
20
|
-
export declare function renderHeadlessResourceMarker(): string;
|
|
8
|
+
export declare function renderDockerProjectResource(packageId: string, projectPath?: string, forcePull?: boolean, projectNameOverride?: string): Record<string, unknown>;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { cookieStripHeaders, resolvedWebLaunch, usesAuthGateway, webStationAlias, webStationServiceName, } from "../contract/web-launch.js";
|
|
3
|
-
export { classifyComposeRoute, resourceWorkerEligibility, } from "../compose/analyze.js";
|
|
1
|
+
/** Static metadata for DSM's official Container Manager worker. No runtime code. */
|
|
4
2
|
export function composeProjectName(packageId) {
|
|
5
3
|
const slug = packageId
|
|
6
4
|
.toLowerCase()
|
|
@@ -13,8 +11,8 @@ export function composeProjectName(packageId) {
|
|
|
13
11
|
* shared with other containers. Digest-pinned images cannot move, so it is only
|
|
14
12
|
* needed (and kept, to preserve upgrade behaviour) while a tag is unpinned.
|
|
15
13
|
*/
|
|
16
|
-
export function renderDockerProjectResource(packageId, projectPath = "compose", forcePull = true) {
|
|
17
|
-
const projectName = composeProjectName(packageId);
|
|
14
|
+
export function renderDockerProjectResource(packageId, projectPath = "compose", forcePull = true, projectNameOverride) {
|
|
15
|
+
const projectName = projectNameOverride ?? composeProjectName(packageId);
|
|
18
16
|
return {
|
|
19
17
|
"docker-project": {
|
|
20
18
|
projects: [
|
|
@@ -31,164 +29,3 @@ export function renderDockerProjectResource(packageId, projectPath = "compose",
|
|
|
31
29
|
},
|
|
32
30
|
};
|
|
33
31
|
}
|
|
34
|
-
export function renderWebserviceResource(contract, adminport) {
|
|
35
|
-
const alias = webStationAlias(String(contract.package));
|
|
36
|
-
const launch = resolvedWebLaunch(contract);
|
|
37
|
-
const cookieHeaders = cookieStripHeaders(contract);
|
|
38
|
-
const proxyHeaderEntries = cookieHeaders.map((header) => ({
|
|
39
|
-
name: header.name,
|
|
40
|
-
value: header.value,
|
|
41
|
-
}));
|
|
42
|
-
const service = {
|
|
43
|
-
service: webStationServiceName(alias),
|
|
44
|
-
display_name: String(contract.displayname ?? contract.package),
|
|
45
|
-
type: "reverse_proxy",
|
|
46
|
-
support_alias: true,
|
|
47
|
-
support_server: false,
|
|
48
|
-
proxy_target: `http://127.0.0.1:${adminport}`,
|
|
49
|
-
proxy_headers: proxyHeaderEntries,
|
|
50
|
-
};
|
|
51
|
-
const portal = {
|
|
52
|
-
service: webStationServiceName(alias),
|
|
53
|
-
name: alias,
|
|
54
|
-
type: "alias",
|
|
55
|
-
alias,
|
|
56
|
-
};
|
|
57
|
-
if (launch === "dsm-window") {
|
|
58
|
-
portal.app = String(contract.package);
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
webservice: {
|
|
62
|
-
services: [service],
|
|
63
|
-
portals: [portal],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
export function renderResourceConfig(contract, eligibility, compose) {
|
|
68
|
-
const packageId = String(contract.package);
|
|
69
|
-
const forcePull = collectUnpinnedImages(compose).length > 0;
|
|
70
|
-
const dockerResource = renderDockerProjectResource(packageId, "compose", forcePull);
|
|
71
|
-
let combined = { ...dockerResource };
|
|
72
|
-
// A gateway package is reached through its DSM-authenticated CGI only; a Web
|
|
73
|
-
// Station portal in front of the container would be an anonymous second door.
|
|
74
|
-
if (eligibility.adminport !== undefined && !usesAuthGateway(contract)) {
|
|
75
|
-
const webResource = renderWebserviceResource(contract, eligibility.adminport);
|
|
76
|
-
combined = {
|
|
77
|
-
...combined,
|
|
78
|
-
...webResource,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return `${JSON.stringify(combined, null, 2)}\n`;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Renders the lifecycle control script for the Docker Project resource-worker
|
|
85
|
-
* route. ContainerManager owns the actual project start/stop operation through
|
|
86
|
-
* the worker timing hooks; the package script must wait for that operation to
|
|
87
|
-
* leave a reachable service and must not claim running unconditionally.
|
|
88
|
-
*/
|
|
89
|
-
export function renderResourceWorkerStartStopStatus(packageId, adminport, headless = false) {
|
|
90
|
-
if (headless) {
|
|
91
|
-
return `#!/bin/sh
|
|
92
|
-
SYNOPKG_PKGNAME="\${SYNOPKG_PKGNAME:-${packageId}}"
|
|
93
|
-
SYNOPKGLAND_PROC_ROOT="\${SYNOPKGLAND_PROC_ROOT:-/proc}"
|
|
94
|
-
|
|
95
|
-
probe_project_process() {
|
|
96
|
-
for p in "$SYNOPKGLAND_PROC_ROOT"/[0-9]*; do
|
|
97
|
-
[ -d "$p" ] || continue
|
|
98
|
-
if grep -q docker "$p/cgroup" 2>/dev/null &&
|
|
99
|
-
grep -q \\
|
|
100
|
-
"@appstore/\${SYNOPKG_PKGNAME}/compose/" \\
|
|
101
|
-
"$p/mountinfo" 2>/dev/null; then
|
|
102
|
-
return 0
|
|
103
|
-
fi
|
|
104
|
-
done
|
|
105
|
-
return 1
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
case "$1" in
|
|
109
|
-
start|stop) exit 0 ;;
|
|
110
|
-
status)
|
|
111
|
-
probe_project_process
|
|
112
|
-
exit $?
|
|
113
|
-
;;
|
|
114
|
-
*) exit 1 ;;
|
|
115
|
-
esac
|
|
116
|
-
`;
|
|
117
|
-
}
|
|
118
|
-
if (adminport === undefined) {
|
|
119
|
-
return `#!/bin/sh
|
|
120
|
-
case "$1" in
|
|
121
|
-
start|status)
|
|
122
|
-
echo "resource-worker package has no published port to probe" >&2
|
|
123
|
-
exit 3
|
|
124
|
-
;;
|
|
125
|
-
stop) exit 0 ;;
|
|
126
|
-
*) exit 1 ;;
|
|
127
|
-
esac
|
|
128
|
-
`;
|
|
129
|
-
}
|
|
130
|
-
const url = `http://127.0.0.1:${adminport}/`;
|
|
131
|
-
return `#!/bin/sh
|
|
132
|
-
probe_service() {
|
|
133
|
-
if command -v curl >/dev/null 2>&1; then
|
|
134
|
-
curl --silent --output /dev/null --connect-timeout 2 --max-time 5 "${url}"
|
|
135
|
-
return $?
|
|
136
|
-
fi
|
|
137
|
-
if command -v wget >/dev/null 2>&1; then
|
|
138
|
-
wget --quiet --output-document=/dev/null --timeout=5 --tries=1 "${url}"
|
|
139
|
-
return $?
|
|
140
|
-
fi
|
|
141
|
-
return 127
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
wait_for_service() {
|
|
145
|
-
attempts=0
|
|
146
|
-
while [ "$attempts" -lt 30 ]; do
|
|
147
|
-
if probe_service; then
|
|
148
|
-
return 0
|
|
149
|
-
fi
|
|
150
|
-
attempts=$((attempts + 1))
|
|
151
|
-
sleep 1
|
|
152
|
-
done
|
|
153
|
-
return 1
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case "$1" in
|
|
157
|
-
start)
|
|
158
|
-
if wait_for_service; then
|
|
159
|
-
exit 0
|
|
160
|
-
fi
|
|
161
|
-
exit 3
|
|
162
|
-
;;
|
|
163
|
-
stop)
|
|
164
|
-
exit 0
|
|
165
|
-
;;
|
|
166
|
-
status)
|
|
167
|
-
if probe_service; then
|
|
168
|
-
exit 0
|
|
169
|
-
fi
|
|
170
|
-
exit 3
|
|
171
|
-
;;
|
|
172
|
-
*)
|
|
173
|
-
exit 1
|
|
174
|
-
;;
|
|
175
|
-
esac
|
|
176
|
-
`;
|
|
177
|
-
}
|
|
178
|
-
export function renderHeadlessResourceMarker() {
|
|
179
|
-
return `#!/bin/sh
|
|
180
|
-
set -eu
|
|
181
|
-
PKGVAR="\${SYNOPKG_PKGVAR:?}"
|
|
182
|
-
[ -d "$PKGVAR" ] || mkdir -p "$PKGVAR"
|
|
183
|
-
[ ! -L "$PKGVAR" ] || { echo "package var is a symlink" >&2; exit 1; }
|
|
184
|
-
MARKER="$PKGVAR/.docker-project-managed"
|
|
185
|
-
[ ! -L "$MARKER" ] || { echo "docker-project marker is a symlink" >&2; exit 1; }
|
|
186
|
-
tmp="$(mktemp "$PKGVAR/.docker-project-managed.tmp.XXXXXX")"
|
|
187
|
-
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
188
|
-
printf '%s\n' managed > "$tmp"
|
|
189
|
-
chmod 600 "$tmp"
|
|
190
|
-
mv "$tmp" "$MARKER"
|
|
191
|
-
trap - EXIT HUP INT TERM
|
|
192
|
-
exit 0
|
|
193
|
-
`;
|
|
194
|
-
}
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { codedMessage, formatErrors } from "./errors.js";
|
|
|
9
9
|
import { getErrorCode } from "./error-registry.js";
|
|
10
10
|
import { formatInspectReport, inspectProject } from "./inspect.js";
|
|
11
11
|
import { formatPinReport, pinProjectImages } from "./images/pin.js";
|
|
12
|
-
import { initFromTemplate, initNextSteps, isTemplateName, listTemplates } from "./init.js";
|
|
12
|
+
import { initFromTemplate, initNextSteps, isTemplateName, listTemplates, } from "./init.js";
|
|
13
13
|
import { templatesRoot } from "./template-paths.js";
|
|
14
14
|
import { addCapabilities } from "./add.js";
|
|
15
15
|
import { CAPABILITY_NAMES } from "./capabilities.js";
|
|
@@ -29,9 +29,9 @@ import { followProjectLogs, followStandaloneLogs } from "./log.js";
|
|
|
29
29
|
import { formatSkillList, readSkill, resolveSkillsRoot } from "./skill.js";
|
|
30
30
|
import { resolveInstallParameterExtraValues } from "./deploy/param-values.js";
|
|
31
31
|
import { findStandaloneRoot, loadStandaloneManifest, } from "./standalone/manifest.js";
|
|
32
|
-
import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES } from "./standalone/native-layout.js";
|
|
32
|
+
import { buildNativeLayout, isNativeLayout, loadNativeLayout, LAYOUT_ARCHES, } from "./standalone/native-layout.js";
|
|
33
33
|
import { buildStandalone, runnerFor } from "./standalone/build.js";
|
|
34
|
-
import { checkJobs, commitOutputs, forEachArch } from "./standalone/arch-jobs.js";
|
|
34
|
+
import { checkJobs, commitOutputs, forEachArch, } from "./standalone/arch-jobs.js";
|
|
35
35
|
import { checkReproducible } from "./reproducible.js";
|
|
36
36
|
import { inspectAnySpk } from "./spk/inspect.js";
|
|
37
37
|
function failure(code, message, hint = "Fix the configuration and retry; use --help to see command usage.") {
|
|
@@ -61,9 +61,19 @@ function formatIconReport(report, manifestFile) {
|
|
|
61
61
|
lines: report.source === "custom"
|
|
62
62
|
? [`Icon: custom ${report.path}`]
|
|
63
63
|
: report.source === "layout"
|
|
64
|
-
? [
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
? [
|
|
65
|
+
"Icon: provided by the layout metadata's PACKAGE_ICON*.PNG (if present)",
|
|
66
|
+
`info: set icon: in ${manifestFile} to have the CLI generate the Package Center icon.`,
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
"Icon: SDK default icon",
|
|
70
|
+
`info: set icon: in ${manifestFile} to <a square PNG (at least 256x256) or an icon_<size>.png directory> to use your own icon.`,
|
|
71
|
+
],
|
|
72
|
+
...(report.solid
|
|
73
|
+
? {
|
|
74
|
+
warning: "warning: the icon is a single colour, so Package Center and the DSM main menu show a flat block; use an icon with a graphic.",
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
const LABEL = { error: "error", warning: "warning" };
|
|
@@ -95,8 +105,12 @@ function formatSpkChecks(report) {
|
|
|
95
105
|
function formatRootSteps(report) {
|
|
96
106
|
const policy = report.executor?.policy;
|
|
97
107
|
const steps = [
|
|
98
|
-
...(policy?.start
|
|
99
|
-
|
|
108
|
+
...(policy?.start
|
|
109
|
+
? [`start ${policy.start} (once after each install or upgrade)`]
|
|
110
|
+
: []),
|
|
111
|
+
...(policy?.stop
|
|
112
|
+
? [`stop ${policy.stop} (at package stop and upgrade)`]
|
|
113
|
+
: []),
|
|
100
114
|
...(policy?.purge ? [`purge ${policy.purge} (at uninstall)`] : []),
|
|
101
115
|
];
|
|
102
116
|
return steps.length ? [`Root steps: ${steps.join(", ")}`] : [];
|
|
@@ -105,19 +119,47 @@ function formatSpkReport(spk, report) {
|
|
|
105
119
|
return [
|
|
106
120
|
`SPK: ${spk}`,
|
|
107
121
|
`package: ${report.package ?? "?"} version: ${report.version ?? "?"}`,
|
|
108
|
-
report.synopkgland
|
|
122
|
+
report.synopkgland
|
|
123
|
+
? `arch: ${report.arch ?? "?"} runtime: ${report.kind ?? "?"}`
|
|
124
|
+
: `arch: ${report.arch ?? "?"} build: not synopkgland (DSM format only)`,
|
|
125
|
+
...(report.compose
|
|
126
|
+
? [
|
|
127
|
+
`Compose readiness: ${report.compose.readiness.profile}`,
|
|
128
|
+
report.compose.readiness.limitation,
|
|
129
|
+
]
|
|
130
|
+
: []),
|
|
109
131
|
`Signature: ${report.signed ? "Synology-signed" : "unsigned (Package Center must allow any publisher)"}`,
|
|
110
132
|
...(report.synopkgland ? [`CGI: ${report.cgi ?? "none"}`] : []),
|
|
111
|
-
...(report.icon
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
...(report.icon
|
|
134
|
+
? [
|
|
135
|
+
`Icon: ${report.icon.source === "default" ? "SDK default icon" : report.icon.source === "custom" ? `custom (PACKAGE_ICON_256 sha256 ${report.icon.sha256.slice(0, 12)}…)` : "no PACKAGE_ICON"}`,
|
|
136
|
+
]
|
|
137
|
+
: []),
|
|
138
|
+
...(report.entry === "admin-only"
|
|
139
|
+
? [
|
|
140
|
+
"Entry: entry: admin-only; only a DSM administrator can open it and call any route (including SDK routes)",
|
|
141
|
+
]
|
|
142
|
+
: []),
|
|
143
|
+
...(report.entry === "app-privilege"
|
|
144
|
+
? [
|
|
145
|
+
"Entry: entry: app-privilege; DSM Control Panel > Application Privileges decides who can use it",
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
114
148
|
...formatRootSteps(report),
|
|
115
|
-
...(report.appScripts
|
|
149
|
+
...(report.appScripts
|
|
150
|
+
? [
|
|
151
|
+
`App scripts: ${report.appScripts.join(", ")} (scripts/app, run by the generated scripts of the same name as the package account)`,
|
|
152
|
+
]
|
|
153
|
+
: []),
|
|
116
154
|
...(report.loginStyle ? [formatLoginStyle(report.loginStyle)] : []),
|
|
117
|
-
...(report.portal
|
|
155
|
+
...(report.portal
|
|
156
|
+
? [
|
|
157
|
+
`Application Portal: default alias /${report.portal.alias}/ (${report.portal.appId}), active after an administrator applies it`,
|
|
158
|
+
]
|
|
159
|
+
: []),
|
|
118
160
|
...report.capabilities.map((cap) => `Capability ${cap.name}: ${cap.requiresAdminApproval ? "needs DSM administrator approval" : "ready after install"}`),
|
|
119
161
|
"payload:",
|
|
120
|
-
...(report.uninstallCleanup ?? []).map(item => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
|
|
162
|
+
...(report.uninstallCleanup ?? []).map((item) => `Uninstall cleanup (if needed): ${item.target} — ${item.condition} ${item.action}`),
|
|
121
163
|
...report.payload.map((e) => ` ${e.mode} ${e.path}`),
|
|
122
164
|
...formatSpkChecks(report),
|
|
123
165
|
].join("\n");
|
|
@@ -131,8 +173,20 @@ async function standaloneBuild(root, arches, options) {
|
|
|
131
173
|
? resolve(options.outDir)
|
|
132
174
|
: undefined;
|
|
133
175
|
const result = isNativeLayout(root)
|
|
134
|
-
? await buildNativeLayout({
|
|
135
|
-
|
|
176
|
+
? await buildNativeLayout({
|
|
177
|
+
root,
|
|
178
|
+
arches,
|
|
179
|
+
outDir,
|
|
180
|
+
minDsm: options.minDsm,
|
|
181
|
+
jobs: options.jobs,
|
|
182
|
+
})
|
|
183
|
+
: await buildStandalone({
|
|
184
|
+
root,
|
|
185
|
+
arches: arches,
|
|
186
|
+
outDir,
|
|
187
|
+
minDsm: options.minDsm,
|
|
188
|
+
jobs: options.jobs,
|
|
189
|
+
});
|
|
136
190
|
result.warnings?.forEach((warning) => console.error(warning));
|
|
137
191
|
return result.outputs.map((output) => {
|
|
138
192
|
if (!options.output)
|
|
@@ -145,26 +199,46 @@ async function standaloneBuild(root, arches, options) {
|
|
|
145
199
|
/** What the reproducible check needs to know about the project being built. */
|
|
146
200
|
function reproducibleTarget(standalone, project, arches) {
|
|
147
201
|
if (!standalone)
|
|
148
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
root: project.root,
|
|
204
|
+
manifestPath: project.contractPath,
|
|
205
|
+
arches: arches ?? [],
|
|
206
|
+
};
|
|
149
207
|
const manifestPath = join(standalone, "dsm-package.yaml");
|
|
150
208
|
if (!isNativeLayout(standalone)) {
|
|
151
209
|
const loaded = loadStandaloneManifest(standalone);
|
|
152
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
root: standalone,
|
|
212
|
+
manifestPath,
|
|
213
|
+
arches: arches ?? (loaded.ok ? loaded.manifest.arches : []),
|
|
214
|
+
};
|
|
153
215
|
}
|
|
154
216
|
const loaded = loadNativeLayout(standalone);
|
|
155
217
|
if (!loaded.ok)
|
|
156
218
|
return { root: standalone, manifestPath, arches: arches ?? [] };
|
|
157
219
|
// Toolkit pins decide what the native hooks compile against.
|
|
158
|
-
const toolkits = createHash("sha256")
|
|
159
|
-
|
|
220
|
+
const toolkits = createHash("sha256")
|
|
221
|
+
.update(JSON.stringify(loaded.manifest.native.toolkits))
|
|
222
|
+
.digest("hex");
|
|
223
|
+
return {
|
|
224
|
+
root: standalone,
|
|
225
|
+
manifestPath,
|
|
226
|
+
arches: arches ?? loaded.manifest.arches,
|
|
227
|
+
extraInputs: { "native.toolkits": toolkits },
|
|
228
|
+
};
|
|
160
229
|
}
|
|
161
230
|
function reproducibleLines(report) {
|
|
162
231
|
if (report.skipped)
|
|
163
|
-
return [
|
|
232
|
+
return [
|
|
233
|
+
`Reproducible check: the dependency fingerprint ${report.fingerprint.slice(0, 12)} matches the last passing run, skipping.`,
|
|
234
|
+
];
|
|
164
235
|
return report.results.map((result) => result.identical
|
|
165
236
|
? `Reproducible check: ${basename(result.output)} is identical (sha256 ${result.sha256[0].slice(0, 12)})`
|
|
166
237
|
: `Reproducible check: ${basename(result.output)} differs (sha256 ${result.sha256[0].slice(0, 12)} ≠ ${result.sha256[1].slice(0, 12)})\n` +
|
|
167
|
-
[
|
|
238
|
+
[
|
|
239
|
+
...result.payload.map((file) => ` payload ${file}`),
|
|
240
|
+
...result.metadata.map((file) => ` metadata ${file}`),
|
|
241
|
+
].join("\n"));
|
|
168
242
|
}
|
|
169
243
|
const ARCH_REQUIRED = [
|
|
170
244
|
"NATIVE_ARCH_FAMILY_REQUIRED",
|
|
@@ -177,8 +251,16 @@ async function containerBuild(project, arches, options) {
|
|
|
177
251
|
throw failure("CONTAINER_ARCH_UNSUPPORTED", `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${unsupported}.`);
|
|
178
252
|
const output = options.output ? resolve(options.output) : undefined;
|
|
179
253
|
const build = async (archFamily, outputPath = output) => {
|
|
180
|
-
const result = await buildSpk({
|
|
181
|
-
|
|
254
|
+
const result = await buildSpk({
|
|
255
|
+
project,
|
|
256
|
+
archFamily,
|
|
257
|
+
outputPath,
|
|
258
|
+
minDsm: options.minDsm,
|
|
259
|
+
gatewayRun: archFamily && runnerFor(undefined, jobs, archFamily),
|
|
260
|
+
});
|
|
261
|
+
if (!result.ok &&
|
|
262
|
+
!(archFamily === undefined &&
|
|
263
|
+
result.errors.every((code) => ARCH_REQUIRED.includes(code))))
|
|
182
264
|
throw failure(result.errors[0] ?? "BUILD_FAILED", `Build failed: ${result.errors.join(", ")}`);
|
|
183
265
|
return result;
|
|
184
266
|
};
|
|
@@ -190,9 +272,15 @@ async function containerBuild(project, arches, options) {
|
|
|
190
272
|
if (output)
|
|
191
273
|
throw failure("CLI_OUTPUT_ARCH_REQUIRED", "-o requires a single --arch.");
|
|
192
274
|
const packageId = String(loadContract(project.contractPath).contract.package);
|
|
193
|
-
return commitOutputs(options.outDir ? resolve(options.outDir) : project.root, (stage) => forEachArch(arches ?? CONTAINER_ARCH_FAMILIES, jobs, async (arch) => (await build(arch, join(stage, `${packageId}-${arch}.spk`)))
|
|
275
|
+
return commitOutputs(options.outDir ? resolve(options.outDir) : project.root, (stage) => forEachArch(arches ?? CONTAINER_ARCH_FAMILIES, jobs, async (arch) => (await build(arch, join(stage, `${packageId}-${arch}.spk`)))
|
|
276
|
+
.outputPath));
|
|
194
277
|
}
|
|
195
278
|
export async function runCli(argv) {
|
|
279
|
+
if (argv[2] === "ctl") {
|
|
280
|
+
const { runCtl } = await import("./ctl/cli.js");
|
|
281
|
+
await runCtl(argv.slice(3));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
196
284
|
const json = argv.includes("--json");
|
|
197
285
|
const stdout = console.log.bind(console);
|
|
198
286
|
const progress = console.log;
|
|
@@ -211,6 +299,7 @@ export async function runCli(argv) {
|
|
|
211
299
|
throw failure("NAS_CREDENTIALS_MISSING", "NAS credentials were not found.", "Set a synoagentcli profile or DSM_URL/DSM_PASSWORD.");
|
|
212
300
|
return nas;
|
|
213
301
|
};
|
|
302
|
+
program.command("ctl").description("Control registered NAS Apps using described AI commands (ctl --help)");
|
|
214
303
|
program
|
|
215
304
|
.command("check")
|
|
216
305
|
.description("Validate the manifest, capability combination and Compose")
|
|
@@ -223,26 +312,61 @@ export async function runCli(argv) {
|
|
|
223
312
|
throw failure(validation.errors[0].code, formatErrors(validation.errors), validation.errors[0].suggestion);
|
|
224
313
|
const standalone = standaloneRoot(rootOf(command));
|
|
225
314
|
if (standalone) {
|
|
226
|
-
const loaded = isNativeLayout(standalone)
|
|
315
|
+
const loaded = isNativeLayout(standalone)
|
|
316
|
+
? loadNativeLayout(standalone)
|
|
317
|
+
: loadStandaloneManifest(standalone);
|
|
227
318
|
if (!loaded.ok)
|
|
228
319
|
throw failure(loaded.errors[0].code, loaded.errors.map((e) => `[${e.code}] ${e.message}`).join("\n"), "Update dsm-package.yaml to schema: 2, fix the declaration and retry.");
|
|
229
|
-
const declared = "icon" in loaded.manifest && loaded.manifest.icon
|
|
230
|
-
|
|
320
|
+
const declared = "icon" in loaded.manifest && loaded.manifest.icon
|
|
321
|
+
? resolve(standalone, loaded.manifest.icon)
|
|
322
|
+
: undefined;
|
|
323
|
+
const icon = isNativeLayout(standalone) && !declared
|
|
324
|
+
? { source: "layout", solid: false }
|
|
325
|
+
: iconReport(standalone, declared);
|
|
231
326
|
const iconText = formatIconReport(icon, "dsm-package.yaml");
|
|
232
327
|
if (iconText.warning)
|
|
233
328
|
console.error(iconText.warning);
|
|
234
329
|
// A portal build carries the login style; a native layout keeps its own ui/config.
|
|
235
|
-
const loginStyle = !isNativeLayout(standalone) &&
|
|
236
|
-
|
|
330
|
+
const loginStyle = !isNativeLayout(standalone) &&
|
|
331
|
+
"portal" in loaded.manifest &&
|
|
332
|
+
loaded.manifest.portal
|
|
333
|
+
? loginStyleReport(standalone, "loginStyle" in loaded.manifest
|
|
334
|
+
? loaded.manifest.loginStyle
|
|
335
|
+
: undefined)
|
|
336
|
+
: undefined;
|
|
237
337
|
emit("check", {
|
|
238
338
|
package: loaded.manifest.package,
|
|
239
|
-
capabilities: "capabilities" in loaded.manifest
|
|
339
|
+
capabilities: "capabilities" in loaded.manifest
|
|
340
|
+
? loaded.manifest.capabilities
|
|
341
|
+
: {},
|
|
342
|
+
...("compose" in loaded.manifest && loaded.manifest.compose
|
|
343
|
+
? {
|
|
344
|
+
compose: {
|
|
345
|
+
images: loaded.manifest.compose.plan.images,
|
|
346
|
+
services: loaded.manifest.compose.plan.services,
|
|
347
|
+
mountsBlocked: loaded.manifest.compose.plan.mounts_blocked,
|
|
348
|
+
acceptanceBlockers: loaded.manifest.compose.plan.acceptance_blockers,
|
|
349
|
+
},
|
|
350
|
+
}
|
|
351
|
+
: {}),
|
|
240
352
|
icon,
|
|
241
353
|
...(loginStyle ? { loginStyle } : {}),
|
|
242
|
-
}, [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
354
|
+
}, [
|
|
355
|
+
"Validation passed.",
|
|
356
|
+
...("compose" in loaded.manifest && loaded.manifest.compose
|
|
357
|
+
? [
|
|
358
|
+
"Containers use the fixed package identity; signed-in File Station ACLs are not inherited.",
|
|
359
|
+
...loaded.manifest.compose.plan.acceptance_blockers.map((b) => `Acceptance blocker: ${b}`),
|
|
360
|
+
]
|
|
361
|
+
: []),
|
|
362
|
+
...iconText.lines,
|
|
363
|
+
...(loginStyle
|
|
364
|
+
? [
|
|
365
|
+
formatLoginStyle(loginStyle),
|
|
366
|
+
"info: set login_style: in dsm-package.yaml (color, wallpaper with wallpaper_thumbnail, title) to brand the Login Portal page.",
|
|
367
|
+
]
|
|
368
|
+
: []),
|
|
369
|
+
].join("\n"));
|
|
246
370
|
return;
|
|
247
371
|
}
|
|
248
372
|
const project = requireProject(rootOf(command));
|
|
@@ -256,7 +380,11 @@ export async function runCli(argv) {
|
|
|
256
380
|
const iconText = formatIconReport(icon, "synology-app.yaml");
|
|
257
381
|
if (iconText.warning)
|
|
258
382
|
console.error(iconText.warning);
|
|
259
|
-
emit("check", { report, warnings: result.warnings ?? [], icon }, [
|
|
383
|
+
emit("check", { report, warnings: result.warnings ?? [], icon }, [
|
|
384
|
+
`Validation passed.`,
|
|
385
|
+
formatInspectReport(report),
|
|
386
|
+
...iconText.lines,
|
|
387
|
+
].join("\n"));
|
|
260
388
|
});
|
|
261
389
|
program
|
|
262
390
|
.command("inspect <spk>")
|
|
@@ -390,8 +518,7 @@ export async function runCli(argv) {
|
|
|
390
518
|
arch: options.arch,
|
|
391
519
|
dev: options.dev,
|
|
392
520
|
});
|
|
393
|
-
if (plan.kind !== "container" &&
|
|
394
|
-
(options.share || options.runAs))
|
|
521
|
+
if (plan.kind !== "container" && (options.share || options.runAs))
|
|
395
522
|
throw failure("CLI_OPTION_INVALID", "native-go/binary do not accept --share/--run-as; pass install wizard values with --param.");
|
|
396
523
|
if (options.dryRun) {
|
|
397
524
|
emit("deploy", plan, JSON.stringify(plan, null, 2));
|
|
@@ -399,7 +526,9 @@ export async function runCli(argv) {
|
|
|
399
526
|
}
|
|
400
527
|
if (plan.kind !== "container") {
|
|
401
528
|
const host = hostOf(options.host);
|
|
402
|
-
const output = await deployStandalone(plan, host, {
|
|
529
|
+
const output = await deployStandalone(plan, host, {
|
|
530
|
+
params: options.param,
|
|
531
|
+
});
|
|
403
532
|
const status = await standaloneInfo({ root: plan.root, host });
|
|
404
533
|
emit("deploy", { ...plan, output, status }, `Deployed ${plan.packageId}: ${output}\n${formatProjectInfo(status)}${plan.notes.length ? "\n" + plan.notes.join("\n") : ""}`);
|
|
405
534
|
return;
|
|
@@ -472,7 +601,9 @@ export async function runCli(argv) {
|
|
|
472
601
|
if (result.diagnostics)
|
|
473
602
|
stdout(JSON.stringify(result.diagnostics, null, 2));
|
|
474
603
|
});
|
|
475
|
-
const skill = program
|
|
604
|
+
const skill = program
|
|
605
|
+
.command("skill")
|
|
606
|
+
.description("Bundled Agent Skill Pack");
|
|
476
607
|
skill.command("path").action(() => stdout(resolveSkillsRoot()));
|
|
477
608
|
skill.command("list").action(() => stdout(formatSkillList()));
|
|
478
609
|
skill.command("show <id>").action((id) => stdout(readSkill(id)));
|
|
@@ -482,14 +613,19 @@ export async function runCli(argv) {
|
|
|
482
613
|
.option("--json", "Print a single JSON object")
|
|
483
614
|
.option("-o, --output <path>", "Output SPK; use with a single --arch")
|
|
484
615
|
.option("--out-dir <dir>", "Output directory; native projects default to dist/")
|
|
485
|
-
.option("--arch <families>", `Architectures, comma-separated or repeated: ${NATIVE_ARCH_FAMILIES.join(",")} (armv7 is for native-go and binary, not for Container Manager Apps); native layout also supports armv8`, (value, previous) => [
|
|
616
|
+
.option("--arch <families>", `Architectures, comma-separated or repeated: ${NATIVE_ARCH_FAMILIES.join(",")} (armv7 is for native-go and binary, not for Container Manager Apps); native layout also supports armv8`, (value, previous) => [
|
|
617
|
+
...(previous ?? []),
|
|
618
|
+
value,
|
|
619
|
+
])
|
|
486
620
|
.option("--min-dsm <version>", "Minimum DSM version; uses the manifest value when omitted")
|
|
487
621
|
.option("--jobs <n>", "Number of architectures to build in parallel, default 1 (sequential)", Number)
|
|
488
622
|
.option("--verify-reproducible [when]", "Build again from a clean state and compare; when: changed (default, skips when the dependency fingerprint is unchanged) or always")
|
|
489
623
|
.action(async (options, command) => {
|
|
490
624
|
const root = rootOf(command);
|
|
491
625
|
const standalone = standaloneRoot(root);
|
|
492
|
-
const verify = options.verifyReproducible === true
|
|
626
|
+
const verify = options.verifyReproducible === true
|
|
627
|
+
? "changed"
|
|
628
|
+
: options.verifyReproducible;
|
|
493
629
|
if (verify !== undefined && verify !== "changed" && verify !== "always")
|
|
494
630
|
throw failure("CLI_OPTION_INVALID", `--verify-reproducible accepts only changed or always, got ${verify}.`);
|
|
495
631
|
let arches;
|
|
@@ -498,7 +634,10 @@ export async function runCli(argv) {
|
|
|
498
634
|
options.arch === undefined
|
|
499
635
|
? undefined
|
|
500
636
|
: standalone && isNativeLayout(standalone)
|
|
501
|
-
? options.arch
|
|
637
|
+
? options.arch
|
|
638
|
+
.join(",")
|
|
639
|
+
.split(",")
|
|
640
|
+
.map((arch) => {
|
|
502
641
|
if (!Object.hasOwn(LAYOUT_ARCHES, arch))
|
|
503
642
|
throw new Error(`Native layout arch must be one of ${Object.keys(LAYOUT_ARCHES).join(", ")}`);
|
|
504
643
|
return arch;
|
|
@@ -511,18 +650,29 @@ export async function runCli(argv) {
|
|
|
511
650
|
const project = standalone ? undefined : requireProject(root);
|
|
512
651
|
const build = (overrides = {}) => standalone
|
|
513
652
|
? standaloneBuild(standalone, arches, { ...options, ...overrides })
|
|
514
|
-
: containerBuild(project, arches, {
|
|
653
|
+
: containerBuild(project, arches, {
|
|
654
|
+
...options,
|
|
655
|
+
...overrides,
|
|
656
|
+
});
|
|
515
657
|
const outputs = await build();
|
|
516
|
-
const reproducible = verify === undefined
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
:
|
|
524
|
-
|
|
525
|
-
|
|
658
|
+
const reproducible = verify === undefined
|
|
659
|
+
? undefined
|
|
660
|
+
: await checkReproducible({
|
|
661
|
+
...reproducibleTarget(standalone, project, arches),
|
|
662
|
+
outputs,
|
|
663
|
+
mode: verify,
|
|
664
|
+
// The second build writes only to its own folder; a single Compose SPK keeps its name there.
|
|
665
|
+
rebuild: (outDir) => standalone || outputs.length > 1
|
|
666
|
+
? build({ output: undefined, outDir })
|
|
667
|
+
: build({
|
|
668
|
+
output: join(outDir, basename(outputs[0])),
|
|
669
|
+
outDir: undefined,
|
|
670
|
+
}),
|
|
671
|
+
});
|
|
672
|
+
const lines = [
|
|
673
|
+
...outputs.map((output) => `Built ${output}`),
|
|
674
|
+
...(reproducible ? reproducibleLines(reproducible) : []),
|
|
675
|
+
];
|
|
526
676
|
if (reproducible && !reproducible.identical)
|
|
527
677
|
throw Object.assign(failure("BUILD_NOT_REPRODUCIBLE", lines.join("\n"), "Find the payload files that differ between builds (timestamps, random paths, unpinned dependencies) and retry after fixing them."), { report: { outputs, reproducible } });
|
|
528
678
|
emit("build", { outputs, ...(reproducible ? { reproducible } : {}) }, lines.join("\n"));
|
|
@@ -553,7 +703,9 @@ export async function runCli(argv) {
|
|
|
553
703
|
ok: false,
|
|
554
704
|
code,
|
|
555
705
|
message,
|
|
556
|
-
hint: detail.hint ??
|
|
706
|
+
hint: detail.hint ??
|
|
707
|
+
getErrorCode(code)?.suggestion ??
|
|
708
|
+
"Fix the configuration and retry; use --help to see command usage.",
|
|
557
709
|
...(detail.report ? { data: detail.report } : {}),
|
|
558
710
|
}));
|
|
559
711
|
}
|