@synopackageland/cli 0.17.4 → 0.17.6

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 (49) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +27 -1
  3. package/dist/build-spk/compose-retained-data.d.ts +11 -0
  4. package/dist/build-spk/compose-retained-data.js +34 -0
  5. package/dist/build-spk/delete-app-data.d.ts +1 -1
  6. package/dist/build-spk/delete-app-data.js +1 -1
  7. package/dist/build-spk/uninstall-wizard.d.ts +2 -0
  8. package/dist/build-spk/uninstall-wizard.js +2 -1
  9. package/dist/build-spk/wizard-messages.d.ts +22 -0
  10. package/dist/build-spk/wizard-messages.js +22 -0
  11. package/dist/ctl/cli.js +10 -6
  12. package/dist/ctl/flows.js +1 -1
  13. package/dist/ctl/service.d.ts +4 -1
  14. package/dist/ctl/service.js +20 -6
  15. package/dist/ctl/transfers.d.ts +1 -1
  16. package/dist/ctl/transfers.js +5 -2
  17. package/dist/ctl/transport.d.ts +2 -1
  18. package/dist/ctl/transport.js +7 -3
  19. package/dist/runtime/compose-data.d.ts +10 -0
  20. package/dist/runtime/compose-data.js +20 -0
  21. package/dist/runtime/executor-digests.json +11 -0
  22. package/dist/runtime/executor-policy.d.ts +25 -0
  23. package/dist/runtime/executor-policy.js +10 -1
  24. package/dist/runtime/runtime-budgets.d.ts +24 -0
  25. package/dist/runtime/runtime-budgets.js +13 -0
  26. package/dist/standalone/background.d.ts +3 -3
  27. package/dist/standalone/background.js +12 -11
  28. package/dist/standalone/compose.d.ts +3 -1
  29. package/dist/standalone/compose.js +8 -1
  30. package/dist/standalone/daemon.d.ts +4 -0
  31. package/dist/standalone/daemon.js +36 -23
  32. package/dist/standalone/dsm-window.js +2 -1
  33. package/dist/standalone/identity-gateway.js +9 -1
  34. package/dist/standalone/inspect-spk.js +122 -35
  35. package/dist/standalone/lifecycle.js +16 -9
  36. package/dist/standalone/manifest.d.ts +1 -0
  37. package/dist/standalone/manifest.js +8 -5
  38. package/dist/standalone/native-gateway.d.ts +17 -0
  39. package/dist/standalone/native-gateway.js +115 -0
  40. package/dist/standalone/resource.d.ts +3 -0
  41. package/dist/standalone/resource.js +3 -1
  42. package/dist/standalone/spk.js +14 -3
  43. package/dist/standalone/uninstall-hook.js +21 -8
  44. package/dist/standalone/uninstall.d.ts +1 -1
  45. package/dist/standalone/uninstall.js +2 -2
  46. package/package.json +2 -2
  47. package/skills/reference/compose.md +31 -0
  48. package/skills/reference/create.md +1 -1
  49. package/skills/reference/lifecycle.md +25 -0
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A CGI's declared request budget owns operation work. Binary and Compose Apps
3
+ * have no compiled CGI deadline to configure, so their existing status allowance
4
+ * can enlarge the shared operation budget above its 60-second default.
5
+ */
6
+ export function operationBudgetSeconds(source) {
7
+ return source.backend?.timeoutSeconds ?? Math.max(60, source.lifecycleTimeouts?.status ?? source.daemon?.timeouts?.status ?? 5);
8
+ }
9
+ /** First released SDK that decodes operation/lifecycle budget config fields. */
10
+ export function runtimeBudgetSdkSupported(sdk) {
11
+ const [major, minor, patch] = sdk.split('.').map(Number);
12
+ return major > 0 || minor > 17 || minor === 17 && patch >= 5;
13
+ }
@@ -18,7 +18,7 @@ export declare function readBackground(value: unknown, fail: (error: StandaloneE
18
18
  export declare function renderBackgroundResource(): string;
19
19
  /** The worker's arguments: none for a Go App's own worker, `--worker` for the bridge runtime (spec/bridge/v1). */
20
20
  export declare const BRIDGE_WORKER_ARGS: readonly ["--worker"];
21
- export declare function renderBackgroundUnit(pkg: string, args?: readonly string[]): string;
21
+ export declare function renderBackgroundUnit(pkg: string, args?: readonly string[], stopSeconds?: number): string;
22
22
  /** Shell function that stops the worker in lifecycle hooks; call it as `stop_background_worker`. */
23
23
  export declare function renderBackgroundWorkerStop(pkg: string): string;
24
24
  /**
@@ -32,8 +32,8 @@ export declare function renderBackgroundWorkerStop(pkg: string): string;
32
32
  * exits. Stop only such a manager, as the package user; the acquire that
33
33
  * follows then starts a fresh one.
34
34
  */
35
- export declare function renderUserManagerRecovery(): string;
35
+ export declare function renderUserManagerRecovery(startSeconds?: number): string;
36
36
  /** Shell function that starts the worker and names the failed step; call it as `start_background_worker`. */
37
37
  export declare function renderBackgroundWorkerStart(pkg: string): string;
38
38
  /** Package Center's start/stop/status map straight onto the user unit. */
39
- export declare function renderBackgroundControl(pkg: string): string;
39
+ export declare function renderBackgroundControl(pkg: string, startSeconds?: number): string;
@@ -24,7 +24,7 @@ export function renderBackgroundResource() {
24
24
  }
25
25
  /** The worker's arguments: none for a Go App's own worker, `--worker` for the bridge runtime (spec/bridge/v1). */
26
26
  export const BRIDGE_WORKER_ARGS = ["--worker"];
27
- export function renderBackgroundUnit(pkg, args = []) {
27
+ export function renderBackgroundUnit(pkg, args = [], stopSeconds = 30) {
28
28
  return `[Unit]
29
29
  Description=${pkg} background worker
30
30
 
@@ -33,7 +33,7 @@ Type=simple
33
33
  ExecStart=${[`/var/packages/${pkg}/target/${BACKGROUND_WORKER_PAYLOAD}`, ...args].join(" ")}
34
34
  UMask=0077
35
35
  KillMode=control-group
36
- TimeoutStopSec=10
36
+ TimeoutStopSec=${stopSeconds}
37
37
  Restart=no
38
38
  `;
39
39
  }
@@ -66,13 +66,15 @@ export function renderBackgroundWorkerStop(pkg) {
66
66
  * exits. Stop only such a manager, as the package user; the acquire that
67
67
  * follows then starts a fresh one.
68
68
  */
69
- export function renderUserManagerRecovery() {
69
+ export function renderUserManagerRecovery(startSeconds = 30) {
70
70
  return `recover_user_manager() {
71
71
  uid="$(id -u)" || return 1
72
72
  [ "$uid" != 0 ] || return 0
73
- i=0
74
- while [ "$(/usr/bin/loginctl show-user "$uid" -p State 2>/dev/null)" = State=closing ] && [ "$i" -lt 100 ]; do
75
- sleep 0.1; i=$((i + 1))
73
+ manager_now() { awk '{print int($1)}' /proc/uptime; }
74
+ manager_until=$(( $(manager_now) + ${startSeconds} ))
75
+ while [ "$(/usr/bin/loginctl show-user "$uid" -p State 2>/dev/null)" = State=closing ]; do
76
+ [ "$(manager_now)" -lt "$manager_until" ] || { echo "package user manager still closing" >&2; return 1; }
77
+ sleep 0.1
76
78
  done
77
79
  [ -S "/run/user/$uid/systemd/private" ] && return 0
78
80
  for exe in /proc/[0-9]*/exe; do
@@ -81,12 +83,11 @@ export function renderUserManagerRecovery() {
81
83
  case " $(tr '\\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)" in *" --user "*) ;; *) continue ;; esac
82
84
  echo "package user manager $pid has no socket in /run/user/$uid; stopping it so DSM starts a fresh one" >&2
83
85
  kill -TERM "$pid" || return 1
84
- i=0
85
86
  # DSM's bash logs a failed kill to syslog even with 2>/dev/null, so the
86
87
  # exit is looked up in /proc.
87
88
  while [ -d "/proc/$pid" ]; do
88
- [ "$i" -lt 100 ] || { echo "package user manager $pid did not exit" >&2; return 1; }
89
- sleep 0.1; i=$((i + 1))
89
+ [ "$(manager_now)" -lt "$manager_until" ] || { echo "package user manager $pid did not exit" >&2; return 1; }
90
+ sleep 0.1
90
91
  done
91
92
  done
92
93
  }
@@ -105,10 +106,10 @@ export function renderBackgroundWorkerStart(pkg) {
105
106
  `;
106
107
  }
107
108
  /** Package Center's start/stop/status map straight onto the user unit. */
108
- export function renderBackgroundControl(pkg) {
109
+ export function renderBackgroundControl(pkg, startSeconds = 30) {
109
110
  const unit = backgroundUnitName(pkg);
110
111
  return `#!/bin/sh
111
- ${renderUserManagerRecovery()}${renderBackgroundWorkerStart(pkg)}case "$1" in
112
+ ${renderUserManagerRecovery(startSeconds)}${renderBackgroundWorkerStart(pkg)}case "$1" in
112
113
  prestart) recover_user_manager ;;
113
114
  prestop) exit 0 ;;
114
115
  start) start_background_worker ;;
@@ -1,7 +1,9 @@
1
+ import { type ComposeAppData } from "../runtime/compose-data.js";
1
2
  import type { NativeArchFamily } from "../build-spk/dsm-arch.js";
2
3
  import { type Daemon } from "./daemon.js";
3
4
  import type { StandaloneError, StandaloneManifest } from "./manifest.js";
4
5
  export interface ComposePlan {
6
+ app_data?: ComposeAppData;
5
7
  startup_timeout_seconds?: number;
6
8
  ingress?: {
7
9
  address: string;
@@ -79,7 +81,7 @@ export declare function readComposeApp(value: unknown, root: string, arches: Nat
79
81
  architecture?: NativeArchFamily;
80
82
  imageProofs?: unknown;
81
83
  }): ComposeApp | undefined;
82
- export declare function composeDaemon(m: Pick<StandaloneManifest, "compose">): Daemon;
84
+ export declare function composeDaemon(m: Pick<StandaloneManifest, "compose" | "lifecycleTimeouts">): Daemon;
83
85
  export declare function composeNetwork(m: Pick<StandaloneManifest, "compose" | "identityGateway" | "spkVersion">): {
84
86
  public_ports: number[];
85
87
  readiness?: {
@@ -1,3 +1,4 @@
1
+ import { validComposeAppData } from "../runtime/compose-data.js";
1
2
  import { composeDiagnosisMessages, renderComposeDiagnosis } from "../build-spk/compose-diagnosis.js";
2
3
  import { COMPOSE_ERRORS } from "./compose-errors.js";
3
4
  import { spawnSync } from "node:child_process";
@@ -117,6 +118,12 @@ export function readComposeApp(value, root, arches, fail, context = {}) {
117
118
  plan.startup_timeout_seconds !== value.startup_timeout_seconds)
118
119
  throw new Error("[SDK_COMPOSE_UNSUPPORTED] This SDK does not implement the declared startup budget; use the matching SDK/CLI release.");
119
120
  const document = parse(plan.effective);
121
+ const volumes = Object.entries(document?.volumes ?? {});
122
+ if ((!validComposeAppData(plan.app_data, context.package ?? "ComposeApp")
123
+ || JSON.stringify(plan.app_data) !== JSON.stringify(plan.lock?.app_data)
124
+ || plan.app_data.project !== plan.project_name || plan.app_data.volumes.length !== volumes.length
125
+ || volumes.some(([key, value]) => !plan.app_data.volumes.some(v => v.key === key && v.name === value.name))))
126
+ throw new Error("[SDK_COMPOSE_UNSUPPORTED] This SDK does not provide the complete Compose App data contract; update the SDK and rebuild.");
120
127
  const web = value.web;
121
128
  return {
122
129
  declaration: { ...value },
@@ -150,7 +157,7 @@ export function composeDaemon(m) {
150
157
  args: ["run"],
151
158
  targets: m.compose.targets,
152
159
  env: {},
153
- timeouts: { start: m.compose.plan.startup_timeout_seconds ?? 120, stop: 30, status: 5, uninstall: 60 },
160
+ timeouts: { stop: 30, status: 5, uninstall: 60, ...m.lifecycleTimeouts, start: m.compose.plan.startup_timeout_seconds ?? m.lifecycleTimeouts?.start ?? 120 },
154
161
  };
155
162
  }
156
163
  export function composeNetwork(m) {
@@ -30,3 +30,7 @@ export declare function buildDaemonControl(manifest: StandaloneManifest, arch: N
30
30
  export declare function copyDaemonPayload(manifest: StandaloneManifest, arch: NativeArchFamily, target: string, controller: string): void;
31
31
  /** Copies a bounded, link-free package-account payload tree. */
32
32
  export declare function copyPayloadTree(root: string, source: string, target: string): void;
33
+ /** Existing lifecycle namespace; a timeout-only declaration runs no daemon. */
34
+ export declare function readLifecycleTimeouts(value: unknown, fail: (e: StandaloneError) => void): Daemon['timeouts'] | undefined;
35
+ /** Legacy SDKs keep their supported config and timeout bounds. */
36
+ export declare function runtimeBudgetSource(dir: string, manifest?: StandaloneManifest): string;
@@ -1,5 +1,6 @@
1
+ import { operationBudgetSeconds, runtimeBudgetSdkSupported } from "../runtime/runtime-budgets.js";
1
2
  import { goChildEnv } from "../runtime/go-env.js";
2
- import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
3
+ import { chmodSync, existsSync, copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, writeFileSync, } from "node:fs";
3
4
  import { isAbsolute, join, relative, resolve } from "node:path";
4
5
  import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
5
6
  import { sdkSource, appSdkVersion } from "../runtime/sdk-source.js";
@@ -79,23 +80,9 @@ export function readDaemon(value, root, arches, fail) {
79
80
  Object.keys(env).length > 64 ||
80
81
  Object.entries(env).some(([k, v]) => !envKey(k) || !token(v)))
81
82
  return invalid();
82
- const limits = value.timeouts ?? {};
83
- if (!record(limits) ||
84
- Object.keys(limits).some((k) => !["start", "stop", "status", "uninstall"].includes(k)))
85
- return invalid();
86
- // Defaults hold under IO load on a busy NAS; DSM's package unit allows 3600 s.
87
- const timeouts = {
88
- start: limits.start ?? 30,
89
- stop: limits.stop ?? 30,
90
- status: limits.status ?? 5,
91
- uninstall: limits.uninstall ?? 60,
92
- };
93
- for (const [key, v] of Object.entries(timeouts)) {
94
- if (!Number.isInteger(v) ||
95
- v < (key === "status" ? 1 : 2) ||
96
- v > (key === "status" ? 10 : 120))
97
- return invalid();
98
- }
83
+ const timeouts = readLifecycleTimeouts(value.timeouts, fail);
84
+ if (!timeouts)
85
+ return undefined;
99
86
  return {
100
87
  exec: value.exec,
101
88
  targets,
@@ -107,6 +94,11 @@ export function readDaemon(value, root, arches, fail) {
107
94
  timeouts: timeouts,
108
95
  };
109
96
  }
97
+ function daemonSdkVersion(manifest) {
98
+ return manifest.identityGateway?.sdk ??
99
+ (manifest.backend.kind === "go" ? appSdkVersion(manifest.backend) : undefined) ??
100
+ templateSdkVersion();
101
+ }
110
102
  export function daemonConfig(manifest) {
111
103
  const { exec, args, uninstall_args, env, timeouts } = manifest.compose
112
104
  ? composeDaemon(manifest)
@@ -120,6 +112,8 @@ export function daemonConfig(manifest) {
120
112
  ...(uninstall_args ? { uninstall_args } : {}),
121
113
  env,
122
114
  timeouts,
115
+ ...(runtimeBudgetSdkSupported(daemonSdkVersion(manifest))
116
+ ? { operation_seconds: operationBudgetSeconds(manifest) } : {}),
123
117
  ...(manifest.compose ? { network: composeNetwork(manifest) } : {}),
124
118
  }) + "\n");
125
119
  }
@@ -137,11 +131,7 @@ export async function buildDaemonControl(manifest, arch, work, run) {
137
131
  out,
138
132
  "./daemonctl",
139
133
  ], {
140
- cwd: sdkSource(manifest.identityGateway?.sdk ??
141
- (manifest.backend.kind === "go"
142
- ? appSdkVersion(manifest.backend)
143
- : undefined) ??
144
- templateSdkVersion()).goDir,
134
+ cwd: runtimeBudgetSource(sdkSource(daemonSdkVersion(manifest)).goDir, manifest),
145
135
  env: goChildEnv({ ...process.env, ...pinnedGoBuildEnv(arch) }),
146
136
  });
147
137
  if (result.missing || result.status !== 0)
@@ -212,3 +202,26 @@ export function copyPayloadTree(root, source, target) {
212
202
  invalid(`Cannot read the daemon payload: ${String(error)}`);
213
203
  }
214
204
  }
205
+ /** Existing lifecycle namespace; a timeout-only declaration runs no daemon. */
206
+ export function readLifecycleTimeouts(value, fail) {
207
+ const v = value ?? {};
208
+ const bad = () => { fail({ code: 'STANDALONE_DAEMON_INVALID', message: 'daemon.timeouts accepts start/stop/uninstall 2–3600s and status 1–3600s.' }); return undefined; };
209
+ if (!record(v) || Object.keys(v).some(k => !['start', 'stop', 'status', 'uninstall'].includes(k)))
210
+ return bad();
211
+ const limits = { start: v.start ?? 30, stop: v.stop ?? 30, status: v.status ?? 5, uninstall: v.uninstall ?? 60 };
212
+ for (const [key, n] of Object.entries(limits))
213
+ if (!Number.isInteger(n) || Number(n) < (key === 'status' ? 1 : 2) || Number(n) > 3600)
214
+ return bad();
215
+ return limits;
216
+ }
217
+ /** Legacy SDKs keep their supported config and timeout bounds. */
218
+ export function runtimeBudgetSource(dir, manifest) {
219
+ if (!existsSync(join(dir, 'internal/runtimebudget/budget.go'))) {
220
+ const timeouts = manifest?.daemon?.timeouts ?? manifest?.lifecycleTimeouts;
221
+ if (!manifest || operationBudgetSeconds(manifest) > 60 || timeouts &&
222
+ (timeouts.start > 120 || timeouts.stop > 120 || timeouts.status > 10 || timeouts.uninstall > 120)) {
223
+ throw new StandaloneBuildError('STANDALONE_DAEMON_BUILD_FAILED', 'The requested timeouts need an SDK with runtime budget support; use SDK 0.17.5 or later before rebuilding.');
224
+ }
225
+ }
226
+ return dir;
227
+ }
@@ -1,3 +1,4 @@
1
+ import { gatewayEntryPath } from './native-gateway.js';
1
2
  import { resolvedSdkLaunch, dsmLauncherType } from "../contract/app-launch.js";
2
3
  import { REOPEN_EVENTS_RUNTIME } from "./reopen-events.js";
3
4
  import { LAUNCH_PARAMS_RUNTIME } from "./launch-params.js";
@@ -120,7 +121,7 @@ export function renderUiConfig(manifest, icons = packageIcons(manifest.icon), lo
120
121
  ...titleFields,
121
122
  icon,
122
123
  ...launcherAccess(manifest),
123
- url: `/webman/3rdparty/${manifest.package}/sso/open.cgi`,
124
+ url: gatewayEntryPath(manifest),
124
125
  },
125
126
  },
126
127
  }, null, 2) + "\n");
@@ -1,3 +1,5 @@
1
+ import { nativeGatewaySupported } from './native-gateway.js';
2
+ import { operationBudgetSeconds, runtimeBudgetSdkSupported } from "../runtime/runtime-budgets.js";
1
3
  import { existsSync, lstatSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem, } from "../build-spk/go-toolchain.js";
@@ -5,7 +7,7 @@ import { goChildEnv } from "../runtime/go-env.js";
5
7
  import { sdkSource } from "../runtime/sdk-source.js";
6
8
  import { checkCgiBinary } from "./binary.js";
7
9
  import { isWebAccessMode, DSM_GROUP_NAME, } from "../contract/web-launch.js";
8
- import { relativeExec, token } from "./daemon.js";
10
+ import { relativeExec, runtimeBudgetSource, token } from "./daemon.js";
9
11
  import { StandaloneBuildError } from "./spk.js";
10
12
  /** The SDK's identity gateway: the daemon wrapper and the DSM-origin authorize CGI. */
11
13
  export const GATEWAY_MAIN = "./idgateway/cmd/gateway";
@@ -309,6 +311,11 @@ export function identityGatewayConfig(m) {
309
311
  const g = m.identityGateway;
310
312
  return (JSON.stringify({
311
313
  package: m.package,
314
+ ...(nativeGatewaySupported(m) ? { native_ingress: true } : {}),
315
+ ...(runtimeBudgetSdkSupported(g.sdk) ? {
316
+ operation_seconds: operationBudgetSeconds(m),
317
+ stop_seconds: m.daemon?.timeouts.stop ?? m.lifecycleTimeouts?.stop ?? 30,
318
+ } : {}),
312
319
  mode: g.mode,
313
320
  access: g.access ?? "dsm-users",
314
321
  ...(g.groups ? { groups: g.groups } : {}),
@@ -385,6 +392,7 @@ export async function buildIdentityGateway(m, arch, work, run) {
385
392
  catch (error) {
386
393
  throw new StandaloneBuildError("SDK_DOWNLOAD_FAILED", error.message.replace(/^\[[A-Z_]+\] /, ""));
387
394
  }
395
+ runtimeBudgetSource(sdk.goDir, m);
388
396
  const out = join(work, `identity-gateway-${arch}`);
389
397
  const ldflags = `-s -w -buildid= -X main.configPath=${packageDest(m)}/${GATEWAY_CONFIG_PATH}`;
390
398
  const result = await run("go", [