@dinoxx/dinox-cli 2.13.1 → 2.14.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 (58) hide show
  1. package/dist/cli.js +5 -2
  2. package/dist/commands/auth/index.d.ts +2 -1
  3. package/dist/commands/auth/index.js +18 -1
  4. package/dist/commands/doctor.d.ts +4 -1
  5. package/dist/commands/doctor.js +20 -1
  6. package/dist/commands/graph/index.js +2 -1
  7. package/dist/commands/notes/contentPatch.js +2 -1
  8. package/dist/commands/notes/export.js +3 -2
  9. package/dist/commands/runtime.d.ts +8 -0
  10. package/dist/commands/runtime.js +49 -1
  11. package/dist/commands/schema/globalOptions.d.ts +1 -0
  12. package/dist/commands/schema/globalOptions.js +1 -0
  13. package/dist/commands/schema/renderSkills.js +1 -0
  14. package/dist/commands/schema/spec.js +9 -1
  15. package/dist/commands/storage/repo/operations.js +3 -1
  16. package/dist/commands/sync.d.ts +2 -1
  17. package/dist/commands/sync.js +24 -5
  18. package/dist/daemon/client.js +32 -2
  19. package/dist/daemon/connectivity.d.ts +20 -0
  20. package/dist/daemon/connectivity.js +42 -0
  21. package/dist/daemon/ensure.js +48 -12
  22. package/dist/daemon/forward.d.ts +34 -0
  23. package/dist/daemon/forward.js +146 -0
  24. package/dist/daemon/fresh.d.ts +4 -0
  25. package/dist/daemon/fresh.js +10 -0
  26. package/dist/daemon/index.d.ts +11 -6
  27. package/dist/daemon/index.js +90 -25
  28. package/dist/daemon/launch.d.ts +13 -1
  29. package/dist/daemon/launch.js +31 -2
  30. package/dist/daemon/lifecycle.d.ts +3 -1
  31. package/dist/daemon/lifecycle.js +27 -3
  32. package/dist/daemon/pid.d.ts +8 -0
  33. package/dist/daemon/pid.js +20 -0
  34. package/dist/daemon/route.js +2 -1
  35. package/dist/daemon/server.d.ts +44 -0
  36. package/dist/daemon/server.js +175 -3
  37. package/dist/daemon/supervise.d.ts +6 -6
  38. package/dist/daemon/supervise.js +12 -10
  39. package/dist/daemon/supervisor.d.ts +21 -5
  40. package/dist/daemon/supervisor.js +125 -6
  41. package/dist/daemon/types.d.ts +16 -0
  42. package/dist/dinox.js +41 -64
  43. package/dist/gateway/mcpCommandTools.d.ts +9 -0
  44. package/dist/gateway/mcpCommandTools.js +40 -10
  45. package/dist/powersync/runtime.d.ts +2 -0
  46. package/dist/powersync/runtime.js +3 -1
  47. package/dist/requests/store.d.ts +10 -0
  48. package/dist/requests/store.js +190 -0
  49. package/dist/runCli.d.ts +2 -0
  50. package/dist/runCli.js +64 -0
  51. package/dist/utils/argValue.js +2 -1
  52. package/dist/utils/errorCodes.d.ts +20 -0
  53. package/dist/utils/errorCodes.js +5 -0
  54. package/dist/utils/output.d.ts +2 -0
  55. package/dist/utils/output.js +31 -7
  56. package/dist/utils/requestContext.d.ts +24 -0
  57. package/dist/utils/requestContext.js +13 -0
  58. package/package.json +2 -1
@@ -5,11 +5,11 @@
5
5
  * every gateway goes offline. Under launchd / systemd the daemon belongs to
6
6
  * the login session instead, starts at login, and is restarted after a crash.
7
7
  *
8
- * Restart policy is "on failure" only: a clean exit (`dino daemon stop`, a
9
- * version/account restart, exclusive DB access for `doctor`) leaves it
10
- * stopped until the CLI starts it again through the manager. launchd and
11
- * systemd restart the daemon themselves; on Windows, Task Scheduler only
12
- * starts `dino daemon supervise`, which restarts it (see supervise.ts).
8
+ * Restart policy is "always": launchd KeepAlive, systemd Restart=always, and
9
+ * on Windows `dino daemon supervise` (Task Scheduler only starts it; see
10
+ * supervise.ts) bring the daemon back after any exit, including a clean one
11
+ * for exclusive DB access or a crash. Only `dino daemon stop` / `uninstall`,
12
+ * which go through stop() / uninstall() here, leave it down.
13
13
  */
14
14
  export type DaemonServiceKind = 'launchd' | 'systemd' | 'schtasks';
15
15
  export type DaemonServiceSpec = {
@@ -33,25 +33,41 @@ export type DaemonService = {
33
33
  isInstalled(): boolean;
34
34
  /** True when the installed definition is exactly what this CLI would write. */
35
35
  isCurrent(spec: DaemonServiceSpec): boolean;
36
+ /** The program and arguments the installed definition runs, or null when there is none. */
37
+ installedCommand(): string[] | null;
36
38
  /** Writes the definition and (re)loads it, which starts the daemon. */
37
39
  install(spec: DaemonServiceSpec): Promise<void>;
38
40
  /** Starts the installed daemon when it is not running. */
39
41
  start(): Promise<void>;
42
+ /** Stops the daemon and keeps it stopped until start() (or the next login). */
43
+ stop(): Promise<void>;
44
+ /** Restarts it now, skipping restart throttling. Windows: the caller then stops it through the socket. */
45
+ restart(): Promise<void>;
40
46
  /** Stops the daemon and removes the definition; false when nothing was installed. */
41
47
  uninstall(): Promise<boolean>;
42
48
  };
43
49
  /** Set in the service definition so the daemon can report who supervises it. */
44
50
  export declare const DAEMON_SERVICE_ENV = "DINOX_DAEMON_SUPERVISOR";
51
+ /** Windows: file in the daemon directory telling the supervisor not to restart. */
52
+ export declare const SUPERVISOR_STOP_MARKER = "supervisor.stop";
45
53
  export declare const runServiceCommand: ServiceCommandRunner;
46
54
  /**
47
55
  * One daemon per data directory (it owns that directory's socket), so a
48
56
  * non-default `DINOX_DATA_DIR` gets its own service.
49
57
  */
50
58
  export declare function daemonServiceLabel(env?: NodeJS.ProcessEnv): string;
59
+ /** ProgramArguments of a plist written by renderLaunchdPlist. */
60
+ export declare function parseLaunchdProgramArguments(plist: string): string[] | null;
51
61
  export declare function renderLaunchdPlist(label: string, spec: DaemonServiceSpec): string;
62
+ /** The words of an ExecStart line written by renderSystemdUnit. */
63
+ export declare function parseSystemdExecStart(unit: string): string[] | null;
52
64
  export declare function renderSystemdUnit(spec: DaemonServiceSpec): string;
53
65
  /** Quotes one argument the way CommandLineToArgvW (and so Node) parses it back. */
54
66
  export declare function quoteWindowsArg(value: string): string;
67
+ /** Splits a command line the way the Windows C runtime does (the inverse of quoteWindowsArg). */
68
+ export declare function splitWindowsCommandLine(commandLine: string): string[];
69
+ /** The command line of a launcher written by renderWindowsLauncher. */
70
+ export declare function parseWindowsLauncherCommand(launcher: string): string[] | null;
55
71
  /**
56
72
  * Task Scheduler runs console programs in a visible window, so the task runs
57
73
  * this script with wscript, which sets the daemon environment and starts the
@@ -8,6 +8,8 @@ import { DinoxError } from '../utils/errors.js';
8
8
  /** Set in the service definition so the daemon can report who supervises it. */
9
9
  export const DAEMON_SERVICE_ENV = 'DINOX_DAEMON_SUPERVISOR';
10
10
  const DEFAULT_LABEL = 'com.dinox.cli.daemon';
11
+ /** Windows: file in the daemon directory telling the supervisor not to restart. */
12
+ export const SUPERVISOR_STOP_MARKER = 'supervisor.stop';
11
13
  const COMMAND_TIMEOUT_MS = 15_000;
12
14
  const LAUNCHD_BOOTSTRAP_ATTEMPTS = 5;
13
15
  const LAUNCHD_BOOTSTRAP_RETRY_MS = 300;
@@ -54,6 +56,21 @@ function xmlEscape(value) {
54
56
  .replace(/>/g, '&gt;')
55
57
  .replace(/"/g, '&quot;');
56
58
  }
59
+ function xmlUnescape(value) {
60
+ return value
61
+ .replace(/&lt;/g, '<')
62
+ .replace(/&gt;/g, '>')
63
+ .replace(/&quot;/g, '"')
64
+ .replace(/&apos;/g, "'")
65
+ .replace(/&amp;/g, '&');
66
+ }
67
+ /** ProgramArguments of a plist written by renderLaunchdPlist. */
68
+ export function parseLaunchdProgramArguments(plist) {
69
+ const array = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(plist);
70
+ if (!array)
71
+ return null;
72
+ return [...array[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((match) => xmlUnescape(match[1]));
73
+ }
57
74
  export function renderLaunchdPlist(label, spec) {
58
75
  const string = (value) => `<string>${xmlEscape(value)}</string>`;
59
76
  const args = [spec.program, ...spec.args].map((arg) => `\t\t${string(arg)}`).join('\n');
@@ -80,10 +97,7 @@ ${env}
80
97
  \t<key>RunAtLoad</key>
81
98
  \t<true/>
82
99
  \t<key>KeepAlive</key>
83
- \t<dict>
84
- \t\t<key>SuccessfulExit</key>
85
- \t\t<false/>
86
- \t</dict>
100
+ \t<true/>
87
101
  \t<key>ThrottleInterval</key>
88
102
  \t<integer>30</integer>
89
103
  \t<key>StandardOutPath</key>
@@ -117,6 +131,10 @@ class LaunchdService {
117
131
  isCurrent(spec) {
118
132
  return readDefinition(this.definitionPath) === this.render(spec);
119
133
  }
134
+ installedCommand() {
135
+ const plist = readDefinition(this.definitionPath);
136
+ return plist === null ? null : parseLaunchdProgramArguments(plist);
137
+ }
120
138
  async isLoaded() {
121
139
  return (await this.run('launchctl', ['print', this.target])).code === 0;
122
140
  }
@@ -147,6 +165,18 @@ class LaunchdService {
147
165
  if (result.code !== 0)
148
166
  throw failed(this.kind, 'kickstart', result);
149
167
  }
168
+ async stop() {
169
+ const result = await this.run('launchctl', ['bootout', this.target]);
170
+ if (result.code !== 0 && await this.isLoaded())
171
+ throw failed(this.kind, 'bootout', result);
172
+ }
173
+ async restart() {
174
+ if (!(await this.isLoaded()))
175
+ await this.bootstrap();
176
+ const result = await this.run('launchctl', ['kickstart', '-k', this.target]);
177
+ if (result.code !== 0)
178
+ throw failed(this.kind, 'kickstart', result);
179
+ }
150
180
  async uninstall() {
151
181
  const installed = this.isInstalled();
152
182
  if (await this.isLoaded()) {
@@ -170,6 +200,16 @@ function systemdQuote(value) {
170
200
  .replace(/\n/g, '\\n');
171
201
  return `"${escaped}"`;
172
202
  }
203
+ /** The words of an ExecStart line written by renderSystemdUnit. */
204
+ export function parseSystemdExecStart(unit) {
205
+ const line = /^ExecStart=(.*)$/m.exec(unit);
206
+ if (!line)
207
+ return null;
208
+ return [...line[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((match) => match[1]
209
+ .replace(/%%/g, '%')
210
+ .replace(/\$\$/g, '$')
211
+ .replace(/\\(.)/g, (_, char) => (char === 'n' ? '\n' : char)));
212
+ }
173
213
  export function renderSystemdUnit(spec) {
174
214
  const execStart = [spec.program, ...spec.args].map(systemdQuote).join(' ');
175
215
  const env = Object.keys(spec.env)
@@ -185,8 +225,8 @@ Type=simple
185
225
  ExecStart=${execStart}
186
226
  WorkingDirectory=${spec.workingDirectory.replace(/%/g, '%%')}
187
227
  ${env}
188
- Restart=on-failure
189
- RestartSec=30
228
+ Restart=always
229
+ RestartSec=5
190
230
  StandardOutput=append:${spec.logPath}
191
231
  StandardError=append:${spec.logPath}
192
232
 
@@ -215,6 +255,10 @@ class SystemdService {
215
255
  isCurrent(spec) {
216
256
  return readDefinition(this.definitionPath) === this.render(spec);
217
257
  }
258
+ installedCommand() {
259
+ const unit = readDefinition(this.definitionPath);
260
+ return unit === null ? null : parseSystemdExecStart(unit);
261
+ }
218
262
  async systemctl(step, ...args) {
219
263
  const result = await this.run('systemctl', ['--user', ...args]);
220
264
  if (result.code !== 0)
@@ -229,6 +273,12 @@ class SystemdService {
229
273
  async start() {
230
274
  await this.systemctl('start', 'start', this.unit);
231
275
  }
276
+ async stop() {
277
+ await this.systemctl('stop', 'stop', this.unit);
278
+ }
279
+ async restart() {
280
+ await this.systemctl('restart', 'restart', this.unit);
281
+ }
232
282
  async uninstall() {
233
283
  if (!this.isInstalled())
234
284
  return false;
@@ -260,6 +310,57 @@ export function quoteWindowsArg(value) {
260
310
  }
261
311
  return `${quoted}${'\\'.repeat(backslashes * 2)}"`;
262
312
  }
313
+ /** Splits a command line the way the Windows C runtime does (the inverse of quoteWindowsArg). */
314
+ export function splitWindowsCommandLine(commandLine) {
315
+ const args = [];
316
+ let current = '';
317
+ let inArg = false;
318
+ let quoted = false;
319
+ for (let index = 0; index < commandLine.length; index += 1) {
320
+ const char = commandLine[index];
321
+ if (char === '\\') {
322
+ let backslashes = 0;
323
+ while (commandLine[index] === '\\') {
324
+ backslashes += 1;
325
+ index += 1;
326
+ }
327
+ if (commandLine[index] === '"') {
328
+ current += '\\'.repeat(Math.floor(backslashes / 2));
329
+ if (backslashes % 2 === 1)
330
+ current += '"';
331
+ else
332
+ quoted = !quoted;
333
+ }
334
+ else {
335
+ current += '\\'.repeat(backslashes);
336
+ index -= 1;
337
+ }
338
+ inArg = true;
339
+ }
340
+ else if (char === '"') {
341
+ quoted = !quoted;
342
+ inArg = true;
343
+ }
344
+ else if (/\s/.test(char) && !quoted) {
345
+ if (inArg)
346
+ args.push(current);
347
+ current = '';
348
+ inArg = false;
349
+ }
350
+ else {
351
+ current += char;
352
+ inArg = true;
353
+ }
354
+ }
355
+ if (inArg)
356
+ args.push(current);
357
+ return args;
358
+ }
359
+ /** The command line of a launcher written by renderWindowsLauncher. */
360
+ export function parseWindowsLauncherCommand(launcher) {
361
+ const line = /^shell\.Run "((?:[^"]|"")*)", 0, False\r?$/m.exec(launcher);
362
+ return line ? splitWindowsCommandLine(line[1].replace(/""/g, '"')) : null;
363
+ }
263
364
  function vbsString(value) {
264
365
  return `"${value.replace(/"/g, '""').replace(/\r?\n/g, '" & vbLf & "')}"`;
265
366
  }
@@ -366,6 +467,10 @@ class WindowsTaskService {
366
467
  isCurrent(spec) {
367
468
  return readUtf16(this.definitionPath) === this.render(spec);
368
469
  }
470
+ installedCommand() {
471
+ const launcher = readUtf16(this.definitionPath);
472
+ return launcher === null ? null : parseWindowsLauncherCommand(launcher);
473
+ }
369
474
  async isRegistered() {
370
475
  return (await this.run('schtasks', ['/Query', '/TN', this.taskName])).code === 0;
371
476
  }
@@ -398,15 +503,29 @@ class WindowsTaskService {
398
503
  throw failed(this.kind, 'run', result);
399
504
  }
400
505
  async install(spec) {
506
+ this.clearStopMarker();
401
507
  writeUtf16(this.definitionPath, this.render(spec));
402
508
  await this.register();
403
509
  await this.runTask();
404
510
  }
405
511
  async start() {
512
+ this.clearStopMarker();
406
513
  if (!(await this.isRegistered()))
407
514
  await this.register();
408
515
  await this.runTask();
409
516
  }
517
+ async stop() {
518
+ mkdirSync(this.launcherDir, { recursive: true });
519
+ writeFileSync(path.join(this.launcherDir, SUPERVISOR_STOP_MARKER), '');
520
+ }
521
+ async restart() {
522
+ this.clearStopMarker();
523
+ }
524
+ clearStopMarker() {
525
+ const marker = path.join(this.launcherDir, SUPERVISOR_STOP_MARKER);
526
+ if (existsSync(marker))
527
+ unlinkSync(marker);
528
+ }
410
529
  async uninstall() {
411
530
  const installed = this.isInstalled();
412
531
  if (await this.isRegistered()) {
@@ -12,6 +12,11 @@ export type DaemonResponse = {
12
12
  };
13
13
  export type DaemonShutdownParams = {
14
14
  expectedInstanceId: string;
15
+ requester?: {
16
+ pid: number;
17
+ version: string;
18
+ command?: string;
19
+ };
15
20
  };
16
21
  export type DaemonShutdownResult = {
17
22
  stopping: true;
@@ -47,3 +52,14 @@ export type DaemonPingResult = {
47
52
  };
48
53
  /** Kept only for old scripts that pass --port; the daemon transport is socket-based. */
49
54
  export declare const DEFAULT_DAEMON_PORT = 45678;
55
+ export type CommandExecuteParams = {
56
+ argv: string[];
57
+ cwd: string;
58
+ clientVersion: string;
59
+ requestId?: string;
60
+ };
61
+ export type CommandExecuteResult = {
62
+ stdout: string;
63
+ stderr: string;
64
+ exitCode: number;
65
+ };
package/dist/dinox.js CHANGED
@@ -1,70 +1,47 @@
1
1
  #!/usr/bin/env node
2
- import { createCli } from './cli.js';
3
- import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
4
- import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, printHumanReadableError, setRequestedStructuredOutputFormat, } from './utils/output.js';
2
+ import { commandExecutionTarget, tryForwardToDaemon } from './daemon/forward.js';
3
+ import { getPackageVersion } from './utils/version.js';
4
+ import { requestIdFromArgv, runWithRequestId } from './requests/store.js';
5
+ import { isDinoxError } from './utils/errors.js';
5
6
  import { exitProcessAfterFlushing } from './utils/processExit.js';
6
- function normalizeArgv(argv) {
7
- // pnpm/npm script forwarding can prepend a standalone "--".
8
- if (argv[0] === '--') {
9
- return argv.slice(1);
7
+ import { localCommandExecution, requestContext } from './utils/requestContext.js';
8
+ const argv = process.argv.slice(2);
9
+ let fatalError;
10
+ try {
11
+ const versionOnly = argv.length === 1 && ['--version', '-V'].includes(argv[0]);
12
+ const forwarded = versionOnly
13
+ ? { stdout: `${getPackageVersion()}\n`, stderr: '', exitCode: 0 }
14
+ : await tryForwardToDaemon(argv);
15
+ if (forwarded) {
16
+ process.stdout.write(forwarded.stdout);
17
+ process.stderr.write(forwarded.stderr);
18
+ process.exitCode = forwarded.exitCode;
19
+ }
20
+ else {
21
+ const { runCli } = await import('./runCli.js');
22
+ const key = requestIdFromArgv(argv);
23
+ if (key && commandExecutionTarget(argv) === 'daemon') {
24
+ const result = await runWithRequestId(key, argv, async () => {
25
+ const context = {
26
+ stdout: [], stderr: [], format: null, noticeProvider: null, cwd: process.cwd(),
27
+ };
28
+ const exitCode = await requestContext.run(context, () => localCommandExecution.run(true, () => runCli(argv, error => { fatalError = error; })));
29
+ return { stdout: context.stdout.join(''), stderr: context.stderr.join(''), exitCode };
30
+ });
31
+ process.stdout.write(result.stdout);
32
+ process.stderr.write(result.stderr);
33
+ process.exitCode = result.exitCode;
34
+ }
35
+ else {
36
+ process.exitCode = await localCommandExecution.run(true, () => runCli(argv, error => { fatalError = error; }));
37
+ }
10
38
  }
11
- return argv;
12
39
  }
13
- async function main() {
14
- const program = createCli();
15
- const argv = normalizeArgv(process.argv.slice(2));
16
- const executable = process.argv[1] ?? 'dino';
17
- await program.parseAsync(['node', executable, ...argv]);
40
+ catch (error) {
41
+ // Routing failures use the same printer as command failures.
42
+ const { printCliError } = await import('./runCli.js');
43
+ process.exitCode = printCliError(error, argv);
18
44
  }
19
- function isVerbose(argv) {
20
- return argv.includes('--verbose');
45
+ if (isDinoxError(fatalError) && fatalError.requiresProcessExit) {
46
+ await exitProcessAfterFlushing(Number(process.exitCode));
21
47
  }
22
- function isCommanderError(error) {
23
- return Boolean(error)
24
- && typeof error === 'object'
25
- && typeof error.code === 'string'
26
- && error.code.startsWith('commander.')
27
- && typeof error.exitCode === 'number';
28
- }
29
- async function exitAfterFatalCleanup(error, exitCode) {
30
- if (isDinoxError(error) && error.requiresProcessExit) {
31
- await exitProcessAfterFlushing(exitCode);
32
- }
33
- }
34
- main().catch(async (error) => {
35
- if (isCommanderError(error)) {
36
- process.exitCode = error.exitCode === 0 ? 0 : 2;
37
- return;
38
- }
39
- const argv = normalizeArgv(process.argv.slice(2));
40
- let format = null;
41
- try {
42
- format = parseStructuredOutputFormatFromArgv(argv);
43
- }
44
- catch {
45
- format = null;
46
- }
47
- const verbose = isVerbose(argv);
48
- const payload = buildStructuredErrorPayload(error, verbose);
49
- const exitCode = isDinoxError(error) ? error.exitCode : 1;
50
- process.exitCode = exitCode;
51
- if (format) {
52
- setRequestedStructuredOutputFormat(format);
53
- printError(payload, format);
54
- clearRequestedStructuredOutputFormat();
55
- await exitAfterFatalCleanup(error, exitCode);
56
- return;
57
- }
58
- printHumanReadableError(payload.message);
59
- if (payload.suggested_action) {
60
- printHumanReadableError(`suggested action: ${payload.suggested_action.command}`);
61
- printHumanReadableError(`reason: ${payload.suggested_action.reason}`);
62
- }
63
- else if (payload.suggestion) {
64
- printHumanReadableError(`suggestion: ${payload.suggestion}`);
65
- }
66
- if (verbose && payload.stack) {
67
- printHumanReadableError(payload.stack);
68
- }
69
- await exitAfterFatalCleanup(error, exitCode);
70
- });
@@ -1,6 +1,8 @@
1
+ import { type ExecFileOptionsWithStringEncoding } from 'node:child_process';
1
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { z } from 'zod';
3
4
  import type { CommandSchema } from '../commands/schema/types.js';
5
+ import { tryForwardToDaemon } from '../daemon/forward.js';
4
6
  /**
5
7
  * One MCP tool per command group, generated from the command schemas. Each runs
6
8
  * the real CLI as a child process without a shell, preserving the CLI's own
@@ -17,6 +19,13 @@ export type CommandRunner = (argv: string[]) => Promise<{
17
19
  stderr: string;
18
20
  }>;
19
21
  /** Runs the current CLI entry point without a shell. */
22
+ export declare function createCliCommandRunner(overrides?: {
23
+ forward?: typeof tryForwardToDaemon;
24
+ execute?: (file: string, args: string[], options: ExecFileOptionsWithStringEncoding) => Promise<{
25
+ stdout: string;
26
+ stderr: string;
27
+ }>;
28
+ }): CommandRunner;
20
29
  export declare const runCliCommand: CommandRunner;
21
30
  /** Validates tool inputs and builds CLI arguments in schema order. */
22
31
  export declare function commandArgv(schema: CommandSchema, input: Record<string, unknown>): string[];
@@ -2,6 +2,8 @@ import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import { z } from 'zod';
4
4
  import { listCommandSchemas } from '../commands/schema/spec.js';
5
+ import { tryForwardToDaemon } from '../daemon/forward.js';
6
+ import { requestContext } from '../utils/requestContext.js';
5
7
  /**
6
8
  * One MCP tool per command group, generated from the command schemas. Each runs
7
9
  * the real CLI as a child process without a shell, preserving the CLI's own
@@ -18,16 +20,44 @@ export const MCP_COMMAND_EXCLUSIONS = [
18
20
  ];
19
21
  const execute = promisify(execFile);
20
22
  /** Runs the current CLI entry point without a shell. */
21
- export const runCliCommand = (argv) => {
22
- const entryPoint = process.argv[1];
23
- if (entryPoint === undefined)
24
- throw new Error('Cannot run CLI command: process.argv[1] is undefined');
25
- return execute(process.execPath, [...process.execArgv, entryPoint, ...argv], {
26
- env: process.env,
27
- timeout: 120_000,
28
- maxBuffer: 32 * 1024 * 1024,
29
- });
30
- };
23
+ export function createCliCommandRunner(overrides = {}) {
24
+ return (argv) => {
25
+ const entryPoint = process.argv[1];
26
+ if (entryPoint === undefined)
27
+ throw new Error('Cannot run CLI command: process.argv[1] is undefined');
28
+ return (async () => {
29
+ let result;
30
+ try {
31
+ result = await (overrides.forward ?? tryForwardToDaemon)(argv);
32
+ }
33
+ catch (error) {
34
+ const context = {
35
+ stdout: [], stderr: [], format: null, noticeProvider: null, cwd: process.cwd(),
36
+ };
37
+ const { printCliError } = await import('../runCli.js');
38
+ const code = requestContext.run(context, () => printCliError(error, argv));
39
+ throw Object.assign(new Error(`Command failed with exit code ${code}`), {
40
+ code, stdout: context.stdout.join(''), stderr: context.stderr.join(''),
41
+ });
42
+ }
43
+ if (result) {
44
+ if (result.exitCode !== 0) {
45
+ throw Object.assign(new Error(`Command failed with exit code ${result.exitCode}`), {
46
+ code: result.exitCode, stdout: result.stdout, stderr: result.stderr,
47
+ });
48
+ }
49
+ return result;
50
+ }
51
+ return (overrides.execute ?? execute)(process.execPath, [...process.execArgv, entryPoint, ...argv], {
52
+ encoding: 'utf8',
53
+ env: process.env,
54
+ timeout: 120_000,
55
+ maxBuffer: 32 * 1024 * 1024,
56
+ });
57
+ })();
58
+ };
59
+ }
60
+ export const runCliCommand = createCliCommandRunner();
31
61
  function key(name) {
32
62
  return name
33
63
  .replace(/^[<[.]+|[>\].]+$/g, '')
@@ -7,6 +7,8 @@ export type ConnectResult = {
7
7
  dbPath: string;
8
8
  stale: boolean;
9
9
  releaseOwner: () => Promise<void>;
10
+ /** Drops the sync stream and connects again (online connections only). */
11
+ reconnect?: () => Promise<void>;
10
12
  };
11
13
  type ClosablePowerSyncDatabase = {
12
14
  close: () => Promise<void>;
@@ -215,9 +215,11 @@ export async function connectPowerSync(args) {
215
215
  });
216
216
  }, uploader);
217
217
  await db.connect(connector);
218
+ // connect() disconnects an existing stream first.
219
+ const reconnect = () => db.connect(connector);
218
220
  if (args.waitForInitialSync === false) {
219
221
  const status = getStatusSnapshot(db);
220
- return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner };
222
+ return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner, reconnect };
221
223
  }
222
224
  const { signal, cancel } = createTimeoutSignal(args.timeoutMs);
223
225
  try {
@@ -0,0 +1,10 @@
1
+ import type { CommandExecuteResult } from '../daemon/types.js';
2
+ export declare const AUTO_REQUEST_ID_PREFIX = "auto:";
3
+ export declare function validateRequestId(key: string): string;
4
+ /** Reads only options before the argument terminator. */
5
+ export declare function requestIdFromArgv(argv: string[]): string | undefined;
6
+ /** Claims once across processes and preserves the complete output for replay. */
7
+ export declare function runWithRequestId(key: string, argv: string[], run: () => Promise<CommandExecuteResult>, opts?: {
8
+ waitTimeoutMs?: number;
9
+ pollIntervalMs?: number;
10
+ }): Promise<CommandExecuteResult>;