@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.
- package/dist/cli.js +5 -2
- package/dist/commands/auth/index.d.ts +2 -1
- package/dist/commands/auth/index.js +18 -1
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- package/dist/commands/notes/contentPatch.js +2 -1
- package/dist/commands/notes/export.js +3 -2
- package/dist/commands/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +1 -0
- package/dist/commands/schema/spec.js +9 -1
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +24 -5
- package/dist/daemon/client.js +32 -2
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +34 -0
- package/dist/daemon/forward.js +146 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +44 -0
- package/dist/daemon/server.js +175 -3
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +16 -0
- package/dist/dinox.js +41 -64
- package/dist/gateway/mcpCommandTools.d.ts +9 -0
- package/dist/gateway/mcpCommandTools.js +40 -10
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- package/dist/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/utils/argValue.js +2 -1
- package/dist/utils/errorCodes.d.ts +20 -0
- package/dist/utils/errorCodes.js +5 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +31 -7
- package/dist/utils/requestContext.d.ts +24 -0
- package/dist/utils/requestContext.js +13 -0
- 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 "
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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, '>')
|
|
55
57
|
.replace(/"/g, '"');
|
|
56
58
|
}
|
|
59
|
+
function xmlUnescape(value) {
|
|
60
|
+
return value
|
|
61
|
+
.replace(/</g, '<')
|
|
62
|
+
.replace(/>/g, '>')
|
|
63
|
+
.replace(/"/g, '"')
|
|
64
|
+
.replace(/'/g, "'")
|
|
65
|
+
.replace(/&/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<
|
|
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=
|
|
189
|
-
RestartSec=
|
|
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()) {
|
package/dist/daemon/types.d.ts
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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>;
|