@dinoxx/dinox-cli 2.13.0 → 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 -7
- package/dist/commands/auth/index.js +26 -51
- 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
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>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { getDataDir } from '../config/paths.js';
|
|
6
|
+
import { DinoxError } from '../utils/errors.js';
|
|
7
|
+
const RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
/** Ids the CLI generates only cover its own single retry; their output need not linger. */
|
|
9
|
+
const AUTO_RETENTION_MS = 10 * 60 * 1000;
|
|
10
|
+
export const AUTO_REQUEST_ID_PREFIX = 'auto:';
|
|
11
|
+
const instanceId = randomUUID();
|
|
12
|
+
const cleanedDirs = new Set();
|
|
13
|
+
export function validateRequestId(key) {
|
|
14
|
+
if (typeof key !== 'string' || key.length < 1 || key.length > 128 || /[^A-Za-z0-9._:-]/.test(key)) {
|
|
15
|
+
throw new DinoxError('Request id must contain 1–128 characters from A-Z, a-z, 0-9, ., _, :, -.', { code: 'INVALID_ARGUMENT' });
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
/** Reads only options before the argument terminator. */
|
|
20
|
+
export function requestIdFromArgv(argv) {
|
|
21
|
+
let key;
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
if (argv[i] === '--')
|
|
24
|
+
break;
|
|
25
|
+
if (argv[i] === '--request-id')
|
|
26
|
+
key = validateRequestId(argv[++i] ?? '');
|
|
27
|
+
else if (argv[i].startsWith('--request-id='))
|
|
28
|
+
key = validateRequestId(argv[i].slice(13));
|
|
29
|
+
}
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
function hash(value) {
|
|
33
|
+
return createHash('sha256').update(value).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
function argvHash(argv) {
|
|
36
|
+
const filtered = [];
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
if (argv[i] === '--') {
|
|
39
|
+
filtered.push(...argv.slice(i));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (argv[i] === '--request-id') {
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (argv[i].startsWith('--request-id='))
|
|
47
|
+
continue;
|
|
48
|
+
filtered.push(argv[i]);
|
|
49
|
+
}
|
|
50
|
+
return hash(JSON.stringify(filtered));
|
|
51
|
+
}
|
|
52
|
+
function ownerAlive(pid) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return error.code !== 'ESRCH';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function cleanup(dir) {
|
|
62
|
+
if (cleanedDirs.has(dir))
|
|
63
|
+
return;
|
|
64
|
+
cleanedDirs.add(dir);
|
|
65
|
+
const lockPath = path.join(dir, '.cleanup.lock');
|
|
66
|
+
let lock;
|
|
67
|
+
try {
|
|
68
|
+
lock = await fs.open(lockPath, 'wx', 0o600);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === 'EEXIST')
|
|
72
|
+
return;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
for (const name of await fs.readdir(dir)) {
|
|
77
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name))
|
|
78
|
+
continue;
|
|
79
|
+
const file = path.join(dir, name);
|
|
80
|
+
try {
|
|
81
|
+
const age = Date.now() - (await fs.stat(file)).mtimeMs;
|
|
82
|
+
if (age <= AUTO_RETENTION_MS)
|
|
83
|
+
continue;
|
|
84
|
+
const record = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
85
|
+
if (!record.auto && age <= RETENTION_MS)
|
|
86
|
+
continue;
|
|
87
|
+
// Never expire a command that is still executing, even after 24 hours.
|
|
88
|
+
if (record.state === 'running' && ownerAlive(record.pid))
|
|
89
|
+
continue;
|
|
90
|
+
await fs.unlink(file);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Cleanup is best effort; another process may already have removed it.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await lock.close();
|
|
99
|
+
await fs.rm(lockPath, { force: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Claims once across processes and preserves the complete output for replay. */
|
|
103
|
+
export async function runWithRequestId(key, argv, run, opts = {}) {
|
|
104
|
+
validateRequestId(key);
|
|
105
|
+
const dir = path.join(getDataDir(), 'requests');
|
|
106
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
107
|
+
await fs.chmod(dir, 0o700);
|
|
108
|
+
await cleanup(dir);
|
|
109
|
+
const file = path.join(dir, `${hash(key)}.json`);
|
|
110
|
+
const record = {
|
|
111
|
+
version: 1, argvHash: argvHash(argv), state: 'running', auto: key.startsWith(AUTO_REQUEST_ID_PREFIX), pid: process.pid,
|
|
112
|
+
instanceId, startedAt: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
const deadline = Date.now() + (opts.waitTimeoutMs ?? 300_000);
|
|
115
|
+
try {
|
|
116
|
+
const handle = await fs.open(file, 'wx', 0o600);
|
|
117
|
+
try {
|
|
118
|
+
await handle.writeFile(JSON.stringify(record));
|
|
119
|
+
await handle.sync();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await handle.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'EEXIST')
|
|
127
|
+
throw error;
|
|
128
|
+
while (true) {
|
|
129
|
+
let previous;
|
|
130
|
+
try {
|
|
131
|
+
previous = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// A claimant may still be writing, or may have rejected and removed its claim.
|
|
135
|
+
if (error.code === 'ENOENT') {
|
|
136
|
+
return runWithRequestId(key, argv, run, { ...opts, waitTimeoutMs: Math.max(0, deadline - Date.now()) });
|
|
137
|
+
}
|
|
138
|
+
if (!(error instanceof SyntaxError))
|
|
139
|
+
throw error;
|
|
140
|
+
if (Date.now() >= deadline) {
|
|
141
|
+
throw new DinoxError('Request claim is not readable yet.', { code: 'REQUEST_IN_PROGRESS', recoverable: true });
|
|
142
|
+
}
|
|
143
|
+
await delay(opts.pollIntervalMs ?? 100);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (previous.argvHash !== record.argvHash) {
|
|
147
|
+
throw new DinoxError('Request id was used with different arguments.', { code: 'REQUEST_ID_CONFLICT' });
|
|
148
|
+
}
|
|
149
|
+
if (previous.state === 'done' && previous.result)
|
|
150
|
+
return previous.result;
|
|
151
|
+
if (!ownerAlive(previous.pid)) {
|
|
152
|
+
throw new DinoxError('The earlier request stopped; its outcome is unknown.', {
|
|
153
|
+
code: 'REQUEST_INTERRUPTED',
|
|
154
|
+
suggestion: 'Verify the result, then retry with a new request id.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (Date.now() >= deadline) {
|
|
158
|
+
throw new DinoxError('The earlier request is still running.', {
|
|
159
|
+
code: 'REQUEST_IN_PROGRESS', recoverable: true,
|
|
160
|
+
suggestion: 'Rerun the exact same command with the same request id.',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
await delay(opts.pollIntervalMs ?? 100);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
result = await run();
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
await fs.unlink(file);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
177
|
+
try {
|
|
178
|
+
await handle.writeFile(JSON.stringify({ ...record, state: 'done', finishedAt: Date.now(), result }));
|
|
179
|
+
await handle.sync();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await handle.close();
|
|
183
|
+
}
|
|
184
|
+
await fs.rename(temporary, file);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await fs.rm(temporary, { force: true });
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
package/dist/runCli.d.ts
ADDED
package/dist/runCli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createCli } from './cli.js';
|
|
2
|
+
import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
|
|
3
|
+
import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, printHumanReadableError, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
4
|
+
function normalizeArgv(argv) {
|
|
5
|
+
// pnpm/npm script forwarding can prepend a standalone "--".
|
|
6
|
+
if (argv[0] === '--') {
|
|
7
|
+
return argv.slice(1);
|
|
8
|
+
}
|
|
9
|
+
return argv;
|
|
10
|
+
}
|
|
11
|
+
function isVerbose(argv) {
|
|
12
|
+
return argv.includes('--verbose');
|
|
13
|
+
}
|
|
14
|
+
function isCommanderError(error) {
|
|
15
|
+
return Boolean(error)
|
|
16
|
+
&& typeof error === 'object'
|
|
17
|
+
&& typeof error.code === 'string'
|
|
18
|
+
&& error.code.startsWith('commander.')
|
|
19
|
+
&& typeof error.exitCode === 'number';
|
|
20
|
+
}
|
|
21
|
+
export async function runCli(input, onFatalError) {
|
|
22
|
+
const argv = normalizeArgv(input);
|
|
23
|
+
try {
|
|
24
|
+
await createCli().parseAsync(['node', process.argv[1] ?? 'dino', ...argv]);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
onFatalError?.(error);
|
|
29
|
+
return printCliError(error, argv);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function printCliError(error, argv) {
|
|
33
|
+
if (isCommanderError(error)) {
|
|
34
|
+
return error.exitCode === 0 ? 0 : 2;
|
|
35
|
+
}
|
|
36
|
+
let format = null;
|
|
37
|
+
try {
|
|
38
|
+
format = parseStructuredOutputFormatFromArgv(argv);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
format = null;
|
|
42
|
+
}
|
|
43
|
+
const verbose = isVerbose(argv);
|
|
44
|
+
const payload = buildStructuredErrorPayload(error, verbose);
|
|
45
|
+
const exitCode = isDinoxError(error) ? error.exitCode : 1;
|
|
46
|
+
if (format) {
|
|
47
|
+
setRequestedStructuredOutputFormat(format);
|
|
48
|
+
printError(payload, format);
|
|
49
|
+
clearRequestedStructuredOutputFormat();
|
|
50
|
+
return exitCode;
|
|
51
|
+
}
|
|
52
|
+
printHumanReadableError(payload.message);
|
|
53
|
+
if (payload.suggested_action) {
|
|
54
|
+
printHumanReadableError(`suggested action: ${payload.suggested_action.command}`);
|
|
55
|
+
printHumanReadableError(`reason: ${payload.suggested_action.reason}`);
|
|
56
|
+
}
|
|
57
|
+
else if (payload.suggestion) {
|
|
58
|
+
printHumanReadableError(`suggestion: ${payload.suggestion}`);
|
|
59
|
+
}
|
|
60
|
+
if (verbose && payload.stack) {
|
|
61
|
+
printHumanReadableError(payload.stack);
|
|
62
|
+
}
|
|
63
|
+
return exitCode;
|
|
64
|
+
}
|
package/dist/utils/argValue.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import { DinoxError } from './errors.js';
|
|
3
|
+
import { resolveUserPath } from './requestContext.js';
|
|
3
4
|
export async function readStringOrFile(value) {
|
|
4
5
|
if (!value.startsWith('@')) {
|
|
5
6
|
return value;
|
|
@@ -9,7 +10,7 @@ export async function readStringOrFile(value) {
|
|
|
9
10
|
throw new DinoxError('Invalid @file syntax: expected @<path>');
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
12
|
-
return await fs.readFile(filePath, 'utf8');
|
|
13
|
+
return await fs.readFile(resolveUserPath(filePath), 'utf8');
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
16
|
throw new DinoxError(`Failed to read file: ${filePath}`, { cause: error });
|
|
@@ -47,6 +47,10 @@ export declare const ERROR_CODES: {
|
|
|
47
47
|
readonly exitCode: 1;
|
|
48
48
|
readonly description: "Another process is writing the CLI config.";
|
|
49
49
|
};
|
|
50
|
+
readonly DAEMON_COMMAND_NOT_FORWARDABLE: {
|
|
51
|
+
readonly exitCode: 2;
|
|
52
|
+
readonly description: "The command must run in the CLI process.";
|
|
53
|
+
};
|
|
50
54
|
readonly DAEMON_ERROR: {
|
|
51
55
|
readonly exitCode: 5;
|
|
52
56
|
readonly description: "The background daemon reported an error.";
|
|
@@ -79,6 +83,10 @@ export declare const ERROR_CODES: {
|
|
|
79
83
|
readonly exitCode: 1;
|
|
80
84
|
readonly description: "The service manager (launchd / systemd) rejected a daemon service operation.";
|
|
81
85
|
};
|
|
86
|
+
readonly DAEMON_SERVICE_FOREIGN: {
|
|
87
|
+
readonly exitCode: 1;
|
|
88
|
+
readonly description: "The daemon service runs another dino install; it was left unchanged.";
|
|
89
|
+
};
|
|
82
90
|
readonly DAEMON_SERVICE_UNSUPPORTED: {
|
|
83
91
|
readonly exitCode: 5;
|
|
84
92
|
readonly description: "The daemon cannot run as a login service on this platform or with this auth setup.";
|
|
@@ -343,6 +351,18 @@ export declare const ERROR_CODES: {
|
|
|
343
351
|
readonly exitCode: 1;
|
|
344
352
|
readonly description: "A property value does not match its definition.";
|
|
345
353
|
};
|
|
354
|
+
readonly REQUEST_ID_CONFLICT: {
|
|
355
|
+
readonly exitCode: 2;
|
|
356
|
+
readonly description: "The request id was already used for different arguments.";
|
|
357
|
+
};
|
|
358
|
+
readonly REQUEST_INTERRUPTED: {
|
|
359
|
+
readonly exitCode: 1;
|
|
360
|
+
readonly description: "The earlier request owner exited; its outcome is unknown.";
|
|
361
|
+
};
|
|
362
|
+
readonly REQUEST_IN_PROGRESS: {
|
|
363
|
+
readonly exitCode: 1;
|
|
364
|
+
readonly description: "The earlier request is still running; retry with the same id.";
|
|
365
|
+
};
|
|
346
366
|
readonly SAVED_VIEW_FILTER_CORRUPT: {
|
|
347
367
|
readonly exitCode: 1;
|
|
348
368
|
readonly description: "A stored saved view filter is corrupt.";
|
package/dist/utils/errorCodes.js
CHANGED
|
@@ -17,6 +17,7 @@ export const ERROR_CODES = {
|
|
|
17
17
|
COMMAND_FAILED: { exitCode: 1, description: 'Generic command failure without a more specific code.' },
|
|
18
18
|
CONFIG_INVALID_JSON: { exitCode: 1, description: 'The CLI config file is not valid JSON.' },
|
|
19
19
|
CONFIG_WRITE_BUSY: { exitCode: 1, description: 'Another process is writing the CLI config.' },
|
|
20
|
+
DAEMON_COMMAND_NOT_FORWARDABLE: { exitCode: 2, description: 'The command must run in the CLI process.' },
|
|
20
21
|
DAEMON_ERROR: { exitCode: 5, description: 'The background daemon reported an error.' },
|
|
21
22
|
DAEMON_INSTANCE_CHANGED: { exitCode: 1, description: 'The daemon instance changed between requests.' },
|
|
22
23
|
DAEMON_INSTANCE_MISMATCH: { exitCode: 1, description: 'The running daemon belongs to a different CLI version or account.' },
|
|
@@ -25,6 +26,7 @@ export const ERROR_CODES = {
|
|
|
25
26
|
DAEMON_RELEASE_FAILED: { exitCode: 1, description: 'The daemon could not release database ownership.' },
|
|
26
27
|
DAEMON_RELEASE_TIMEOUT: { exitCode: 1, description: 'The daemon did not release database ownership in time.' },
|
|
27
28
|
DAEMON_SERVICE_FAILED: { exitCode: 1, description: 'The service manager (launchd / systemd) rejected a daemon service operation.' },
|
|
29
|
+
DAEMON_SERVICE_FOREIGN: { exitCode: 1, description: 'The daemon service runs another dino install; it was left unchanged.' },
|
|
28
30
|
DAEMON_SERVICE_UNSUPPORTED: { exitCode: 5, description: 'The daemon cannot run as a login service on this platform or with this auth setup.' },
|
|
29
31
|
DAEMON_STALE: { exitCode: 5, description: 'The daemon state is stale and needs a restart.' },
|
|
30
32
|
DAEMON_START_FAILED: { exitCode: 5, description: 'The daemon process failed to start.' },
|
|
@@ -91,6 +93,9 @@ export const ERROR_CODES = {
|
|
|
91
93
|
PROPERTY_UNIQUE_ID_TYPE_MISMATCH: { exitCode: 1, description: 'A unique-id setting was supplied for a different property type.' },
|
|
92
94
|
PROPERTY_URL_INVALID: { exitCode: 1, description: 'A url property value is invalid.' },
|
|
93
95
|
PROPERTY_VALUE_INVALID: { exitCode: 1, description: 'A property value does not match its definition.' },
|
|
96
|
+
REQUEST_ID_CONFLICT: { exitCode: 2, description: 'The request id was already used for different arguments.' },
|
|
97
|
+
REQUEST_INTERRUPTED: { exitCode: 1, description: 'The earlier request owner exited; its outcome is unknown.' },
|
|
98
|
+
REQUEST_IN_PROGRESS: { exitCode: 1, description: 'The earlier request is still running; retry with the same id.' },
|
|
94
99
|
SAVED_VIEW_FILTER_CORRUPT: { exitCode: 1, description: 'A stored saved view filter is corrupt.' },
|
|
95
100
|
SAVED_VIEW_FILTER_TOO_COMPLEX: { exitCode: 1, description: 'The saved view filter exceeds supported complexity.' },
|
|
96
101
|
SAVED_VIEW_FILTER_VALUE_REQUIRED: { exitCode: 5, description: 'A saved view filter condition is missing its value.' },
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type StructuredOutputFormat = 'yaml' | 'json';
|
|
|
2
2
|
export type StructuredNoticeMap = Record<string, unknown>;
|
|
3
3
|
export declare function toYaml(value: unknown): string;
|
|
4
4
|
export declare function toJson(value: unknown): string;
|
|
5
|
+
export declare function writeToStdout(text: string): void;
|
|
6
|
+
export declare function writeToStderr(text: string): void;
|
|
5
7
|
export declare function printHumanReadableLine(text: string): void;
|
|
6
8
|
export declare function printHumanReadableBlock(text: string): void;
|
|
7
9
|
export declare function printHumanReadableWarning(text: string): void;
|
package/dist/utils/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stringify as stringifyYaml } from 'yaml';
|
|
2
2
|
import { DinoxError } from './errors.js';
|
|
3
3
|
import { sanitizeForTerminal } from './terminalSanitization.js';
|
|
4
|
+
import { requestContext } from './requestContext.js';
|
|
4
5
|
let requestedStructuredOutputFormat = null;
|
|
5
6
|
let pendingStructuredNoticeProvider = null;
|
|
6
7
|
export function toYaml(value) {
|
|
@@ -12,10 +13,20 @@ export function toYaml(value) {
|
|
|
12
13
|
export function toJson(value) {
|
|
13
14
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
14
15
|
}
|
|
15
|
-
function writeToStdout(text) {
|
|
16
|
+
export function writeToStdout(text) {
|
|
17
|
+
const context = requestContext.getStore();
|
|
18
|
+
if (context) {
|
|
19
|
+
context.stdout.push(text);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
16
22
|
process.stdout.write(text);
|
|
17
23
|
}
|
|
18
|
-
function writeToStderr(text) {
|
|
24
|
+
export function writeToStderr(text) {
|
|
25
|
+
const context = requestContext.getStore();
|
|
26
|
+
if (context) {
|
|
27
|
+
context.stderr.push(text);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
19
30
|
process.stderr.write(text);
|
|
20
31
|
}
|
|
21
32
|
export function printHumanReadableLine(text) {
|
|
@@ -92,25 +103,38 @@ export function parseStructuredOutputFormatFromArgv(argv) {
|
|
|
92
103
|
return explicitFormat ?? (hasLegacyJson ? 'yaml' : null);
|
|
93
104
|
}
|
|
94
105
|
export function setRequestedStructuredOutputFormat(format) {
|
|
106
|
+
const context = requestContext.getStore();
|
|
107
|
+
if (context) {
|
|
108
|
+
context.format = format;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
95
111
|
requestedStructuredOutputFormat = format;
|
|
96
112
|
}
|
|
97
113
|
export function clearRequestedStructuredOutputFormat() {
|
|
98
|
-
|
|
114
|
+
setRequestedStructuredOutputFormat(null);
|
|
99
115
|
}
|
|
100
116
|
export function getRequestedStructuredOutputFormat() {
|
|
101
|
-
|
|
117
|
+
const context = requestContext.getStore();
|
|
118
|
+
return context ? context.format : requestedStructuredOutputFormat;
|
|
102
119
|
}
|
|
103
120
|
export function setPendingStructuredNoticeProvider(provider) {
|
|
121
|
+
const context = requestContext.getStore();
|
|
122
|
+
if (context) {
|
|
123
|
+
context.noticeProvider = provider;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
104
126
|
pendingStructuredNoticeProvider = provider;
|
|
105
127
|
}
|
|
106
128
|
export function clearPendingStructuredNoticeProvider() {
|
|
107
|
-
|
|
129
|
+
setPendingStructuredNoticeProvider(null);
|
|
108
130
|
}
|
|
109
131
|
function getPendingStructuredNotice() {
|
|
110
|
-
|
|
132
|
+
const context = requestContext.getStore();
|
|
133
|
+
const provider = context ? context.noticeProvider : pendingStructuredNoticeProvider;
|
|
134
|
+
if (!provider) {
|
|
111
135
|
return null;
|
|
112
136
|
}
|
|
113
|
-
return
|
|
137
|
+
return provider();
|
|
114
138
|
}
|
|
115
139
|
export function printStructuredData(value, format = getRequestedStructuredOutputFormat()) {
|
|
116
140
|
if (format === 'json') {
|