@pellux/goodvibes-daemon 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/bin/goodvibes-daemon +100 -0
  5. package/bin/launcher-support.js +226 -0
  6. package/package.json +96 -0
  7. package/scripts/check-bun.sh +20 -0
  8. package/scripts/postinstall.js +244 -0
  9. package/src/cli/command-catalog.ts +828 -0
  10. package/src/cli/completion.ts +299 -0
  11. package/src/cli/help.ts +167 -0
  12. package/src/cli/index.ts +21 -0
  13. package/src/cli/parser.ts +55 -0
  14. package/src/cli/surface-catalog.ts +26 -0
  15. package/src/cli/types.ts +63 -0
  16. package/src/cluster/daemon-ws-call.ts +235 -0
  17. package/src/cluster/raw-reply-route.ts +111 -0
  18. package/src/config/checkpoint-settings.ts +113 -0
  19. package/src/config/run-daemon-config-migration.ts +47 -0
  20. package/src/config/secret-config.ts +175 -0
  21. package/src/config/secrets.ts +71 -0
  22. package/src/config/surface.ts +24 -0
  23. package/src/core/pairing-banner.ts +82 -0
  24. package/src/daemon/cli.ts +878 -0
  25. package/src/daemon/config-command.ts +281 -0
  26. package/src/daemon/handlers/context.ts +29 -0
  27. package/src/daemon/handlers/contracts.ts +43 -0
  28. package/src/daemon/handlers/credentials.ts +139 -0
  29. package/src/daemon/handlers/drafts/draft-store.ts +427 -0
  30. package/src/daemon/handlers/drafts/index.ts +17 -0
  31. package/src/daemon/handlers/drafts/register.ts +331 -0
  32. package/src/daemon/handlers/errors.ts +18 -0
  33. package/src/daemon/handlers/inbox/aggregator.ts +375 -0
  34. package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
  35. package/src/daemon/handlers/inbox/index.ts +221 -0
  36. package/src/daemon/handlers/inbox/mapping.ts +192 -0
  37. package/src/daemon/handlers/inbox/poller.ts +239 -0
  38. package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
  39. package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
  40. package/src/daemon/handlers/inbox/providers/email.ts +176 -0
  41. package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
  42. package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
  43. package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
  44. package/src/daemon/handlers/index.ts +117 -0
  45. package/src/daemon/handlers/register.ts +180 -0
  46. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
  47. package/src/daemon/handlers/remote/backends/docker.ts +79 -0
  48. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  49. package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
  51. package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
  52. package/src/daemon/handlers/remote/backends/types.ts +97 -0
  53. package/src/daemon/handlers/remote/dispatcher.ts +181 -0
  54. package/src/daemon/handlers/remote/index.ts +120 -0
  55. package/src/daemon/handlers/remote/peer-registry.ts +357 -0
  56. package/src/daemon/handlers/remote/service.ts +191 -0
  57. package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
  58. package/src/daemon/handlers/routing/index.ts +261 -0
  59. package/src/daemon/handlers/routing/route-store.ts +319 -0
  60. package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
  61. package/src/daemon/handlers/sqlite-store.ts +303 -0
  62. package/src/daemon/handlers/triage/index.ts +57 -0
  63. package/src/daemon/handlers/triage/integration.ts +213 -0
  64. package/src/daemon/handlers/triage/pipeline.ts +274 -0
  65. package/src/daemon/handlers/triage/scorer.ts +287 -0
  66. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  67. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  68. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  69. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  70. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  71. package/src/daemon/handlers/triage/types.ts +50 -0
  72. package/src/daemon/lifecycle.ts +41 -0
  73. package/src/daemon/local-daemon-state.ts +233 -0
  74. package/src/daemon/pair-command.ts +301 -0
  75. package/src/daemon/provision-wake-model.ts +81 -0
  76. package/src/daemon/send/channels.ts +200 -0
  77. package/src/daemon/send/command.ts +333 -0
  78. package/src/daemon/send/composition.ts +100 -0
  79. package/src/daemon/send/failure-text.ts +93 -0
  80. package/src/daemon/send/inert-text.ts +225 -0
  81. package/src/daemon/send/stdin.ts +24 -0
  82. package/src/daemon/service-commands.ts +530 -0
  83. package/src/daemon/sessions-command.ts +209 -0
  84. package/src/daemon/status-command.ts +481 -0
  85. package/src/daemon/webui-command.ts +339 -0
  86. package/src/runtime/boot-tasks.ts +110 -0
  87. package/src/runtime/cluster-composition.ts +124 -0
  88. package/src/runtime/cluster-group-composition.ts +284 -0
  89. package/src/runtime/conversation-rewind-port.ts +171 -0
  90. package/src/runtime/credential-composition.ts +54 -0
  91. package/src/runtime/daemon-handler-composition.ts +76 -0
  92. package/src/runtime/device-posture-composition.ts +115 -0
  93. package/src/runtime/disposal-wiring.ts +101 -0
  94. package/src/runtime/fleet-needs-input-push.ts +61 -0
  95. package/src/runtime/fleet-services.ts +41 -0
  96. package/src/runtime/hosted-session-composition.ts +128 -0
  97. package/src/runtime/index.ts +100 -0
  98. package/src/runtime/knowledge-services.ts +101 -0
  99. package/src/runtime/legacy-daemon-migration.ts +605 -0
  100. package/src/runtime/legacy-daemon-reconcile.ts +448 -0
  101. package/src/runtime/mail-composition.ts +65 -0
  102. package/src/runtime/notification-dispatch.ts +86 -0
  103. package/src/runtime/plugin-composition.ts +111 -0
  104. package/src/runtime/runtime-services-types.ts +268 -0
  105. package/src/runtime/services.ts +756 -0
  106. package/src/runtime/trigger-services.ts +62 -0
  107. package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
  108. package/src/runtime/trust/trust-gated-approvals.ts +169 -0
  109. package/src/runtime/update-check.ts +61 -0
  110. package/src/runtime/workspace-checkpointing.ts +116 -0
  111. package/src/testing/daemon-fixture.ts +276 -0
  112. package/src/testing/hosted-session-failures.ts +92 -0
  113. package/src/version.ts +26 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * completion.ts — shell completion generated from the command catalog.
3
+ *
4
+ * The scripts are DERIVED, never hand-maintained: a command added to
5
+ * `./command-catalog.ts` is completable the moment it exists, and one removed
6
+ * stops being offered. A hand-written completion script is a second vocabulary,
7
+ * and a second vocabulary drifts.
8
+ *
9
+ * What each script completes:
10
+ * - the command word (names and aliases, in catalog order)
11
+ * - a command's own sub-words (`sessions list`, `config get`, `webui enable`)
12
+ * - a command's flags plus the global ones, once a command word is present
13
+ *
14
+ * Values are deliberately NOT completed. A settings key, a session id or a host
15
+ * name would each have to be fetched from a running daemon, and a completion
16
+ * that hangs while a socket times out is worse than one that offers nothing.
17
+ */
18
+ import {
19
+ DAEMON_COMMANDS,
20
+ GLOBAL_FLAGS,
21
+ daemonCommandSpec,
22
+ flagsForCommand,
23
+ type DaemonCommand,
24
+ } from './command-catalog.ts';
25
+
26
+ export const COMPLETION_SHELLS = ['bash', 'zsh', 'fish'] as const;
27
+ export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
28
+
29
+ export function isCompletionShell(value: string | undefined): value is CompletionShell {
30
+ return typeof value === 'string' && (COMPLETION_SHELLS as readonly string[]).includes(value);
31
+ }
32
+
33
+ /** Every word that selects a command, catalog order, names before aliases. */
34
+ export function completionCommandWords(): readonly string[] {
35
+ return DAEMON_COMMANDS.flatMap((spec) => [spec.name, ...spec.aliases]);
36
+ }
37
+
38
+ /** Long and short flag tokens a command accepts, its own plus the global ones. */
39
+ export function completionFlagsFor(command: DaemonCommand): readonly string[] {
40
+ return flagsForCommand(command).flatMap((flag) => flag.tokens);
41
+ }
42
+
43
+ /** A shell-safe identifier fragment (`goodvibes-daemon` -> `goodvibes_daemon`). */
44
+ function shellIdent(value: string): string {
45
+ return value.replace(/[^A-Za-z0-9_]/g, '_');
46
+ }
47
+
48
+ function bashScript(binary: string): string {
49
+ const fn = `_${shellIdent(binary)}_complete`;
50
+ const commandWords = completionCommandWords().join(' ');
51
+ const globalFlags = GLOBAL_FLAGS.flatMap((flag) => flag.tokens).join(' ');
52
+
53
+ const caseArms = DAEMON_COMMANDS.map((spec) => {
54
+ const words = [spec.name, ...spec.aliases].join('|');
55
+ const flags = completionFlagsFor(spec.name).join(' ');
56
+ const subs = spec.subcommands.join(' ');
57
+ return [
58
+ ` ${words})`,
59
+ ` __gvd_flags="${flags}"`,
60
+ ` __gvd_subs="${subs}"`,
61
+ ' ;;',
62
+ ].join('\n');
63
+ }).join('\n');
64
+
65
+ return [
66
+ `# bash completion for ${binary} — generated from its command catalog.`,
67
+ `# Install: ${binary} completion bash > ~/.local/share/bash-completion/completions/${binary}`,
68
+ '',
69
+ `${fn}() {`,
70
+ ' local cur prev words cword',
71
+ ' cur="${COMP_WORDS[COMP_CWORD]}"',
72
+ ' prev="${COMP_WORDS[COMP_CWORD-1]}"',
73
+ '',
74
+ ` local __gvd_commands="${commandWords}"`,
75
+ ` local __gvd_global="${globalFlags}"`,
76
+ ' local __gvd_flags=""',
77
+ ' local __gvd_subs=""',
78
+ ' local __gvd_command=""',
79
+ '',
80
+ ' # The first word that is not an option and not an option value is the command.',
81
+ ' local i',
82
+ ' for (( i=1; i < COMP_CWORD; i++ )); do',
83
+ ' local w="${COMP_WORDS[i]}"',
84
+ ' case "$w" in',
85
+ ' -*) continue ;;',
86
+ ' *)',
87
+ ' case " $__gvd_commands " in',
88
+ ' *" $w "*) __gvd_command="$w"; break ;;',
89
+ ' esac',
90
+ ' ;;',
91
+ ' esac',
92
+ ' done',
93
+ '',
94
+ ' if [[ -z "$__gvd_command" ]]; then',
95
+ ' if [[ "$cur" == -* ]]; then',
96
+ ' COMPREPLY=( $(compgen -W "$__gvd_global" -- "$cur") )',
97
+ ' else',
98
+ ' COMPREPLY=( $(compgen -W "$__gvd_commands" -- "$cur") )',
99
+ ' fi',
100
+ ' return 0',
101
+ ' fi',
102
+ '',
103
+ ' case "$__gvd_command" in',
104
+ caseArms,
105
+ ' esac',
106
+ '',
107
+ ' if [[ "$cur" == -* ]]; then',
108
+ ' COMPREPLY=( $(compgen -W "$__gvd_flags" -- "$cur") )',
109
+ ' else',
110
+ ' COMPREPLY=( $(compgen -W "$__gvd_subs" -- "$cur") )',
111
+ ' fi',
112
+ ' return 0',
113
+ '}',
114
+ '',
115
+ `complete -F ${fn} ${binary}`,
116
+ '',
117
+ ].join('\n');
118
+ }
119
+
120
+ function zshDescribe(value: string): string {
121
+ // Colons separate a zsh completion candidate from its description.
122
+ return value.replace(/:/g, '\\:').replace(/'/g, "'\\''");
123
+ }
124
+
125
+ function zshScript(binary: string): string {
126
+ const commandLines = DAEMON_COMMANDS
127
+ .map((spec) => ` '${zshDescribe(spec.name)}:${zshDescribe(spec.summary)}'`)
128
+ .join('\n');
129
+
130
+ const caseArms = DAEMON_COMMANDS.map((spec) => {
131
+ const words = [spec.name, ...spec.aliases].join('|');
132
+ const flags = completionFlagsFor(spec.name)
133
+ .map((token) => `'${zshDescribe(token)}'`)
134
+ .join(' ');
135
+ const subs = spec.subcommands.map((sub) => `'${zshDescribe(sub)}'`).join(' ');
136
+ return [
137
+ ` ${words})`,
138
+ ` __gvd_flags=(${flags})`,
139
+ ` __gvd_subs=(${subs})`,
140
+ ' ;;',
141
+ ].join('\n');
142
+ }).join('\n');
143
+
144
+ const fn = `_${shellIdent(binary)}`;
145
+ return [
146
+ `#compdef ${binary}`,
147
+ `# zsh completion for ${binary} — generated from its command catalog.`,
148
+ `# Install: ${binary} completion zsh > ~/.zfunc/_${binary} (with ~/.zfunc on $fpath)`,
149
+ '',
150
+ `${fn}() {`,
151
+ ' local -a __gvd_commands __gvd_flags __gvd_subs',
152
+ ' __gvd_commands=(',
153
+ commandLines,
154
+ ' )',
155
+ '',
156
+ ' local __gvd_command="" w',
157
+ ' for w in ${words[2,CURRENT-1]}; do',
158
+ ' case $w in',
159
+ ' -*) continue ;;',
160
+ ' *) __gvd_command=$w; break ;;',
161
+ ' esac',
162
+ ' done',
163
+ '',
164
+ ' if [[ -z $__gvd_command ]]; then',
165
+ ' _describe -t commands "command" __gvd_commands',
166
+ ' return',
167
+ ' fi',
168
+ '',
169
+ ' case $__gvd_command in',
170
+ caseArms,
171
+ ' esac',
172
+ '',
173
+ ' if [[ ${words[CURRENT]} == -* ]]; then',
174
+ ' compadd -a __gvd_flags',
175
+ ' else',
176
+ ' compadd -a __gvd_subs',
177
+ ' fi',
178
+ '}',
179
+ '',
180
+ `${fn} "$@"`,
181
+ '',
182
+ ].join('\n');
183
+ }
184
+
185
+ function fishEscape(value: string): string {
186
+ return value.replace(/'/g, "\\'");
187
+ }
188
+
189
+ function fishScript(binary: string): string {
190
+ const guard = `__${shellIdent(binary)}_no_command`;
191
+ const lines: string[] = [
192
+ `# fish completion for ${binary} — generated from its command catalog.`,
193
+ `# Install: ${binary} completion fish > ~/.config/fish/completions/${binary}.fish`,
194
+ '',
195
+ `function ${guard}`,
196
+ ' set -l tokens (commandline -opc)',
197
+ ' set -e tokens[1]',
198
+ ' for token in $tokens',
199
+ ' switch $token',
200
+ " case '-*'",
201
+ ' continue',
202
+ " case '*'",
203
+ ' return 1',
204
+ ' end',
205
+ ' end',
206
+ ' return 0',
207
+ 'end',
208
+ '',
209
+ ];
210
+
211
+ for (const spec of DAEMON_COMMANDS) {
212
+ lines.push(
213
+ `complete -c ${binary} -n '${guard}' `
214
+ + `-a '${fishEscape(spec.name)}' -d '${fishEscape(spec.summary)}'`,
215
+ );
216
+ }
217
+ lines.push('');
218
+
219
+ for (const spec of DAEMON_COMMANDS) {
220
+ for (const sub of spec.subcommands) {
221
+ lines.push(`complete -c ${binary} -n '__fish_seen_subcommand_from ${spec.name}' -a '${fishEscape(sub)}'`);
222
+ }
223
+ for (const flag of daemonCommandSpec(spec.name).flags) {
224
+ for (const token of flag.tokens) {
225
+ const option = token.startsWith('--') ? `-l ${token.slice(2)}` : `-s ${token.slice(1)}`;
226
+ const takesValue = flag.kind === 'boolean' ? '' : ' -r';
227
+ lines.push(
228
+ `complete -c ${binary} -n '__fish_seen_subcommand_from ${spec.name}' `
229
+ + `${option}${takesValue} -d '${fishEscape(flag.summary)}'`,
230
+ );
231
+ }
232
+ }
233
+ }
234
+
235
+ lines.push('');
236
+ for (const flag of GLOBAL_FLAGS) {
237
+ for (const token of flag.tokens) {
238
+ const option = token.startsWith('--') ? `-l ${token.slice(2)}` : `-s ${token.slice(1)}`;
239
+ const takesValue = flag.kind === 'boolean' ? '' : ' -r';
240
+ lines.push(`complete -c ${binary} ${option}${takesValue} -d '${fishEscape(flag.summary)}'`);
241
+ }
242
+ }
243
+ lines.push('');
244
+ return lines.join('\n');
245
+ }
246
+
247
+ export function renderCompletionScript(shell: CompletionShell, binary = 'goodvibes-daemon'): string {
248
+ if (shell === 'bash') return bashScript(binary);
249
+ if (shell === 'zsh') return zshScript(binary);
250
+ return fishScript(binary);
251
+ }
252
+
253
+ export interface CompletionCommandResult {
254
+ readonly exitCode: number;
255
+ readonly lines: readonly string[];
256
+ }
257
+
258
+ /**
259
+ * `goodvibes-daemon completion <shell>`.
260
+ *
261
+ * A missing or unrecognized shell is a usage refusal (exit 2) naming the ones
262
+ * that exist, rather than a default guess: writing a bash script into a zsh
263
+ * fpath produces a completion that silently never fires.
264
+ */
265
+ export function runCompletionCommand(
266
+ argv: readonly string[],
267
+ binary = 'goodvibes-daemon',
268
+ ): CompletionCommandResult {
269
+ const positional = argv.filter((token) => !token.startsWith('-'));
270
+ const shell = positional[0];
271
+ if (shell === undefined) {
272
+ return {
273
+ exitCode: 2,
274
+ lines: [
275
+ 'completion: name the shell.',
276
+ ` ${binary} completion ${COMPLETION_SHELLS.join('|')}`,
277
+ ],
278
+ };
279
+ }
280
+ if (!isCompletionShell(shell)) {
281
+ return {
282
+ exitCode: 2,
283
+ lines: [
284
+ `completion: '${shell}' is not a shell this generates for.`,
285
+ ` ${binary} completion ${COMPLETION_SHELLS.join('|')}`,
286
+ ],
287
+ };
288
+ }
289
+ if (positional.length > 1) {
290
+ return {
291
+ exitCode: 2,
292
+ lines: [
293
+ `completion: '${positional[1]}' is one argument too many.`,
294
+ ` ${binary} completion ${COMPLETION_SHELLS.join('|')}`,
295
+ ],
296
+ };
297
+ }
298
+ return { exitCode: 0, lines: [renderCompletionScript(shell, binary)] };
299
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { VERSION } from '../version.ts';
5
+ import {
6
+ DAEMON_COMMANDS,
7
+ GLOBAL_FLAGS,
8
+ daemonCommandSpec,
9
+ resolveDaemonCommand,
10
+ type DaemonCommandFlagSpec,
11
+ } from './command-catalog.ts';
12
+
13
+ function readJsonVersion(path: string): string | null {
14
+ try {
15
+ if (!existsSync(path)) return null;
16
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as { name?: unknown; version?: unknown };
17
+ // Only trust OUR package.json — a compiled single-file binary can resolve
18
+ // this path to a different package.json (a bundled dependency's) that
19
+ // reports a placeholder like "0.0.0". Fall through to the baked VERSION in
20
+ // that case rather than rendering a stray version in `--version`/banners.
21
+ if (parsed.name !== 'goodvibes-daemon') return null;
22
+ return typeof parsed.version === 'string' && parsed.version.length > 0 ? parsed.version : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function getPackageVersion(): string {
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+ return readJsonVersion(join(here, '..', '..', 'package.json'))
31
+ ?? VERSION;
32
+ }
33
+
34
+ export function renderGoodVibesVersion(binary = 'goodvibes-daemon'): string {
35
+ return `${binary} ${getPackageVersion()}`;
36
+ }
37
+
38
+ /**
39
+ * Honest one-line startup identity for the daemon binary, emitted right as it
40
+ * begins serving — including on a bare (no-arg) systemd launch. It states the
41
+ * RESOLVED version (never a placeholder), the home/host/port it actually bound,
42
+ * and points at the real service-setup command. This replaces the field
43
+ * behavior where a bare launch showed a wrong "v0.0.0" banner and gave an
44
+ * operator nothing to act on. `version` is passed in (never read from the live
45
+ * build here) so callers/tests can pin a sentinel and never compare the live
46
+ * VERSION.
47
+ */
48
+ export function renderDaemonStartupBanner(
49
+ version: string,
50
+ binding: { readonly homeDir: string; readonly host: string; readonly port: number },
51
+ binary = 'goodvibes-daemon',
52
+ ): string {
53
+ return (
54
+ `${binary} ${version} starting — ` +
55
+ `home=${binding.homeDir} host=${binding.host} port=${binding.port} ` +
56
+ `(manage as a service: ${binary} install-service)`
57
+ );
58
+ }
59
+
60
+ /**
61
+ * What the host actually uses to keep the daemon running, named per platform.
62
+ *
63
+ * The help said "systemd user service" on every platform, including macOS,
64
+ * where `install-service` writes a launchd agent and nothing named systemd
65
+ * exists. Taking the platform as an argument keeps that testable without
66
+ * stubbing `process`.
67
+ */
68
+ export function serviceKindForPlatform(platform: NodeJS.Platform = process.platform): string {
69
+ if (platform === 'darwin') return 'launchd user agent';
70
+ if (platform === 'win32') return 'Scheduled Task';
71
+ return 'systemd user service';
72
+ }
73
+
74
+ const COLUMN = 32;
75
+
76
+ function pad(left: string): string {
77
+ return left.length >= COLUMN ? `${left}\n${' '.repeat(COLUMN)}` : left.padEnd(COLUMN);
78
+ }
79
+
80
+ /** `-y, --yes` / ` --json` / `-m, --model <registryKey>` */
81
+ function renderFlagLine(flag: DaemonCommandFlagSpec): string {
82
+ const shorts = flag.tokens.filter((token) => !token.startsWith('--'));
83
+ const longs = flag.tokens.filter((token) => token.startsWith('--'));
84
+ const value = flag.valueName ? ` <${flag.valueName}>` : '';
85
+ const left = shorts.length > 0
86
+ ? ` ${shorts.join(', ')}, ${longs.join(', ')}${value}`
87
+ : ` ${longs.join(', ')}${value}`;
88
+ return `${pad(left)}${flag.summary}`;
89
+ }
90
+
91
+ /**
92
+ * The top-level help: what the binary is, what it does, what it accepts, and
93
+ * what its exit codes mean. Generated from the catalog, so a command that
94
+ * exists is listed and a command that is listed exists.
95
+ */
96
+ export function renderGoodVibesDaemonHelp(
97
+ binary = 'goodvibes-daemon',
98
+ platform: NodeJS.Platform = process.platform,
99
+ ): string {
100
+ const commands = DAEMON_COMMANDS
101
+ .filter((spec) => spec.name !== 'serve')
102
+ .map((spec) => `${pad(` ${spec.name}`)}${spec.summary}`);
103
+
104
+ return [
105
+ `Usage: ${binary} [COMMAND] [OPTIONS]`,
106
+ '',
107
+ 'The GoodVibes daemon: the one long-running host for the control plane, the',
108
+ 'channels, cluster membership, scheduled work, the knowledge and memory stores,',
109
+ 'and the verb families every GoodVibes client calls.',
110
+ '',
111
+ `Run with no command it starts serving in the foreground. Run \`${binary}`,
112
+ `install-service\` to have it come back after a reboot as a ${serviceKindForPlatform(platform)}.`,
113
+ '',
114
+ 'Commands:',
115
+ ...commands,
116
+ '',
117
+ `Run \`${binary} help <command>\` for a command's own arguments and flags.`,
118
+ '',
119
+ 'Global options (accepted by every command):',
120
+ ...GLOBAL_FLAGS.map(renderFlagLine),
121
+ '',
122
+ 'Serving options (a bare invocation, or `serve`):',
123
+ ...daemonCommandSpec('serve').flags.map(renderFlagLine),
124
+ '',
125
+ 'Exit codes:',
126
+ `${pad(' 0')}the command did what it says`,
127
+ `${pad(' 1')}it ran and failed — the reason is printed`,
128
+ `${pad(' 2')}the command line was wrong: an unknown command, an unknown flag,`,
129
+ `${pad(' ')}a flag this command does not take, or a missing value`,
130
+ `${pad(' 3')}service-status only: installed, but not running`,
131
+ `${pad(' 4')}service-status only: not installed`,
132
+ ].join('\n');
133
+ }
134
+
135
+ /**
136
+ * `help <command>` — one command's usage, its own flags, and what it does.
137
+ *
138
+ * Returns null when the word names no command, so the caller can refuse with
139
+ * the same "Unknown command" message the parser produces rather than printing
140
+ * a help page for something that does not exist.
141
+ */
142
+ export function renderDaemonCommandHelp(
143
+ commandWord: string,
144
+ binary = 'goodvibes-daemon',
145
+ platform: NodeJS.Platform = process.platform,
146
+ ): string | null {
147
+ const command = resolveDaemonCommand(commandWord);
148
+ if (command === undefined) return null;
149
+ const spec = daemonCommandSpec(command);
150
+ const usage = spec.usage.replace(/^goodvibes-daemon/, binary);
151
+
152
+ const lines = [`Usage: ${usage}`, '', ...spec.detail];
153
+ if (spec.flags.length > 0) {
154
+ lines.push('', 'Options:', ...spec.flags.map(renderFlagLine));
155
+ }
156
+ if (spec.passthrough) {
157
+ lines.push(
158
+ '',
159
+ `This command has its own flags; run \`${binary} ${spec.name}\` with none to see them.`,
160
+ );
161
+ }
162
+ lines.push('', 'Global options:', ...GLOBAL_FLAGS.map(renderFlagLine));
163
+ if (spec.name.endsWith('-service')) {
164
+ lines.push('', `On this host that means a ${serviceKindForPlatform(platform)}.`);
165
+ }
166
+ return lines.join('\n');
167
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * CLI barrel for the daemon product.
3
+ *
4
+ * Two halves, deliberately separable:
5
+ * - `command-catalog.ts` is WHAT this binary understands — every command,
6
+ * alias, flag and help string, as data.
7
+ * - `parser.ts`, `help.ts` and `completion.ts` are the ENGINE that reads a
8
+ * catalog and produces a parse, a help page or a completion script. None of
9
+ * them names a daemon command.
10
+ *
11
+ * The argument surface a `goodvibes` front-end shares — the parse engine's
12
+ * catalog contract, redaction, config overrides, settings-value reading and
13
+ * endpoint resolution — is @pellux/goodvibes-terminal-shell's, imported at the
14
+ * point of use. What stays here is what only this binary has: its own command
15
+ * vocabulary, and the help and completion built on it.
16
+ */
17
+ export * from './types.ts';
18
+ export * from './command-catalog.ts';
19
+ export * from './parser.ts';
20
+ export * from './help.ts';
21
+ export * from './completion.ts';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * parser.ts — this binary's command line, read by the shared argument engine.
3
+ *
4
+ * The engine is `parseWithCatalog` in @pellux/goodvibes-terminal-shell: tokens,
5
+ * values, arity, `--`, refusals, and no knowledge of any product's commands.
6
+ * Everything daemon-shaped is in ./command-catalog.ts, and this file is the two
7
+ * lines that put them together.
8
+ *
9
+ * THE TWO RULES THE CATALOG ASKS THE ENGINE TO ENFORCE
10
+ *
11
+ * 1. Serving happens on a bare invocation or on `serve`, and on nothing else.
12
+ * Previously an unmatched word became a positional and the process fell
13
+ * through to "start a daemon in the foreground", so `goodvibes-daemon
14
+ * doctor`, `goodvibes-daemon sessions` and `goodvibes-daemon install-servce`
15
+ * all silently served. `unmatchedFirstToken: 'reject'` is what ends that.
16
+ *
17
+ * 2. Every refusal is a refusal. An unrecognized command, a flag that belongs
18
+ * to another surface, a flag this command does not take, a missing value —
19
+ * each produces an error line, and the caller exits 2 with the help. Nothing
20
+ * is accepted-and-ignored.
21
+ */
22
+ import {
23
+ findCatalogFlagArityConflicts as findConflictsIn,
24
+ parseWithCatalog,
25
+ } from '@pellux/goodvibes-terminal-shell';
26
+ import { DAEMON_CLI_CATALOG } from './command-catalog.ts';
27
+ import type { DaemonCliParseResult } from './types.ts';
28
+
29
+ /**
30
+ * Parse a daemon command line.
31
+ *
32
+ * Never throws: every problem comes back as a line in `errors`, so the caller
33
+ * decides how to report it (this binary writes them to the descriptor a service
34
+ * journal is attached to, then exits 2).
35
+ */
36
+ export function parseDaemonCli(
37
+ argv: readonly string[],
38
+ binary = 'goodvibes-daemon',
39
+ ): DaemonCliParseResult {
40
+ return parseWithCatalog(argv, DAEMON_CLI_CATALOG, binary);
41
+ }
42
+
43
+ /**
44
+ * This catalog's own consistency, checked rather than assumed.
45
+ *
46
+ * The engine's search for the command word runs before the command is known
47
+ * and therefore reads arity from one table shared across commands. That is only
48
+ * honest while no token means "boolean" under one command and "takes a value"
49
+ * under another. Exported bound to this catalog so a unit test asserts it on
50
+ * the real vocabulary; a violation comes back as a list of problems, never
51
+ * thrown, so the test can name them.
52
+ */
53
+ export function findCatalogFlagArityConflicts(): readonly string[] {
54
+ return findConflictsIn(DAEMON_CLI_CATALOG);
55
+ }
@@ -0,0 +1,26 @@
1
+ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
2
+
3
+ /**
4
+ * The channel surfaces the platform can speak on, with the settings keys each
5
+ * one needs configured.
6
+ *
7
+ * The daemon reads this to answer "which channels are actually usable" — the
8
+ * `send` subcommand lists them and refuses a channel whose keys are unset,
9
+ * rather than accepting the message and dropping it. A surface added here
10
+ * becomes visible to `send` with no further wiring.
11
+ */
12
+ export const SURFACE_CONFIGS = [
13
+ ['slack', 'Slack', ['surfaces.slack.signingSecret', 'surfaces.slack.botToken']],
14
+ ['discord', 'Discord', ['surfaces.discord.publicKey', 'surfaces.discord.botToken', 'surfaces.discord.applicationId']],
15
+ ['telegram', 'Telegram', ['surfaces.telegram.botToken']],
16
+ ['webhook', 'Webhook', ['surfaces.webhook.secret']],
17
+ ['ntfy', 'ntfy', ['surfaces.ntfy.baseUrl']],
18
+ ['googleChat', 'Google Chat', ['surfaces.googleChat.webhookUrl']],
19
+ ['signal', 'Signal', ['surfaces.signal.bridgeUrl', 'surfaces.signal.account']],
20
+ ['whatsapp', 'WhatsApp', ['surfaces.whatsapp.accessToken', 'surfaces.whatsapp.phoneNumberId']],
21
+ ['imessage', 'iMessage', ['surfaces.imessage.bridgeUrl', 'surfaces.imessage.account']],
22
+ ['msteams', 'Microsoft Teams', ['surfaces.msteams.appId', 'surfaces.msteams.appPassword']],
23
+ ['bluebubbles', 'BlueBubbles', ['surfaces.bluebubbles.serverUrl', 'surfaces.bluebubbles.password']],
24
+ ['mattermost', 'Mattermost', ['surfaces.mattermost.baseUrl', 'surfaces.mattermost.botToken']],
25
+ ['matrix', 'Matrix', ['surfaces.matrix.homeserverUrl', 'surfaces.matrix.accessToken', 'surfaces.matrix.userId']],
26
+ ] as const;
@@ -0,0 +1,63 @@
1
+ import type { DaemonCommand } from './command-catalog.ts';
2
+
3
+ /**
4
+ * Everything a parse can produce.
5
+ *
6
+ * One record for every command, rather than a discriminated union per command:
7
+ * the fields a command does not use are simply left at their empty value, and
8
+ * the catalog is what decides which flags could have set them. A command's
9
+ * dispatcher reads only the fields its own catalog entry declares.
10
+ *
11
+ * Fields describing starting or resuming a conversation — prompt, print,
12
+ * outputFormat, noAltScreen, open, continueLast, resume, session, fork,
13
+ * strict — are not here. This binary does not start or resume conversations,
14
+ * so those flags are parsed, stored, and read by nothing. See
15
+ * REJECTED_TERMINAL_FLAGS in ./command-catalog.ts for the refusal that
16
+ * replaced them.
17
+ */
18
+ export interface DaemonCliFlags {
19
+ readonly daemonHome: string | undefined;
20
+ readonly workingDir: string | undefined;
21
+ readonly help: boolean;
22
+ readonly version: boolean;
23
+ /** `--json`: print one JSON document instead of prose. */
24
+ readonly json: boolean;
25
+ /** `-y` / `--yes` / `--non-interactive`: consent to a destructive confirmation. */
26
+ readonly yes: boolean;
27
+ /** `update --check`: ask for an update check now. */
28
+ readonly check: boolean;
29
+ /** `sessions list --all`: include sessions that have already ended. */
30
+ readonly all: boolean;
31
+ readonly provider: string | undefined;
32
+ readonly model: string | undefined;
33
+ /** `serve` only: the address to BIND. */
34
+ readonly hostname: string | undefined;
35
+ /** `serve`: the port to bind. Remote commands: the port to CALL. */
36
+ readonly port: number | undefined;
37
+ /** Remote commands only: the machine to call. */
38
+ readonly host: string | undefined;
39
+ /** Remote commands only: the operator token to authenticate with. */
40
+ readonly token: string | undefined;
41
+ readonly configOverrides: readonly string[];
42
+ readonly enableFeatures: readonly string[];
43
+ readonly disableFeatures: readonly string[];
44
+ }
45
+
46
+ export interface DaemonCliParseResult {
47
+ readonly binary: string;
48
+ /** Always resolved. A bare invocation is `serve`; an unrecognized word is an error. */
49
+ readonly command: DaemonCommand;
50
+ /** The word the operator actually typed, when they typed one. */
51
+ readonly rawCommand: string | undefined;
52
+ /**
53
+ * Everything after the command word that is not a flag this parser owns. For
54
+ * a passthrough command (`send`, `cluster`, `webui`, `provision-wake-model`)
55
+ * it is every remaining token verbatim, flags included.
56
+ */
57
+ readonly commandArgs: readonly string[];
58
+ readonly flags: DaemonCliFlags;
59
+ /** Usage refusals. A non-empty list means exit 2 with these lines and the help. */
60
+ readonly errors: readonly string[];
61
+ /** Non-fatal notices. Printed, then the command runs. */
62
+ readonly warnings: readonly string[];
63
+ }