@synopackageland/cli 0.14.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 +10 -0
- package/README.md +26 -16
- package/dist/build-spk/generator.d.ts +3 -2
- package/dist/build-spk/generator.js +6 -846
- 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/web-launch.d.ts +1 -12
- package/dist/contract/web-launch.js +10 -74
- 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 -834
- 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 +39 -8
- package/dist/standalone/identity-gateway.d.ts +9 -2
- package/dist/standalone/identity-gateway.js +282 -66
- package/dist/standalone/inspect-spk.d.ts +12 -0
- package/dist/standalone/inspect-spk.js +666 -136
- package/dist/standalone/lifecycle.js +124 -42
- package/dist/standalone/manifest.d.ts +8 -1
- package/dist/standalone/manifest.js +303 -86
- package/dist/standalone/resource.js +6 -2
- package/dist/standalone/spk.d.ts +4 -1
- package/dist/standalone/spk.js +138 -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/synopkgland-api-v1.schema.json +423 -0
- package/skills/SKILL.md +3 -1
- package/skills/reference/compose.md +349 -0
- package/skills/reference/create.md +8 -17
- 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 -144
- package/skills/reference/wrap.md +10 -39
- 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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { strictJson } from './json.js';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import { CtlError } from './errors.js';
|
|
5
|
+
export const APP_COOKIE = Symbol("ctlAppCookie");
|
|
6
|
+
export function canonicalOrigin(input, tunnel = false) {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(input);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new CtlError('CTL_URL_INVALID', 'A NAS origin URL is required.', 'run: synopkgland ctl host add <name> --url https://<nas>:5001');
|
|
13
|
+
}
|
|
14
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || !(url.protocol === 'https:' || (tunnel && url.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(url.hostname))))
|
|
15
|
+
throw new CtlError('CTL_URL_INVALID', 'Use HTTPS, or explicit --ssh-tunnel for a loopback HTTP tunnel.', 'run: synopkgland ctl host add <name> --url https://<nas>:5001');
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
export function createRequest(origin, timeoutMs = 10000, ca) {
|
|
19
|
+
return async (path, body, session) => {
|
|
20
|
+
if (!path.startsWith('/') || path.startsWith('//') || path.includes('?') || path.includes('#') || path.includes('\\') || path.includes('%') || path.split('/').some(p => p === '.' || p === '..'))
|
|
21
|
+
throw new CtlError('CTL_PATH_INVALID', 'Unsafe request path.', 'run: synopkgland ctl list --refresh');
|
|
22
|
+
const url = new URL(path, origin);
|
|
23
|
+
if (url.origin !== origin || (session && session.origin !== origin))
|
|
24
|
+
throw new CtlError('CTL_ORIGIN_MISMATCH', 'Credentials cannot cross NAS origins.', 'run: synopkgland ctl host list', 4);
|
|
25
|
+
const form = body instanceof URLSearchParams;
|
|
26
|
+
const payload = Buffer.from(form ? body.toString() : JSON.stringify(body));
|
|
27
|
+
if (payload.length > 1048576)
|
|
28
|
+
throw new CtlError('CTL_INPUT_TOO_LARGE', 'Request exceeds 1 MiB.', 'Reduce the input.');
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const headers = { 'content-type': form ? 'application/x-www-form-urlencoded' : 'application/json', accept: 'application/json', 'content-length': String(payload.length), origin };
|
|
31
|
+
if (session) {
|
|
32
|
+
headers.cookie = session.cookie;
|
|
33
|
+
headers['X-SYNO-TOKEN'] = session.token;
|
|
34
|
+
}
|
|
35
|
+
const req = (url.protocol === 'https:' ? https : http).request(url, { method: 'POST', headers, timeout: timeoutMs, ...(ca ? { ca } : {}) }, res => {
|
|
36
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
37
|
+
res.resume();
|
|
38
|
+
reject(new CtlError('CTL_HTTP_ERROR', `HTTP ${res.statusCode}; redirects are never followed.`, 'Check the NAS URL and connection.', 6));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!res.headers['content-type']?.includes('application/json')) {
|
|
42
|
+
res.resume();
|
|
43
|
+
reject(new CtlError('CTL_RESPONSE_INVALID', 'NAS returned non-JSON data.', 'Check the endpoint and installed package.', 5));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const chunks = [];
|
|
47
|
+
let length = 0;
|
|
48
|
+
res.on('data', (c) => { length += c.length; if (length > 1048576) {
|
|
49
|
+
res.destroy();
|
|
50
|
+
reject(new CtlError('CTL_RESPONSE_TOO_LARGE', 'Response exceeds 1 MiB.', 'Use a smaller operation.', 5));
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
chunks.push(c); });
|
|
54
|
+
res.on('error', () => reject(new CtlError('CTL_NETWORK', 'Response interrupted.', 'Check the NAS connection; state-changing requests are not replayed.', 6)));
|
|
55
|
+
res.on('end', () => {
|
|
56
|
+
try {
|
|
57
|
+
const body = strictJson(Buffer.concat(chunks));
|
|
58
|
+
if (!form && body?.success === true && body.data && path.startsWith('/webman/3rdparty/') && url.protocol === 'https:') {
|
|
59
|
+
const cookies = (res.headers['set-cookie'] ?? []).filter(line => /;\s*Secure(?:;|$)/i.test(line) && /;\s*HttpOnly(?:;|$)/i.test(line) && !/;\s*Domain=/i.test(line));
|
|
60
|
+
const own = path.slice(0, path.lastIndexOf('/cgi/')) + '/';
|
|
61
|
+
const valid = cookies.filter(line => { const match = /;\s*Path=([^;]+)/i.exec(line); return match?.[1] === own; }).map(line => line.split(';')[0]).filter(pair => !pair.startsWith('id='));
|
|
62
|
+
if (valid.length)
|
|
63
|
+
Object.defineProperty(body, APP_COOKIE, { value: valid.join('; '), enumerable: false });
|
|
64
|
+
}
|
|
65
|
+
resolve(body);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
reject(new CtlError('CTL_RESPONSE_INVALID', 'Invalid JSON response.', 'Check the installed package.', 5));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const wallTimer = setTimeout(() => req.destroy(), timeoutMs);
|
|
73
|
+
req.once('close', () => clearTimeout(wallTimer));
|
|
74
|
+
req.on('timeout', () => req.destroy());
|
|
75
|
+
req.on('error', () => reject(new CtlError('CTL_NETWORK', 'NAS request failed or timed out.', 'Check the NAS connection and trusted CA; requests are not replayed.', 6)));
|
|
76
|
+
req.end(payload);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/deploy/deploy.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CommandRunner } from "../standalone/build.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
3
3
|
import type { ProjectRoot } from "../discovery.js";
|
|
4
4
|
import type { NasHost } from "../host/types.js";
|
|
5
|
-
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
5
|
+
import type { resolveDeploySpkVersion } from "./spk-version.js";
|
|
6
6
|
export interface DeployProjectOptions {
|
|
7
7
|
project: ProjectRoot;
|
|
8
8
|
/** Gateway architecture. Omitted means "ask the NAS" when needed. */
|
|
@@ -27,4 +27,5 @@ export interface DeployProjectResult {
|
|
|
27
27
|
/** Non-blocking pre-flight remarks, e.g. a read-only Shared Folder grant. */
|
|
28
28
|
notes?: string[];
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
/** Removed legacy dispatch; schema-2 deployment uses planDeployment/deployStandalone. */
|
|
31
|
+
export declare function deployProject(_options: DeployProjectOptions): Promise<DeployProjectResult>;
|
package/dist/deploy/deploy.js
CHANGED
|
@@ -1,213 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { loadContract } from "../contract/load.js";
|
|
4
|
-
import { usesAuthGateway } from "../contract/web-launch.js";
|
|
5
|
-
import { findUnsupportedInstallTypes } from "../contract/unsupported-types.js";
|
|
6
|
-
import { appOriginFromContract } from "../host/open-url.js";
|
|
7
|
-
import { validateProject } from "../validate.js";
|
|
8
|
-
import { resolveDeployIdentity, PACKAGE_IDENTITY_MAX_LENGTH } from "./identity.js";
|
|
9
|
-
import { resolveDeploySpkVersion } from "./spk-version.js";
|
|
10
|
-
import { resolveSharedFolderExtraValues, sharedFolderParameterNames } from "./share-values.js";
|
|
11
|
-
import { resolveDsmUserExtraValues, dsmUserParameterNames } from "./run-as-values.js";
|
|
12
|
-
import { runSharePermissionPreflight } from "./share-permission-preflight.js";
|
|
13
|
-
import { homesParameterNames, resolveHomesExtraValues } from "./homes-values.js";
|
|
14
|
-
import { resolveInstallParameterExtraValues } from "./param-values.js";
|
|
15
|
-
import { loadCompose } from "../compose/analyze.js";
|
|
16
|
-
import { runPortPreflight } from "./port-preflight.js";
|
|
17
|
-
const POSIX_ALL_SHARES_LOOKUP_FAILED = "DEPLOY_POSIX_ALL_SHARES_LOOKUP_FAILED";
|
|
18
|
-
export async function deployProject(options) {
|
|
19
|
-
const contract = loadContract(options.project.contractPath).contract;
|
|
20
|
-
// A45/A46: refuse before anything is built or uploaded. validate only warns,
|
|
21
|
-
// so this is the gate that keeps an uninstallable App off a NAS.
|
|
22
|
-
const unsupported = findUnsupportedInstallTypes(contract);
|
|
23
|
-
if (unsupported.length > 0) {
|
|
24
|
-
const first = unsupported[0];
|
|
25
|
-
return {
|
|
26
|
-
ok: false,
|
|
27
|
-
code: first.code,
|
|
28
|
-
message: `The Install Parameter "${first.name}" with type: ${first.type} has no usable install path today, ` +
|
|
29
|
-
"a Package Center install ends in DSM broken/150, so deploy refuses to deploy." +
|
|
30
|
-
(first.type === "homes"
|
|
31
|
-
? "If you need an administrator-chosen folder, use type: shared_folder."
|
|
32
|
-
: "Use type: string, or have the App read the file from ${SYNO_APP_SECRETS_DIR}."),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const validation = validateProject(options.project);
|
|
36
|
-
if (!validation.ok) {
|
|
37
|
-
return { ok: false, code: "VALIDATION_FAILED", message: validation.errors.map((e) => e.code).join(", ") };
|
|
38
|
-
}
|
|
39
|
-
const identity = resolveDeployIdentity(options.project, { dev: options.dev });
|
|
40
|
-
if (identity.packageId.length > PACKAGE_IDENTITY_MAX_LENGTH) {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
code: "PACKAGE_IDENTITY_TOO_LONG",
|
|
44
|
-
message: `The final Package Identity "${identity.packageId}" length ${identity.packageId.length} exceeds the DSM limit of ${PACKAGE_IDENTITY_MAX_LENGTH} characters.`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const packageInstalled = await options.host.isPackageInstalled(identity.packageId);
|
|
48
|
-
const explicitParamNames = new Set(Object.keys(options.paramValues ?? {}));
|
|
49
|
-
const paramValues = { ...(options.paramValues ?? {}) };
|
|
50
|
-
const posixAllSharesNames = Object.entries(contract.install ?? {})
|
|
51
|
-
.filter(([, parameter]) => parameter?.type === "posix_all_shares")
|
|
52
|
-
.map(([name]) => name);
|
|
53
|
-
for (const name of posixAllSharesNames) {
|
|
54
|
-
if (Object.prototype.hasOwnProperty.call(paramValues, name)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
let shareNames;
|
|
58
|
-
try {
|
|
59
|
-
shareNames = await options.host.listShareNames?.();
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
shareNames = undefined;
|
|
63
|
-
}
|
|
64
|
-
const names = (shareNames ?? []).map((value) => value.trim()).filter(Boolean);
|
|
65
|
-
if (names.length === 0) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
code: POSIX_ALL_SHARES_LOOKUP_FAILED,
|
|
69
|
-
message: `The Shared Folders of this NAS could not be listed with an administrator session, so the POSIX all-shares parameter "${name}" cannot be filled.`,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
paramValues[name] = names.join(",");
|
|
73
|
-
}
|
|
74
|
-
const paramResolved = resolveInstallParameterExtraValues({
|
|
75
|
-
contract,
|
|
76
|
-
provided: paramValues,
|
|
77
|
-
});
|
|
78
|
-
if (!paramResolved.ok) {
|
|
79
|
-
return { ok: false, code: paramResolved.code, message: paramResolved.message };
|
|
80
|
-
}
|
|
81
|
-
const homesResolved = await resolveHomesExtraValues({
|
|
82
|
-
requiredKeys: homesParameterNames(contract),
|
|
83
|
-
lookup: async () => options.host.resolveHomesPath?.(),
|
|
84
|
-
});
|
|
85
|
-
if (!homesResolved.ok) {
|
|
86
|
-
return { ok: false, code: homesResolved.code, message: homesResolved.message };
|
|
87
|
-
}
|
|
88
|
-
const shareResolved = await resolveSharedFolderExtraValues({
|
|
89
|
-
requiredKeys: sharedFolderParameterNames(contract).filter((key) => !packageInstalled || options.shareValues?.[key] !== undefined),
|
|
90
|
-
provided: options.shareValues ?? {},
|
|
91
|
-
lookup: async (logicalName) => options.host.resolveSharePath?.(logicalName),
|
|
92
|
-
});
|
|
93
|
-
if (!shareResolved.ok) {
|
|
94
|
-
return { ok: false, code: shareResolved.code, message: shareResolved.message };
|
|
95
|
-
}
|
|
96
|
-
const runAsResolved = resolveDsmUserExtraValues({
|
|
97
|
-
requiredKeys: dsmUserParameterNames(contract).filter((key) => !packageInstalled || options.runAsValues?.[key] !== undefined),
|
|
98
|
-
provided: options.runAsValues ?? {},
|
|
99
|
-
});
|
|
100
|
-
if (!runAsResolved.ok) {
|
|
101
|
-
return { ok: false, code: runAsResolved.code, message: runAsResolved.message };
|
|
102
|
-
}
|
|
103
|
-
// A42 pre-flight: preinst runs as the package user and can never read ACL, so
|
|
104
|
-
// the only place that can catch "selected user cannot reach selected Share" is
|
|
105
|
-
// here, where we still hold an administrator session. Refuses before install.
|
|
106
|
-
const sharePermission = await runSharePermissionPreflight({
|
|
107
|
-
shares: Object.fromEntries(sharedFolderParameterNames(contract)
|
|
108
|
-
.map((key) => [key, (options.shareValues ?? {})[key] ?? ""])
|
|
109
|
-
.filter(([, value]) => value.trim() !== "")),
|
|
110
|
-
runAsUsers: runAsResolved.extraValues,
|
|
111
|
-
listSharePermissions: options.host.listSharePermissions
|
|
112
|
-
? (name) => options.host.listSharePermissions(name)
|
|
113
|
-
: undefined,
|
|
114
|
-
listUserGroups: options.host.listUserGroups
|
|
115
|
-
? (name) => options.host.listUserGroups(name)
|
|
116
|
-
: undefined,
|
|
117
|
-
});
|
|
118
|
-
if (!sharePermission.ok) {
|
|
119
|
-
return { ok: false, code: sharePermission.code, message: sharePermission.message };
|
|
120
|
-
}
|
|
121
|
-
const portPreflight = await runPortPreflight({
|
|
122
|
-
compose: loadCompose(options.project.composePath)?.compose,
|
|
123
|
-
packageIdentity: identity.packageId,
|
|
124
|
-
listPortInfo: options.host.listPortInfo
|
|
125
|
-
? () => options.host.listPortInfo()
|
|
126
|
-
: undefined,
|
|
127
|
-
});
|
|
128
|
-
if (!portPreflight.ok) {
|
|
129
|
-
return { ok: false, code: portPreflight.code, message: portPreflight.message };
|
|
130
|
-
}
|
|
131
|
-
const extraValues = {
|
|
132
|
-
...paramResolved.extraValues,
|
|
133
|
-
...homesResolved.extraValues,
|
|
134
|
-
...shareResolved.extraValues,
|
|
135
|
-
...runAsResolved.extraValues,
|
|
136
|
-
};
|
|
137
|
-
// The upgrade wizard deliberately ignores auto-filled Share and Run-as controls unless
|
|
138
|
-
// the administrator opted into a change. Keep the CLI's explicit upgrade
|
|
139
|
-
// flags equivalent to checking those controls.
|
|
140
|
-
if (packageInstalled) {
|
|
141
|
-
for (const name of sharedFolderParameterNames(contract)) {
|
|
142
|
-
if ((options.shareValues?.[name] ?? "").trim() !== "") {
|
|
143
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
for (const name of dsmUserParameterNames(contract)) {
|
|
147
|
-
if ((options.runAsValues?.[name] ?? "").trim() !== "") {
|
|
148
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const name of posixAllSharesNames) {
|
|
152
|
-
if (explicitParamNames.has(name) && (paramValues[name] ?? "").trim() !== "") {
|
|
153
|
-
extraValues[`CHANGE_${name}`] = "true";
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const resolveSpkVersion = options.spkVersionResolver ?? resolveDeploySpkVersion;
|
|
158
|
-
let spkVersion;
|
|
159
|
-
try {
|
|
160
|
-
spkVersion = await resolveSpkVersion(options.project, options.host, identity.packageId);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
return { ok: false, code: "DEPLOY_VERSION_INVALID",
|
|
164
|
-
message: error instanceof Error ? error.message : String(error) };
|
|
165
|
-
}
|
|
166
|
-
let archFamily = options.archFamily;
|
|
167
|
-
// A DSM-authenticated web App ships a native gateway CGI, so it is built per architecture too.
|
|
168
|
-
if (archFamily === undefined && usesAuthGateway(contract)) {
|
|
169
|
-
archFamily = await options.host.getArchFamily?.();
|
|
170
|
-
if (archFamily === undefined) {
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
code: "NATIVE_ARCH_UNDETERMINED",
|
|
174
|
-
message: "The CPU architecture could not be determined from this NAS. Specify --arch x86_64 or --arch aarch64 explicitly.",
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (archFamily !== undefined && !isContainerArchFamily(archFamily)) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
code: "CONTAINER_ARCH_UNSUPPORTED",
|
|
182
|
-
message: `Container Manager Apps support only ${CONTAINER_ARCH_FAMILIES.join(", ")}; DSM does not provide Container Manager for ${archFamily}.`,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
const build = await buildSpk({
|
|
186
|
-
project: options.project,
|
|
187
|
-
packageIdentity: identity.packageId,
|
|
188
|
-
displayname: identity.displayname,
|
|
189
|
-
spkVersion,
|
|
190
|
-
archFamily,
|
|
191
|
-
gatewayRun: options.gatewayRun,
|
|
192
|
-
});
|
|
193
|
-
if (!build.ok || !build.outputPath) {
|
|
194
|
-
return { ok: false, code: "BUILD_SPK_FAILED", message: build.errors.join(", ") };
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
await options.host.installFromLocalSpk({
|
|
198
|
-
packageId: identity.packageId,
|
|
199
|
-
version: spkVersion,
|
|
200
|
-
spkPath: build.outputPath,
|
|
201
|
-
extraValues,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
return { ok: false, code: "LOCAL_SPK_INSTALL_FAILED",
|
|
206
|
-
message: error instanceof Error ? error.message : String(error) };
|
|
207
|
-
}
|
|
1
|
+
/** Removed legacy dispatch; schema-2 deployment uses planDeployment/deployStandalone. */
|
|
2
|
+
export async function deployProject(_options) {
|
|
208
3
|
return {
|
|
209
|
-
ok:
|
|
210
|
-
|
|
211
|
-
|
|
4
|
+
ok: false,
|
|
5
|
+
code: "COMPOSE_LEGACY_MANIFEST_REMOVED",
|
|
6
|
+
message: "Use schema-2 dsm-package.yaml and the unified deployment plan; the legacy Compose installer is removed.",
|
|
212
7
|
};
|
|
213
8
|
}
|
package/dist/deploy/plan.js
CHANGED
|
@@ -3,7 +3,7 @@ import { loadContract } from "../contract/load.js";
|
|
|
3
3
|
import { validateProject } from "../validate.js";
|
|
4
4
|
import { findStandaloneRoot, loadStandaloneManifest, } from "../standalone/manifest.js";
|
|
5
5
|
import { buildStandalone } from "../standalone/build.js";
|
|
6
|
-
import { buildNativeLayout, isNativeLayout, loadNativeLayout } from "../standalone/native-layout.js";
|
|
6
|
+
import { buildNativeLayout, isNativeLayout, loadNativeLayout, } from "../standalone/native-layout.js";
|
|
7
7
|
import { resolveDeployIdentity } from "./identity.js";
|
|
8
8
|
import { nextDeployVersion } from "./spk-version.js";
|
|
9
9
|
import { resolveNativeInstallValues } from "./native-install-values.js";
|
|
@@ -29,17 +29,28 @@ export function planDeployment(root, options = {}) {
|
|
|
29
29
|
throw fail("STANDALONE_ARCH_NOT_DECLARED", `Architecture not declared: ${options.arch}; add it to dsm-package.yaml arches to deploy it`);
|
|
30
30
|
return {
|
|
31
31
|
root: standalone,
|
|
32
|
-
kind: m.
|
|
32
|
+
kind: m.compose
|
|
33
|
+
? "container"
|
|
34
|
+
: m.backend.kind === "binary"
|
|
35
|
+
? "binary"
|
|
36
|
+
: `native-${m.backend.kind}`,
|
|
33
37
|
packageId: m.package,
|
|
34
38
|
version: m.spkVersion,
|
|
35
39
|
arch: options.arch,
|
|
36
40
|
capabilities: Object.keys(m.capabilities),
|
|
37
41
|
transport: "dsm-local-spk",
|
|
38
|
-
notes: m.
|
|
42
|
+
notes: m.compose
|
|
39
43
|
? [
|
|
40
|
-
"
|
|
44
|
+
"Containers use the fixed package identity; they do not inherit signed-in File Station ACLs.",
|
|
45
|
+
...m.compose.plan.acceptance_blockers,
|
|
41
46
|
]
|
|
42
|
-
:
|
|
47
|
+
: m.capabilities.rootOps ||
|
|
48
|
+
m.capabilities.delegate ||
|
|
49
|
+
m.capabilities.webapi
|
|
50
|
+
? [
|
|
51
|
+
"Installation does not mean ready. Approve boot and control tasks once in the App window. An executor-only upgrade stays ready without approval (INFO anchors the executor digest); a policy digest change requires approval again.",
|
|
52
|
+
]
|
|
53
|
+
: [],
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
const project = findProjectRoot(root);
|
|
@@ -100,7 +111,11 @@ async function buildForDeploy(plan, arch, installed, options) {
|
|
|
100
111
|
const name = Object.keys(options.params ?? {})[0];
|
|
101
112
|
if (name)
|
|
102
113
|
throw fail("DEPLOY_PARAM_UNDECLARED", `${m.package} has no install wizard values; remove --param ${name}.`);
|
|
103
|
-
const { outputs } = await buildNativeLayout({
|
|
114
|
+
const { outputs } = await buildNativeLayout({
|
|
115
|
+
root: plan.root,
|
|
116
|
+
arches: [layoutArch],
|
|
117
|
+
run: options.run,
|
|
118
|
+
});
|
|
104
119
|
return { outputs, extraValues: {} };
|
|
105
120
|
}
|
|
106
121
|
const loaded = loadStandaloneManifest(plan.root);
|
|
@@ -109,7 +124,11 @@ async function buildForDeploy(plan, arch, installed, options) {
|
|
|
109
124
|
const values = resolveNativeInstallValues(loaded.manifest, options.params ?? {}, { installed });
|
|
110
125
|
if (!values.ok)
|
|
111
126
|
throw fail(values.code, values.message);
|
|
112
|
-
const { outputs } = await buildStandalone({
|
|
127
|
+
const { outputs } = await buildStandalone({
|
|
128
|
+
root: plan.root,
|
|
129
|
+
arches: [arch],
|
|
130
|
+
run: options.run,
|
|
131
|
+
});
|
|
113
132
|
return { outputs, extraValues: values.extraValues };
|
|
114
133
|
}
|
|
115
134
|
export async function deployStandalone(plan, host, options = {}) {
|
package/dist/dev.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { createDockerComposeRunner } from "./compose/runner.js";
|
|
3
2
|
/** Run the application's own Compose project locally. */
|
|
4
3
|
export async function runDev(options) {
|
|
5
|
-
if (!existsSync(options.project.composePath))
|
|
6
|
-
throw new Error('dev requires compose.yaml');
|
|
7
4
|
const runner = options.composeRunner ?? createDockerComposeRunner();
|
|
8
5
|
try {
|
|
9
6
|
await runner.up(options.project.root);
|
|
@@ -13,5 +10,9 @@ export async function runDev(options) {
|
|
|
13
10
|
await runner.down?.(options.project.root);
|
|
14
11
|
throw error;
|
|
15
12
|
}
|
|
16
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
close: async () => {
|
|
15
|
+
await runner.down?.(options.project.root);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
17
18
|
}
|