@pellux/goodvibes-daemon 1.28.19 → 1.28.21

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 (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. package/src/version.ts +2 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Service-lifecycle subcommands `goodvibes-daemon install-service |
2
+ * Service-lifecycle subcommands, `goodvibes-daemon install-service |
3
3
  * uninstall-service | service-status`.
4
4
  *
5
5
  * The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
11
11
  * bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
12
- * REAL wired service machinery reached in production by the daemon's own HTTP
13
- * `/api/service/*` routes via facade-composition.ts is
12
+ * REAL wired service machinery, reached in production by the daemon's own HTTP
13
+ * `/api/service/*` routes via facade-composition.ts, is
14
14
  * `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
15
15
  * systemd/launchd/windows-aware manager with install/uninstall/status/start/
16
16
  * stop/restart and a `suggestedCommands` hint list. This module now rewires the
@@ -38,16 +38,16 @@
38
38
  * honestly queries systemd via the injected actionRunner) BEFORE the
39
39
  * legacy unit is stopped, disabled, or removed. A failed or unhealthy new
40
40
  * unit rolls itself back (uninstalled) and never touches the legacy one
41
- * a botched takeover must never cost the user their working daemon.
41
+ * , a botched takeover must never cost the user their working daemon.
42
42
  * - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
43
43
  * something is already listening on the configured host:port (Mike's real
44
44
  * dev-host case: a manual `nohup`'d daemon with no unit at all), this is
45
- * an unidentified process, not a managed unit there is nothing to stop
45
+ * an unidentified process, not a managed unit, there is nothing to stop
46
46
  * or disable, and this module will not attempt to kill it. It warns and
47
47
  * leaves the decision to the operator.
48
48
  * - Every action (legacy stop/disable, unit-file removal, daemon-reload)
49
49
  * goes through the SAME injectable `actionRunner`/`legacyUnitFileRemove`
50
- * seams tests already use this module never has a code path that bypasses
50
+ * seams tests already use, this module never has a code path that bypasses
51
51
  * them, so the migration is exercised deterministically via fakes and never
52
52
  * touches a real running service in tests.
53
53
  */
@@ -67,7 +67,7 @@ import {
67
67
  type ManagedServiceActionRunner,
68
68
  } from '../runtime/legacy-daemon-migration.ts';
69
69
 
70
- // `resolveInstalledDaemonBinary` lives in the runtime module re-exported
70
+ // `resolveInstalledDaemonBinary` lives in the runtime module, re-exported
71
71
  // here so this module stays the CLI's stable public surface (and so existing
72
72
  // test imports keep working).
73
73
  export {
@@ -83,7 +83,7 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
83
83
  // literal name `goodvibes-daemon.service`. This module (rewired onto
84
84
  // PlatformServiceManager, see the file banner above) manages a DIFFERENT
85
85
  // unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
86
- // unit installed this dev machine included would otherwise see
86
+ // unit installed, this dev machine included, would otherwise see
87
87
  // service-status honestly report installed:false/running:false for the
88
88
  // tracked name while the legacy unit keeps running untouched underneath it,
89
89
  // uninstall-service would silently leave the legacy unit orphaned with no
@@ -94,13 +94,13 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
94
94
  // migration engine (`runLegacyDaemonMigration`) both live in
95
95
  // `../runtime/legacy-daemon-migration.ts` rather than here, because this
96
96
  // daemon's own boot-time reconcile (`../runtime/legacy-daemon-reconcile.ts`)
97
- // needs them too the entrypoint-agnostic `runtime` layer is what both of
97
+ // needs them too, the entrypoint-agnostic `runtime` layer is what both of
98
98
  // this repository's consumers can import, so this CLI module is just one of
99
99
  // them. The terminal app's own onboarding guided UX solves the same problem
100
100
  // independently, under its own architecture constraints.
101
101
  //
102
102
  // This detection is entirely independent of PlatformServiceManager's own
103
- // status() it does not rely on (or get invalidated by) the parallel SDK
103
+ // status(), it does not rely on (or get invalidated by) the parallel SDK
104
104
  // fix that makes status().running itself query systemd honestly for the
105
105
  // TRACKED unit name.
106
106
  // ---------------------------------------------------------------------------
@@ -125,7 +125,7 @@ export function isDaemonServiceSubcommand(value: string | undefined): value is D
125
125
  * Exit codes `service-status` reports, so a script never parses prose.
126
126
  *
127
127
  * 0/3/4 are the three answers the question actually has; 1 stays what it has
128
- * always been the platform refused the query and the error is printed.
128
+ * always been, the platform refused the query and the error is printed.
129
129
  */
130
130
  export const SERVICE_STATUS_EXIT_RUNNING = 0;
131
131
  export const SERVICE_STATUS_EXIT_INSTALLED_NOT_RUNNING = 3;
@@ -134,10 +134,10 @@ export const SERVICE_STATUS_EXIT_NOT_INSTALLED = 4;
134
134
  export interface DaemonServiceCliInput {
135
135
  readonly subcommand: DaemonServiceSubcommand;
136
136
  readonly binaryPath: string;
137
- /** The GoodVibes tree home (GOODVIBES_HOME-overridable) config/state root only, never unit-path resolution. See `unitHomeDir`. */
137
+ /** The GoodVibes tree home (GOODVIBES_HOME-overridable), config/state root only, never unit-path resolution. See `unitHomeDir`. */
138
138
  readonly homeDir: string;
139
139
  /**
140
- * The LOGIN user's home where the systemd/launchd/Windows unit actually
140
+ * The LOGIN user's home, where the systemd/launchd/Windows unit actually
141
141
  * lives, regardless of any GOODVIBES_HOME/GOODVIBES_DAEMON_HOME override.
142
142
  * Required (no default to `homeDir`) so a unit-management subcommand can
143
143
  * never silently search for its unit under a relocated tree home instead of
@@ -148,14 +148,14 @@ export interface DaemonServiceCliInput {
148
148
  readonly port: number;
149
149
  /**
150
150
  * Whether the invoking CLI line carried an explicit `--hostname`/`--port`
151
- * flag (not merely whether `host`/`port` above are set those are always
151
+ * flag (not merely whether `host`/`port` above are set, those are always
152
152
  * set, resolved from config either way). `install-service`/`migrate-service`
153
- * refuse when either is true see `validateServiceEndpointFlags`. Defaults
153
+ * refuse when either is true, see `validateServiceEndpointFlags`. Defaults
154
154
  * to false (no flag), matching every subcommand these two don't apply to.
155
155
  */
156
156
  readonly hostnameFlagProvided?: boolean | undefined;
157
157
  readonly portFlagProvided?: boolean | undefined;
158
- /** Defaults to `homeDir` overridable so tests can scope both to one tempdir. */
158
+ /** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
159
159
  readonly workingDirectory?: string | undefined;
160
160
  /** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
161
161
  readonly configManager?: ConfigManager | undefined;
@@ -166,7 +166,7 @@ export interface DaemonServiceCliInput {
166
166
  /**
167
167
  * `migrate-service` only: explicit consent to actually execute the
168
168
  * migration (wired from the CLI's `-y`/`--yes` flag). Without it, the
169
- * subcommand prints the exact plan and changes nothing never auto-migrate.
169
+ * subcommand prints the exact plan and changes nothing, never auto-migrate.
170
170
  */
171
171
  readonly confirmMigration?: boolean | undefined;
172
172
  /**
@@ -183,7 +183,7 @@ export interface DaemonServiceCliInput {
183
183
  readonly legacyUnitFileRemove?: ((path: string) => void) | undefined;
184
184
  /**
185
185
  * `service-status` only: report one JSON document instead of prose. The exit
186
- * code is the same either way the codes are the machine-readable answer,
186
+ * code is the same either way, the codes are the machine-readable answer,
187
187
  * and JSON is for when a script wants the fields as well.
188
188
  */
189
189
  readonly json?: boolean | undefined;
@@ -200,7 +200,7 @@ export interface DaemonServiceCliResult {
200
200
  /**
201
201
  * The manager's definition (`ExecStart` command/args, name, description) is
202
202
  * built once, in `../runtime/legacy-daemon-migration.ts`, and shared with the
203
- * onboarding guided UX see that module's doc comment for why.
203
+ * onboarding guided UX, see that module's doc comment for why.
204
204
  */
205
205
  function buildManager(input: DaemonServiceCliInput): PlatformServiceManager {
206
206
  return buildManagedDaemonServiceManager({
@@ -225,12 +225,12 @@ function statusLines(status: ManagedServiceStatus): string[] {
225
225
  if (status.pid !== undefined) lines.push(`pid: ${status.pid}`);
226
226
  if (status.platform !== 'manual' && status.installed && !status.running) {
227
227
  // W3 Finding 4: this used to assert "'running' here only reflects
228
- // processes this tool started directly" true for the pid-file-only
228
+ // processes this tool started directly", true for the pid-file-only
229
229
  // check the (currently linked) SDK still uses, but the parallel SDK
230
230
  // batch is making status().running query systemd honestly via
231
231
  // `is-active`, which would make that specific claim stale. Drop the
232
232
  // claim about HOW running was computed and just offer the escape
233
- // hatch true and useful under either SDK version.
233
+ // hatch, true and useful under either SDK version.
234
234
  lines.push(
235
235
  `note: if this looks wrong, verify directly: ${status.suggestedCommands[status.suggestedCommands.length - 1] ?? 'the platform service-status command'}`,
236
236
  );
@@ -264,7 +264,7 @@ export function buildInstallResultLines(status: ManagedServiceStatus): string[]
264
264
  * Every line names the RESOLVED unit (`status.serviceName`, `status.path`) and
265
265
  * says what the platform reported afterwards, rather than asserting the verb
266
266
  * worked because it was dispatched without throwing. A verb aimed at a service
267
- * that is not installed says exactly that installing it silently would be a
267
+ * that is not installed says exactly that, installing it silently would be a
268
268
  * different command than the one that was run.
269
269
  */
270
270
  export function buildLifecycleResultLines(
@@ -364,7 +364,7 @@ function ok(action: 'install' | 'uninstall' | 'status', status: ManagedServiceSt
364
364
  lines.push(`removed the ${status.platform} service at ${status.path}`);
365
365
  if (status.platform === 'systemd') {
366
366
  lines.push(
367
- "note: this removes the unit file but does not run `disable` run " +
367
+ "note: this removes the unit file but does not run `disable`; run " +
368
368
  "`systemctl --user daemon-reload` to clear any stale enablement symlink.",
369
369
  );
370
370
  }
@@ -388,12 +388,12 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
388
388
  * `install-service` and `migrate-service` refuse an explicit
389
389
  * `--hostname`/`--port` flag rather than silently accepting it. Both flags are
390
390
  * RUNTIME-ONLY overrides applied to this one invocation's in-memory config
391
- * (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`) but the unit
391
+ * (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`), but the unit
392
392
  * this subcommand writes carries no endpoint flags at all (see
393
393
  * `buildManagedDaemonServiceManager`'s ExecStart doc): the daemon always
394
394
  * re-resolves `controlPlane.host`/`port`/`hostMode` from PERSISTED settings at
395
395
  * boot. A flag accepted here would print/probe a binding the installed unit
396
- * will never actually have an honest gap, not a cosmetic one, since the
396
+ * will never actually have, an honest gap, not a cosmetic one, since the
397
397
  * printed value is exactly what an operator would expect the running service
398
398
  * to bind to. Refusing with a pointer to the persistent config path is
399
399
  * strictly better than silently doing nothing with the flag.
@@ -408,7 +408,7 @@ export function validateServiceEndpointFlags(
408
408
  .filter((name): name is string => name !== null)
409
409
  .join('/');
410
410
  return [
411
- `${subcommand} refused: ${flagNames} only override this process's runtime config the installed unit carries ` +
411
+ `${subcommand} refused: ${flagNames} only override this process's runtime config; the installed unit carries ` +
412
412
  'no endpoint flags and re-resolves controlPlane.host/port/hostMode from persisted settings at boot, so the ' +
413
413
  'override would never take effect there.',
414
414
  `Set the persistent binding instead: goodvibes-daemon config set controlPlane.host / controlPlane.port (or ` +
@@ -419,7 +419,7 @@ export function validateServiceEndpointFlags(
419
419
  /**
420
420
  * `migrate-service`: the guided, consented takeover of the legacy
421
421
  * `goodvibes-daemon.service` unit. Thin wrapper over
422
- * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) see
422
+ * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`), see
423
423
  * that module for the design constraints (never auto-migrate,
424
424
  * new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
425
425
  * every action through an injectable seam) and the onboarding UX consumer.
@@ -461,7 +461,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
461
461
  case 'install-service': {
462
462
  // W3 Finding 4: refuse rather than risk starting a second daemon
463
463
  // alongside an already-installed legacy unit. Refuses whenever the
464
- // legacy unit is present at all (not just when currently active)
464
+ // legacy unit is present at all (not just when currently active),
465
465
  // an installed-but-inactive legacy unit can still be enabled and
466
466
  // start competing for the same port later, and "never silently
467
467
  // start a second daemon" is the bar here, not "never right now."
@@ -483,7 +483,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
483
483
  if (installed.actionError) return failed('install', installed);
484
484
  const started = manager.start();
485
485
  // start()'s actionError (e.g. a platform this manager can't dispatch
486
- // actions for) doesn't undo the write report install as ok, but surface
486
+ // actions for) doesn't undo the write, report install as ok, but surface
487
487
  // the follow-up problem honestly instead of claiming it is running.
488
488
  return started.actionError
489
489
  ? ok('install', { ...started, running: false }, [`could not start it automatically: ${started.actionError}`])
@@ -496,7 +496,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
496
496
  const extra: string[] = [];
497
497
  if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
498
498
  // This command only ever touches the TRACKED unit (whatever name/path
499
- // actually resolved not necessarily the SERVICE_NAME constant) above
499
+ // actually resolved, not necessarily the SERVICE_NAME constant) above,
500
500
  // say so explicitly when an install-script unit also exists, so its
501
501
  // continued presence is never a silent surprise.
502
502
  if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
@@ -520,8 +520,8 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
520
520
  };
521
521
  }
522
522
  const result = ok('status', status, legacy.present ? [legacyUnitNote(legacy, resolveManagedUnitName(status))] : []);
523
- // The status verb reports; it does not fail. `ok` is still true the
524
- // question was answered while the exit code carries the answer itself
523
+ // The status verb reports; it does not fail. `ok` is still true, the
524
+ // question was answered, while the exit code carries the answer itself
525
525
  // (0 running / 3 installed-not-running / 4 not installed), so a script
526
526
  // never has to read the prose above.
527
527
  return { ...result, exitCode: serviceStatusExitCode(status) };
@@ -1,14 +1,14 @@
1
1
  /**
2
- * sessions-command.ts `goodvibes-daemon sessions list|kill <id>`.
2
+ * sessions-command.ts, `goodvibes-daemon sessions list|kill <id>`.
3
3
  *
4
4
  * The sessions this daemon HOSTS: conversation loops running inside it, which
5
5
  * outlive the client that started them. That is the whole reason they need a
6
- * command a terminal's own session dies with the terminal and never needs
6
+ * command, a terminal's own session dies with the terminal and never needs
7
7
  * listing from outside, while a hosted one can be running on a headless box
8
8
  * with nothing attached to it at all.
9
9
  *
10
10
  * The verbs are `sessions.hosted.list` and `sessions.hosted.kill`, and both are
11
- * declared ws-only in the method catalog they have no REST binding, so they
11
+ * declared ws-only in the method catalog, they have no REST binding, so they
12
12
  * go through `callDaemonWsVerb` rather than `callDaemonVerb`. Same target, same
13
13
  * operator token, same --host/--port/--token convention; only the transport is
14
14
  * different. A daemon built without hosted sessions answers "does not know the
@@ -62,7 +62,7 @@ export interface SessionsCommandFlags extends RemoteCommandFlags {
62
62
 
63
63
  export interface RunSessionsCommandInput extends RemoteCommandDeps {
64
64
  readonly flags: SessionsCommandFlags;
65
- /** Positional words after `sessions` the subcommand and its argument. */
65
+ /** Positional words after `sessions`, the subcommand and its argument. */
66
66
  readonly args: readonly string[];
67
67
  }
68
68
 
@@ -119,7 +119,7 @@ function renderSession(session: HostedSessionRecord, now: number): string[] {
119
119
  *
120
120
  * Exit 0 when the daemon answered, 1 when it refused or could not be reached,
121
121
  * 2 when the command line was wrong. A `kill` with no id is a usage refusal
122
- * rather than a "kill everything" there is no shape of this command that ends
122
+ * rather than a "kill everything", there is no shape of this command that ends
123
123
  * more than the one session named.
124
124
  */
125
125
  export async function runSessionsCommand(input: RunSessionsCommandInput): Promise<DaemonCommandResult> {
@@ -129,11 +129,11 @@ export async function runSessionsCommand(input: RunSessionsCommandInput): Promis
129
129
  return refusal('name what to do with the sessions.', flags.json);
130
130
  }
131
131
  if (!isSessionsSubcommand(subcommand)) {
132
- return refusal(`'${subcommand}' is not a sessions command try list or kill.`, flags.json);
132
+ return refusal(`'${subcommand}' is not a sessions command; try list or kill.`, flags.json);
133
133
  }
134
134
  const sessionId = args[1];
135
135
  if (subcommand === 'kill' && sessionId === undefined) {
136
- return refusal('kill needs the session to end run `sessions list` to see them.', flags.json);
136
+ return refusal('kill needs the session to end; run `sessions list` to see them.', flags.json);
137
137
  }
138
138
  if (args.length > (subcommand === 'kill' ? 2 : 1)) {
139
139
  return refusal(`'${args[subcommand === 'kill' ? 2 : 1]}' is one argument too many.`, flags.json);
@@ -1,9 +1,9 @@
1
1
  /**
2
- * status-command.ts `goodvibes-daemon status` and `goodvibes-daemon update`.
2
+ * status-command.ts, `goodvibes-daemon status` and `goodvibes-daemon update`.
3
3
  *
4
4
  * The question a headless box's operator asks first: is it up, what version, on
5
5
  * what address, is anything unhealthy, and what did it do to itself while I was
6
- * not looking. Before this, the binary answered none of that `status` fell
6
+ * not looking. Before this, the binary answered none of that, `status` fell
7
7
  * through the parser and started a SECOND daemon in the foreground.
8
8
  *
9
9
  * WHERE EACH LINE COMES FROM
@@ -13,11 +13,11 @@
13
13
  * established: `/status` for identity, `/api/health` for the health roll-up and
14
14
  * the address it actually bound, `/api/channels/status` for the channels, and
15
15
  * `/api/cluster/status` for this machine's place in its group. Hosted sessions
16
- * are a ws-only verb family, so they go through `callDaemonWsVerb` same
16
+ * are a ws-only verb family, so they go through `callDaemonWsVerb`, same
17
17
  * target, same token, different transport.
18
18
  *
19
- * Everything about the daemon's own HISTORY uptime, the receipts it wrote,
20
- * the version an automatic rollback rejected comes from files on the daemon's
19
+ * Everything about the daemon's own HISTORY, uptime, the receipts it wrote,
20
+ * the version an automatic rollback rejected, comes from files on the daemon's
21
21
  * host, because no verb reports them. That makes those lines local-only, and
22
22
  * they say so for a remote target instead of being guessed at.
23
23
  *
@@ -76,8 +76,8 @@ export interface RemoteCommandDeps {
76
76
  *
77
77
  * It carries a `cluster` block the operator contract does not list, and that
78
78
  * block is where this daemon's ROLE in its group comes from. `cluster.uptimeMs`
79
- * is the coordinator's, not the daemon's it reads 0 on a daemon that has been
80
- * up for hours so the uptime line comes from the lifecycle marker instead.
79
+ * is the coordinator's, not the daemon's, it reads 0 on a daemon that has been
80
+ * up for hours, so the uptime line comes from the lifecycle marker instead.
81
81
  */
82
82
  interface ControlStatusPayload {
83
83
  readonly status?: string;
@@ -179,7 +179,7 @@ function optionalLine(label: string, value: string | undefined): string[] {
179
179
  *
180
180
  * `/status` reports `version` from the platform package while the cluster block
181
181
  * it carries reports the DAEMON build's version, and against a live daemon
182
- * those disagreed 1.21.0 against 1.28.0. Printing one of them silently would
182
+ * those disagreed, 1.21.0 against 1.28.0. Printing one of them silently would
183
183
  * put a number on this page that is wrong for whichever question the reader had
184
184
  * in mind, so both are printed and labelled until the daemon states one.
185
185
  */
@@ -210,7 +210,7 @@ function localStateLines(state: LocalDaemonState): string[] {
210
210
  }
211
211
  if (state.marker?.rejectedVersion !== undefined) {
212
212
  lines.push(
213
- ` rejected: ${state.marker.rejectedVersion} crash looped and was rolled back `
213
+ ` rejected: ${state.marker.rejectedVersion} crash looped and was rolled back; `
214
214
  + 'the update loop will not install that version again',
215
215
  );
216
216
  }
@@ -229,7 +229,7 @@ function localStateLines(state: LocalDaemonState): string[] {
229
229
  }
230
230
 
231
231
  function healthLines(outcome: DaemonVerbOutcome<HealthPayload>): string[] {
232
- if (!outcome.ok) return [` health: could not read ${outcome.error}`];
232
+ if (!outcome.ok) return [` health: could not read: ${outcome.error}`];
233
233
  const health = outcome.data;
234
234
  const lines = [` health: ${health.overall ?? 'unknown'}`];
235
235
  const binding = health.network?.controlPlane;
@@ -250,12 +250,12 @@ function healthLines(outcome: DaemonVerbOutcome<HealthPayload>): string[] {
250
250
  *
251
251
  * Only a channel that is switched ON and not healthy is named. Every channel
252
252
  * the daemon knows about appears in this payload, and a daemon with one
253
- * configured channel ships sixteen more in state `disabled` listing those as
253
+ * configured channel ships sixteen more in state `disabled`, listing those as
254
254
  * problems produced a seventeen-line wall under a healthy daemon and buried the
255
255
  * one line that meant something.
256
256
  */
257
257
  function channelLines(outcome: DaemonVerbOutcome<ChannelsPayload>): string[] {
258
- if (!outcome.ok) return [` channels: could not read ${outcome.error}`];
258
+ if (!outcome.ok) return [` channels: could not read: ${outcome.error}`];
259
259
  const channels = outcome.data.channels ?? [];
260
260
  if (channels.length === 0) return [' channels: none configured'];
261
261
  const on = channels.filter((channel) => channel.enabled !== false);
@@ -276,7 +276,7 @@ function channelLines(outcome: DaemonVerbOutcome<ChannelsPayload>): string[] {
276
276
  * Two sources, because neither answers the whole question: `/status` carries
277
277
  * the ROLE this node currently holds, and `/api/cluster/status` carries the
278
278
  * GROUP it holds that role in. A daemon with sharing switched off says so and
279
- * stops a role inside no group is not information.
279
+ * stops, a role inside no group is not information.
280
280
  */
281
281
  function clusterLines(
282
282
  identity: ControlStatusPayload,
@@ -284,18 +284,18 @@ function clusterLines(
284
284
  ): string[] {
285
285
  const role = identity.cluster?.role;
286
286
  if (identity.cluster?.enabled === false) {
287
- return [' cluster: off this machine handles its own inbound work'];
287
+ return [' cluster: off, this machine handles its own inbound work'];
288
288
  }
289
289
  if (!outcome.ok) {
290
290
  return [
291
291
  role === undefined
292
- ? ` cluster: could not read ${outcome.error}`
293
- : ` cluster: ${role} (the group view could not be read ${outcome.error})`,
292
+ ? ` cluster: could not read: ${outcome.error}`
293
+ : ` cluster: ${role} (the group view could not be read: ${outcome.error})`,
294
294
  ];
295
295
  }
296
296
  const cluster = outcome.data;
297
297
  if (cluster.membership === 'no-group') {
298
- return [' cluster: in no group yet `goodvibes-daemon cluster create` starts one'];
298
+ return [' cluster: in no group yet, `goodvibes-daemon cluster create` starts one'];
299
299
  }
300
300
  const group = cluster.groupName ?? cluster.groupId ?? 'its group';
301
301
  const members = cluster.memberCount === undefined ? '' : ` of ${cluster.memberCount}`;
@@ -303,7 +303,7 @@ function clusterLines(
303
303
  }
304
304
 
305
305
  function hostedSessionLines(outcome: DaemonVerbOutcome<HostedSessionsPayload>): string[] {
306
- if (!outcome.ok) return [` sessions: could not read ${outcome.error}`];
306
+ if (!outcome.ok) return [` sessions: could not read: ${outcome.error}`];
307
307
  const sessions = outcome.data.sessions ?? [];
308
308
  return [` sessions: ${sessions.length} hosted by this daemon`];
309
309
  }
@@ -317,7 +317,7 @@ export interface RunStatusCommandInput extends RemoteCommandDeps {
317
317
  *
318
318
  * Exit 0 when the daemon answered its identity call, 1 when it could not be
319
319
  * reached. Every other sub-question that fails is one line inside a successful
320
- * report a daemon with a broken channel is up, and saying otherwise would be
320
+ * report, a daemon with a broken channel is up, and saying otherwise would be
321
321
  * the kind of wrong that makes an operator distrust the whole page.
322
322
  */
323
323
  export async function runStatusCommand(input: RunStatusCommandInput): Promise<DaemonCommandResult> {
@@ -407,8 +407,8 @@ export interface RunUpdateCommandInput extends RemoteCommandDeps {
407
407
  * rejected, and whether a rollback is in force.
408
408
  *
409
409
  * --check is honest about a gap. The daemon runs the whole self-update loop
410
- * itself it checks hourly, swaps at an idle moment and keeps the outgoing
411
- * binary but the control plane publishes NO verb to trigger that check early:
410
+ * itself, it checks hourly, swaps at an idle moment and keeps the outgoing
411
+ * binary, but the control plane publishes NO verb to trigger that check early:
412
412
  * the operator contract this build was written against has no update method of
413
413
  * any kind (no `update.*`, no `admin.update`, nothing under `control.` that
414
414
  * checks). Rather than invent a verb this daemon does not answer, --check says
@@ -439,7 +439,7 @@ export async function runUpdateCommand(input: RunUpdateCommandInput): Promise<Da
439
439
  '',
440
440
  'update --check: this daemon publishes no verb to trigger an update check early.',
441
441
  ' It checks once an hour on its own and swaps only at an idle moment.',
442
- ' To make it check now, restart it it checks on the way up:',
442
+ ' To make it check now, restart it; it checks on the way up:',
443
443
  ' goodvibes-daemon restart-service',
444
444
  ]
445
445
  : [];
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // webui-command.ts `goodvibes-daemon webui enable|disable|status`.
2
+ // webui-command.ts, `goodvibes-daemon webui enable|disable|status`.
3
3
  //
4
4
  // WHAT THIS IS FOR
5
5
  //
@@ -8,7 +8,7 @@
8
8
  // serve such a directory: with `controlPlane.webui.serve` on, its own HTTP
9
9
  // router answers `/` from `controlPlane.webui.bundleDir` and falls back to
10
10
  // index.html for app routes. So installing the web UI is two config writes and
11
- // a directory on disk and this is the command that makes those writes, so the
11
+ // a directory on disk, and this is the command that makes those writes, so the
12
12
  // curl installer does not have to know the key names, the file format, or which
13
13
  // of the three settings tiers a daemon-owned key belongs in. Same reason
14
14
  // `provision-wake-model` exists: the installer runs the binary it just placed
@@ -17,12 +17,12 @@
17
17
  // WHERE IT IS SERVED, AND WHY THE URL SAYS WHAT IT SAYS
18
18
  //
19
19
  // The bundle is served BY THE CONTROL-PLANE LISTENER, same origin as the API
20
- // (that is the whole point a same-origin bundle makes the browser's
20
+ // (that is the whole point, a same-origin bundle makes the browser's
21
21
  // same-origin policy a non-issue and needs no CORS allowlist). So the URL that
22
22
  // opens the web UI is the control-plane origin: `http://<host>:<controlPlane.port>`,
23
23
  // not `web.port`. `web.port` is the surface's DECLARED endpoint, used for links
24
- // and for `tailscale serve`; nothing binds it. This command therefore reports
25
- // and, when it is still sitting on the shipped placeholder, writes
24
+ // and for `tailscale serve`; nothing binds it. This command therefore reports,
25
+ // and, when it is still sitting on the shipped placeholder, writes,
26
26
  // `web.publicBaseUrl` as the origin that actually answers, so the printed URL,
27
27
  // the pairing deep link and the running server all agree.
28
28
  //
@@ -41,7 +41,7 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
41
41
  import { resolveRuntimeEndpointBinding } from '@pellux/goodvibes-terminal-shell';
42
42
  import { probeStableHostInputs, stableUrlHostForBindHost, type StableHostInputs } from '@pellux/goodvibes-sdk/platform/pairing';
43
43
 
44
- /** The shipped `web.publicBaseUrl` placeholder a port nothing binds. */
44
+ /** The shipped `web.publicBaseUrl` placeholder, a port nothing binds. */
45
45
  const SHIPPED_PUBLIC_BASE_URL = 'http://127.0.0.1:3423';
46
46
 
47
47
  export interface WebuiCommandResult {
@@ -175,13 +175,13 @@ function postureLines(origin: string, loopback: boolean): string[] {
175
175
  ];
176
176
  }
177
177
  return [
178
- ` reachable from your network at ${origin} the control-plane listener is bound to all interfaces.`,
178
+ ` reachable from your network at ${origin}; the control-plane listener is bound to all interfaces.`,
179
179
  ' To take it back to this machine only: goodvibes-daemon webui enable --loopback',
180
180
  ];
181
181
  }
182
182
 
183
183
  /**
184
- * Run the command and report it. Never throws for an ordinary refusal a bad
184
+ * Run the command and report it. Never throws for an ordinary refusal, a bad
185
185
  * argument, a missing bundle, an unwritable settings file all come back as an
186
186
  * exit code and lines, because the caller is often an installer reading both.
187
187
  */
@@ -212,14 +212,14 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
212
212
  try {
213
213
  config.set('controlPlane.webui.serve', false);
214
214
  } catch (error) {
215
- return { exitCode: 1, lines: [`webui: could not write settings ${message(error)}`] };
215
+ return { exitCode: 1, lines: [`webui: could not write settings: ${message(error)}`] };
216
216
  }
217
217
  const kept = readString(config, 'controlPlane.webui.bundleDir');
218
218
  return {
219
219
  exitCode: 0,
220
220
  lines: [
221
221
  'web UI: no longer served by the daemon.',
222
- ...(kept ? [` the bundle is left on disk at ${kept} 'goodvibes-daemon webui enable' serves it again`] : []),
222
+ ...(kept ? [` the bundle is left on disk at ${kept}; 'goodvibes-daemon webui enable' serves it again`] : []),
223
223
  ' Restart the daemon for this to take effect on a running process.',
224
224
  ],
225
225
  };
@@ -242,7 +242,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
242
242
  return {
243
243
  exitCode: 1,
244
244
  lines: [
245
- `webui: refusing to serve ${bundleDir} ${problem}.`,
245
+ `webui: refusing to serve ${bundleDir}: ${problem}.`,
246
246
  ' Nothing was changed.',
247
247
  ],
248
248
  };
@@ -260,7 +260,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
260
260
  config.set('web.hostMode', 'local');
261
261
  }
262
262
  } catch (error) {
263
- return { exitCode: 1, lines: [`webui: could not write settings ${message(error)}`] };
263
+ return { exitCode: 1, lines: [`webui: could not write settings: ${message(error)}`] };
264
264
  }
265
265
 
266
266
  const serving = servingOrigin(config, probe);
@@ -284,7 +284,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
284
284
  }
285
285
 
286
286
  if (!serving.recognized) {
287
- lines.push(' note: controlPlane.hostMode is not one of local|network|custom the daemon cannot bind until that is corrected.');
287
+ lines.push(' note: controlPlane.hostMode is not one of local|network|custom; the daemon cannot bind until that is corrected.');
288
288
  }
289
289
  lines.push(...postureLines(serving.origin, serving.loopback));
290
290
  lines.push(' Restart the daemon for this to take effect on a running process.');
@@ -324,7 +324,7 @@ function renderStatus(
324
324
  } else {
325
325
  const absoluteDir = deps.absolute(directory);
326
326
  const problem = describeBundleProblem(absoluteDir, deps.directoryExists, deps.fileExists);
327
- lines.push(problem ? ` bundle (${source}): ${absoluteDir} UNUSABLE: ${problem}` : ` bundle (${source}): ${absoluteDir}`);
327
+ lines.push(problem ? ` bundle (${source}): ${absoluteDir} (UNUSABLE: ${problem})` : ` bundle (${source}): ${absoluteDir}`);
328
328
  }
329
329
  const currentPublic = readString(config, 'web.publicBaseUrl');
330
330
  if (currentPublic && currentPublic !== serving.origin) {
@@ -98,7 +98,7 @@ export async function runDaemonBootTasks(services: RuntimeServices): Promise<voi
98
98
 
99
99
  // Load the plugins this host can serve. The manager was constructed by the
100
100
  // graph and never initialised, so it could list a plugin directory and never
101
- // load anything out of it `enable` persisted a flag that turned nothing on.
101
+ // load anything out of it, `enable` persisted a flag that turned nothing on.
102
102
  // Both hosts read the same directories; each takes the registrations it can
103
103
  // serve (plugin-composition.ts). Best-effort like everything else here: a
104
104
  // plugin that will not load is not a reason for the daemon not to start.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * browser-checkout-seam-holder.ts, holding a seam that arrives after its
3
+ * first reader is constructed.
4
+ *
5
+ * ── Why a holder, and not a constructor argument ──────────────────────────
6
+ *
7
+ * The SDK hands out a `BrowserCheckoutSeam` through `onBrowserCheckout`, a
8
+ * callback `composeDaemonBrowser` invokes once, synchronously, at the moment
9
+ * IT composes the browser. In this daemon that composition runs inside
10
+ * `attachWsOnlyGatewayVerbHandlers` (services.ts), which is called AFTER
11
+ * `createDaemonHandlerComposition` builds the payments handlers, because the
12
+ * ws-only verb groups need managers (disposal scope, workspace checkpoint
13
+ * manager, ...) that do not exist yet at the point payments is composed.
14
+ * Reordering the two would ripple across every other verb group that call
15
+ * builds, for one capability's benefit.
16
+ *
17
+ * So the payments composition cannot receive the seam as a constructor
18
+ * argument; it receives a GETTER, closed over this holder, and reads it at
19
+ * CALL time rather than at registration time. By the time any real invocation
20
+ * reaches `payments.checkout.begin`, the daemon has finished booting and
21
+ * `onBrowserCheckout` has already fired (or the composition is one where the
22
+ * browser was never buildable at all, `composeDaemonBrowser` returned null,
23
+ * homeDirectory absent, and the getter honestly keeps returning undefined
24
+ * forever, which the checkout handler already refuses on cleanly).
25
+ */
26
+ import type { BrowserCheckoutSeam } from '@pellux/goodvibes-sdk/platform/control-plane';
27
+
28
+ export interface BrowserCheckoutSeamHolder {
29
+ readonly get: () => BrowserCheckoutSeam | undefined;
30
+ readonly set: (seam: BrowserCheckoutSeam) => void;
31
+ /**
32
+ * Forgets the held seam. Call this from the browser's own disposal path
33
+ * (services.ts, registered on the SAME disposal scope the browser sessions
34
+ * teardown runs on), so a daemon that has started shutting its browser down
35
+ * cannot hand a checkout call a seam whose `driverFor`/`cardFieldGuard` point
36
+ * at an engine that is being (or has been) torn down. After this, `get()`
37
+ * returns `undefined` again, and `payments.checkout.begin`/`.fillCard` fall
38
+ * back to their ordinary "checkout is not available right now" 409 refusal,
39
+ * see checkout-handlers.ts, rather than reaching into a disposed engine.
40
+ */
41
+ readonly clear: () => void;
42
+ }
43
+
44
+ export function createBrowserCheckoutSeamHolder(): BrowserCheckoutSeamHolder {
45
+ let seam: BrowserCheckoutSeam | undefined;
46
+ return {
47
+ get: () => seam,
48
+ set: (received) => {
49
+ seam = received;
50
+ },
51
+ clear: () => {
52
+ seam = undefined;
53
+ },
54
+ };
55
+ }