@synopackageland/cli 0.14.1 → 0.15.1

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.
Files changed (121) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +33 -18
  3. package/dist/build-spk/generator.d.ts +3 -2
  4. package/dist/build-spk/generator.js +6 -846
  5. package/dist/build-spk/resource-worker.d.ts +2 -14
  6. package/dist/build-spk/resource-worker.js +3 -166
  7. package/dist/cli.js +208 -56
  8. package/dist/compose/analyze.d.ts +3 -61
  9. package/dist/compose/analyze.js +35 -776
  10. package/dist/compose/env.d.ts +5 -3
  11. package/dist/compose/env.js +17 -25
  12. package/dist/compose/runner.js +23 -6
  13. package/dist/contract/unsupported-types.js +1 -1
  14. package/dist/contract/validate.js +2 -2
  15. package/dist/contract/web-launch.d.ts +1 -12
  16. package/dist/contract/web-launch.js +10 -74
  17. package/dist/ctl/cli.d.ts +36 -0
  18. package/dist/ctl/cli.js +218 -0
  19. package/dist/ctl/descriptor.d.ts +81 -0
  20. package/dist/ctl/descriptor.js +93 -0
  21. package/dist/ctl/dsm-auth.d.ts +10 -0
  22. package/dist/ctl/dsm-auth.js +30 -0
  23. package/dist/ctl/errors.d.ts +17 -0
  24. package/dist/ctl/errors.js +32 -0
  25. package/dist/ctl/flows.d.ts +11 -0
  26. package/dist/ctl/flows.js +52 -0
  27. package/dist/ctl/input.d.ts +20 -0
  28. package/dist/ctl/input.js +128 -0
  29. package/dist/ctl/json.d.ts +2 -0
  30. package/dist/ctl/json.js +38 -0
  31. package/dist/ctl/redaction.d.ts +15 -0
  32. package/dist/ctl/redaction.js +198 -0
  33. package/dist/ctl/service.d.ts +38 -0
  34. package/dist/ctl/service.js +156 -0
  35. package/dist/ctl/store.d.ts +51 -0
  36. package/dist/ctl/store.js +93 -0
  37. package/dist/ctl/transfers.d.ts +14 -0
  38. package/dist/ctl/transfers.js +74 -0
  39. package/dist/ctl/transport.d.ts +11 -0
  40. package/dist/ctl/transport.js +79 -0
  41. package/dist/deploy/deploy.d.ts +4 -3
  42. package/dist/deploy/deploy.js +5 -210
  43. package/dist/deploy/plan.js +26 -7
  44. package/dist/dev.js +5 -4
  45. package/dist/e2e/dsm.d.ts +2 -2
  46. package/dist/e2e/dsm.js +10 -6
  47. package/dist/error-registry.js +1020 -834
  48. package/dist/host/share-permission-lookup.d.ts +1 -1
  49. package/dist/host/share-permission.d.ts +1 -1
  50. package/dist/host/share-permission.js +1 -1
  51. package/dist/info.js +120 -28
  52. package/dist/init.js +188 -64
  53. package/dist/inspect.js +21 -6
  54. package/dist/runtime/executor-digests.json +5 -5
  55. package/dist/skill.js +51 -9
  56. package/dist/standalone/api-description-inspect.d.ts +4 -0
  57. package/dist/standalone/api-description-inspect.js +12 -0
  58. package/dist/standalone/api-description.d.ts +7 -0
  59. package/dist/standalone/api-description.js +73 -0
  60. package/dist/standalone/build.d.ts +3 -0
  61. package/dist/standalone/build.js +140 -33
  62. package/dist/standalone/compose-errors.d.ts +3 -0
  63. package/dist/standalone/compose-errors.js +243 -0
  64. package/dist/standalone/compose.d.ts +97 -10
  65. package/dist/standalone/compose.js +306 -75
  66. package/dist/standalone/daemon.js +111 -29
  67. package/dist/standalone/dsm-window.js +39 -8
  68. package/dist/standalone/identity-gateway.d.ts +9 -2
  69. package/dist/standalone/identity-gateway.js +282 -66
  70. package/dist/standalone/inspect-spk.d.ts +12 -0
  71. package/dist/standalone/inspect-spk.js +666 -136
  72. package/dist/standalone/lifecycle.js +124 -42
  73. package/dist/standalone/manifest.d.ts +8 -1
  74. package/dist/standalone/manifest.js +303 -86
  75. package/dist/standalone/resource.js +6 -2
  76. package/dist/standalone/spk.d.ts +4 -1
  77. package/dist/standalone/spk.js +138 -39
  78. package/dist/template-paths.d.ts +0 -2
  79. package/dist/template-paths.js +0 -5
  80. package/dist/test-command.d.ts +1 -1
  81. package/dist/test-command.js +1 -1
  82. package/dist/validate.d.ts +3 -3
  83. package/dist/validate.js +32 -140
  84. package/package.json +5 -3
  85. package/schema/synopkgland-api-v1.schema.json +423 -0
  86. package/skills/SKILL.md +4 -2
  87. package/skills/reference/compose.md +349 -0
  88. package/skills/reference/create.md +9 -18
  89. package/skills/reference/ctl.md +41 -0
  90. package/skills/reference/deploy.md +1 -1
  91. package/skills/reference/files.md +5 -1
  92. package/skills/reference/identity-gateway.md +8 -144
  93. package/skills/reference/wrap.md +10 -39
  94. package/assets/go/daemon/fixture_linux_test.go +0 -15
  95. package/assets/go/daemon/fixture_other_test.go +0 -7
  96. package/assets/go/daemon/go.mod +0 -5
  97. package/assets/go/daemon/main.go +0 -636
  98. package/assets/go/daemon/main_test.go +0 -315
  99. package/assets/go/daemon/network.go +0 -107
  100. package/assets/go/daemon/network_test.go +0 -86
  101. package/assets/go/daemon/process_linux.go +0 -55
  102. package/assets/go/daemon/process_linux_test.go +0 -161
  103. package/assets/go/daemon/process_other.go +0 -14
  104. package/assets/go/daemon/testdata/child.go +0 -44
  105. package/assets/go/gateway/auth.go +0 -101
  106. package/assets/go/gateway/auth_test.go +0 -216
  107. package/assets/go/gateway/cgi.go +0 -133
  108. package/assets/go/gateway/cgi_test.go +0 -156
  109. package/assets/go/gateway/csrf.go +0 -73
  110. package/assets/go/gateway/csrf_test.go +0 -155
  111. package/assets/go/gateway/directory.go +0 -108
  112. package/assets/go/gateway/directory_test.go +0 -202
  113. package/assets/go/gateway/gateway.go +0 -412
  114. package/assets/go/gateway/gateway_test.go +0 -676
  115. package/assets/go/gateway/go.mod +0 -5
  116. package/assets/go/gateway/groups.go +0 -66
  117. package/assets/go/gateway/main.go +0 -45
  118. package/dist/build-spk/gateway.d.ts +0 -31
  119. package/dist/build-spk/gateway.js +0 -60
  120. package/dist/compose/privilege.d.ts +0 -14
  121. 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
+ }
@@ -1,8 +1,8 @@
1
1
  import type { CommandRunner } from "../standalone/build.js";
2
- import { type NativeArchFamily } from "../build-spk/dsm-arch.js";
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
- export declare function deployProject(options: DeployProjectOptions): Promise<DeployProjectResult>;
30
+ /** Removed legacy dispatch; schema-2 deployment uses planDeployment/deployStandalone. */
31
+ export declare function deployProject(_options: DeployProjectOptions): Promise<DeployProjectResult>;
@@ -1,213 +1,8 @@
1
- import { buildSpk } from "../build-spk/generator.js";
2
- import { CONTAINER_ARCH_FAMILIES, isContainerArchFamily } from "../build-spk/dsm-arch.js";
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: true, packageId: identity.packageId, spkPath: build.outputPath,
210
- openUrl: appOriginFromContract(options.host.dsmOrigin, contract),
211
- version: spkVersion, notes: sharePermission.notes,
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
  }
@@ -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.backend.kind === "binary" ? "binary" : `native-${m.backend.kind}`,
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.capabilities.rootOps || m.capabilities.delegate || m.capabilities.webapi
42
+ notes: m.compose
39
43
  ? [
40
- "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.",
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({ root: plan.root, arches: [layoutArch], run: options.run });
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({ root: plan.root, arches: [arch], run: options.run });
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 { close: async () => { await runner.down?.(options.project.root); } };
13
+ return {
14
+ close: async () => {
15
+ await runner.down?.(options.project.root);
16
+ },
17
+ };
17
18
  }
package/dist/e2e/dsm.d.ts CHANGED
@@ -17,7 +17,7 @@ type Page = ReturnType<NonNullable<ReturnType<typeof surfaceOf>>["page"]>;
17
17
  type Locator = ReturnType<Page["locator"]>;
18
18
  /** The DSM URL the browser opens: DSM_E2E_URL, else the local end of the SSH tunnel. */
19
19
  export declare function dsmUrl(env?: Env): string;
20
- /** ssh arguments forwarding the loopback ports of `forwards` to the NAS. */
20
+ /** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
21
21
  export declare function tunnelArgs(env?: Env): string[];
22
22
  /** A loopback port nothing listens on now, for a tunnel that names none. */
23
23
  export declare function freePort(): Promise<number>;
@@ -122,7 +122,7 @@ export declare function waitForDesktop(page?: Page): Promise<void>;
122
122
  /**
123
123
  * Closes every DSM notification card; returns how many were closed. DSM shows
124
124
  * them a moment after the desktop (the admin's "Admin account security" card
125
- * about 2 s later on vt-nas), so it first waits up to
125
+ * about 2 s later on a DSM 7.4 test NAS), so it first waits up to
126
126
  * DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
127
127
  */
128
128
  export declare function dismissNotifications(page?: Page, env?: Env): Promise<number>;
package/dist/e2e/dsm.js CHANGED
@@ -41,11 +41,14 @@ function forwards(env) {
41
41
  ...extra(env.DSM_E2E_TUNNEL_HTTPS_PORT, env.DSM_E2E_REMOTE_HTTPS_PORT, "443"),
42
42
  ];
43
43
  }
44
- /** ssh arguments forwarding the loopback ports of `forwards` to the NAS. */
44
+ /** ssh arguments forwarding loopback ports to the NAS; DSM_E2E_SSH is required. */
45
45
  export function tunnelArgs(env = process.env) {
46
+ const target = env.DSM_E2E_SSH?.trim();
47
+ if (!target)
48
+ throw new Error("DSM_E2E_SSH is required to open a NAS tunnel; set it to user@nas.example (using your NAS host).");
46
49
  return ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes",
47
50
  ...forwards(env).flatMap(([local, remote]) => ["-L", `127.0.0.1:${local}:127.0.0.1:${remote}`]),
48
- env.DSM_E2E_SSH || "root@vt-nas.syno"];
51
+ target];
49
52
  }
50
53
  /** A loopback port nothing listens on now, for a tunnel that names none. */
51
54
  export function freePort() {
@@ -80,12 +83,13 @@ export async function runWithTunnel(command, outer = process.env) {
80
83
  throw new Error("runWithTunnel needs a command to run.");
81
84
  const port = Number(outer.DSM_E2E_TUNNEL_PORT || await freePort());
82
85
  const env = { ...outer, DSM_E2E_TUNNEL_PORT: String(port) };
86
+ const args = tunnelArgs(env);
83
87
  const ports = forwards(env).map(([local]) => Number(local));
84
88
  for (const used of ports) {
85
89
  if (await portOpen(used))
86
90
  throw new Error(`Port ${used} is already in use; name another DSM_E2E_TUNNEL_* port or close it before opening the tunnel.`);
87
91
  }
88
- const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", tunnelArgs(env), { stdio: ["ignore", "ignore", "inherit"] });
92
+ const ssh = spawn(env.DSM_E2E_SSH_BIN || "ssh", args, { stdio: ["ignore", "ignore", "inherit"] });
89
93
  let sshExited = false;
90
94
  ssh.once("exit", () => { sshExited = true; });
91
95
  try {
@@ -93,7 +97,7 @@ export async function runWithTunnel(command, outer = process.env) {
93
97
  for (const waiting of ports) {
94
98
  while (!(await portOpen(waiting))) {
95
99
  if (sshExited || Date.now() > deadline)
96
- throw new Error(`The SSH tunnel to ${tunnelArgs(env).at(-1)} did not open on port ${waiting}.`);
100
+ throw new Error(`The SSH tunnel to ${args.at(-1)} did not open on port ${waiting}.`);
97
101
  await new Promise(accept => setTimeout(accept, 100));
98
102
  }
99
103
  }
@@ -364,7 +368,7 @@ const notificationCloseSelector = ".v-notification-container .v-notification-pan
364
368
  /**
365
369
  * Closes every DSM notification card; returns how many were closed. DSM shows
366
370
  * them a moment after the desktop (the admin's "Admin account security" card
367
- * about 2 s later on vt-nas), so it first waits up to
371
+ * about 2 s later on a DSM 7.4 test NAS), so it first waits up to
368
372
  * DSM_E2E_NOTIFICATION_WAIT_MS (default 5000) for one.
369
373
  */
370
374
  export async function dismissNotifications(page = dsmPage(), env = process.env) {
@@ -454,7 +458,7 @@ async function restoredSessionLive(page) {
454
458
  */
455
459
  export async function signIn(app, user = "admin", env = process.env) {
456
460
  const url = dsmUrl(env);
457
- const file = profilePath(env.DSM_E2E_TARGET || "vt-nas", user, env);
461
+ const file = profilePath(env.DSM_E2E_TARGET || "dsm", user, env);
458
462
  const context = surface().context();
459
463
  const profile = loadProfile(file, url);
460
464
  if (profile)