@dinoxx/dinox-cli 2.13.1 → 2.15.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/ai/ragxClient.d.ts +15 -0
- package/dist/ai/ragxClient.js +71 -0
- package/dist/cli.js +31 -4
- package/dist/commands/auth/index.d.ts +2 -1
- package/dist/commands/auth/index.js +18 -1
- package/dist/commands/batch.d.ts +35 -0
- package/dist/commands/batch.js +202 -0
- package/dist/commands/boxes/index.js +21 -21
- 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/mcp.d.ts +6 -0
- package/dist/commands/mcp.js +47 -0
- package/dist/commands/notes/actions.js +18 -2
- package/dist/commands/notes/contentPatch.js +2 -1
- package/dist/commands/notes/descriptors.js +111 -5
- package/dist/commands/notes/detailContent.d.ts +19 -0
- package/dist/commands/notes/detailContent.js +62 -0
- package/dist/commands/notes/export.js +7 -2
- package/dist/commands/notes/repo/search.d.ts +2 -1
- package/dist/commands/notes/repo/search.js +1 -1
- package/dist/commands/notes/repo/semantic.d.ts +14 -0
- package/dist/commands/notes/repo/semantic.js +31 -0
- package/dist/commands/notes/searchResponse.d.ts +3 -0
- package/dist/commands/notes/searchResponse.js +1 -0
- package/dist/commands/notes/semantic.d.ts +116 -0
- package/dist/commands/notes/semantic.js +332 -0
- package/dist/commands/prompt/index.js +7 -5
- package/dist/commands/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/savedViews/index.js +12 -3
- package/dist/commands/schema/globalOptions.d.ts +7 -0
- package/dist/commands/schema/globalOptions.js +7 -0
- package/dist/commands/schema/renderSkills.js +6 -2
- package/dist/commands/schema/spec.js +20 -1
- package/dist/commands/storage/index.js +7 -5
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +27 -8
- package/dist/commands/tags/index.js +21 -21
- package/dist/commands/todo/index.js +22 -2
- package/dist/commands/todo/repo.d.ts +5 -0
- package/dist/commands/todo/repo.js +7 -2
- package/dist/commands/writeReceipt.d.ts +13 -0
- package/dist/commands/writeReceipt.js +12 -0
- package/dist/config/serviceEndpoints.d.ts +2 -0
- package/dist/config/serviceEndpoints.js +2 -0
- package/dist/daemon/client.d.ts +2 -0
- package/dist/daemon/client.js +53 -3
- 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 +37 -0
- package/dist/daemon/forward.js +155 -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 +45 -1
- package/dist/daemon/server.js +192 -4
- 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 +22 -0
- package/dist/dinox.js +43 -64
- package/dist/gateway/deviceHost.js +2 -1
- package/dist/gateway/mcpCommandTools.d.ts +24 -5
- package/dist/gateway/mcpCommandTools.js +70 -29
- package/dist/gateway/mcpEnv.d.ts +2 -0
- package/dist/gateway/mcpEnv.js +2 -0
- package/dist/gateway/mcpGroups.d.ts +3 -0
- package/dist/gateway/mcpGroups.js +4 -0
- package/dist/gateway/mcpServer.d.ts +10 -1
- package/dist/gateway/mcpServer.js +25 -5
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- package/dist/powersync/syncPass.js +13 -1
- package/dist/powersync/tokenIndex.d.ts +2 -0
- package/dist/powersync/tokenIndex.js +1 -0
- package/dist/requests/execute.d.ts +11 -0
- package/dist/requests/execute.js +26 -0
- 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/skills/commandRefs.js +2 -10
- package/dist/utils/argValue.js +2 -1
- package/dist/utils/cursor.d.ts +9 -0
- package/dist/utils/cursor.js +61 -0
- package/dist/utils/defaultFormat.d.ts +8 -0
- package/dist/utils/defaultFormat.js +38 -0
- package/dist/utils/errorCodes.d.ts +24 -0
- package/dist/utils/errorCodes.js +6 -0
- package/dist/utils/output.d.ts +4 -0
- package/dist/utils/output.js +72 -10
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +59 -0
- package/dist/utils/requestContext.d.ts +32 -0
- package/dist/utils/requestContext.js +13 -0
- package/dist/utils/select.d.ts +20 -0
- package/dist/utils/select.js +92 -0
- package/dist/utils/usageError.d.ts +37 -0
- package/dist/utils/usageError.js +127 -0
- package/package.json +2 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RagxRequest = {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
authorization: string | undefined;
|
|
4
|
+
path: '/api/ragx/search' | '/api/ragx/similar' | '/api/ragx/context';
|
|
5
|
+
body: unknown;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
fetch?: typeof fetch;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
|
|
11
|
+
* onto CLI codes: 000001 is a rejected token or request, 000006 a missing
|
|
12
|
+
* target, anything else (and every transport failure) is the service being
|
|
13
|
+
* unavailable.
|
|
14
|
+
*/
|
|
15
|
+
export declare function postRagx<T>(request: RagxRequest): Promise<T>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { toAuthorizationHeaderValue } from '../utils/authToken.js';
|
|
2
|
+
import { DinoxError } from '../utils/errors.js';
|
|
3
|
+
const RAGX_OK = '000000';
|
|
4
|
+
const RAGX_INVALID = '000001';
|
|
5
|
+
const RAGX_NOT_FOUND = '000006';
|
|
6
|
+
function unavailable(message, cause) {
|
|
7
|
+
return new DinoxError(message, {
|
|
8
|
+
code: 'SEMANTIC_SEARCH_UNAVAILABLE',
|
|
9
|
+
cause,
|
|
10
|
+
recoverable: true,
|
|
11
|
+
suggestion: 'Retry shortly, or fall back to keyword search with `dino note search <query> --format json`.',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
|
|
16
|
+
* onto CLI codes: 000001 is a rejected token or request, 000006 a missing
|
|
17
|
+
* target, anything else (and every transport failure) is the service being
|
|
18
|
+
* unavailable.
|
|
19
|
+
*/
|
|
20
|
+
export async function postRagx(request) {
|
|
21
|
+
if (!request.authorization) {
|
|
22
|
+
throw new DinoxError('Semantic search needs a logged-in account.', { code: 'AUTH_REQUIRED', recoverable: true });
|
|
23
|
+
}
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await (request.fetch ?? fetch)(`${request.baseUrl}${request.path}`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: 'application/json',
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Authorization: toAuthorizationHeaderValue(request.authorization),
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(request.body),
|
|
34
|
+
signal: AbortSignal.timeout(request.timeoutMs),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const timedOut = error instanceof Error && error.name === 'TimeoutError';
|
|
39
|
+
throw unavailable(timedOut
|
|
40
|
+
? `Semantic search did not answer within ${request.timeoutMs} ms`
|
|
41
|
+
: `Semantic search service unreachable: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
42
|
+
}
|
|
43
|
+
let payload;
|
|
44
|
+
try {
|
|
45
|
+
payload = JSON.parse(await response.text());
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
payload = null;
|
|
49
|
+
}
|
|
50
|
+
const code = typeof payload?.code === 'string' ? payload.code : null;
|
|
51
|
+
const message = typeof payload?.msg === 'string' ? payload.msg.trim() : '';
|
|
52
|
+
if (code === RAGX_OK && response.ok)
|
|
53
|
+
return (payload?.data ?? null);
|
|
54
|
+
if (response.status === 401 || response.status === 403 || (code === RAGX_INVALID && /authorization|userNo|token/i.test(message))) {
|
|
55
|
+
throw new DinoxError('The semantic search service rejected the login token.', {
|
|
56
|
+
code: 'AUTH_INVALID',
|
|
57
|
+
recoverable: true,
|
|
58
|
+
suggestion: 'Run `dino auth status --format json`; if the token expired, log in again with `dino auth login`.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (code === RAGX_INVALID) {
|
|
62
|
+
throw new DinoxError(`Semantic search rejected the request: ${message || 'invalid request'}`, {
|
|
63
|
+
code: 'INVALID_ARGUMENT',
|
|
64
|
+
recoverable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (code === RAGX_NOT_FOUND) {
|
|
68
|
+
throw new DinoxError(message || 'The semantic search target was not found.', { code: 'NOT_FOUND', recoverable: true });
|
|
69
|
+
}
|
|
70
|
+
throw unavailable(`Semantic search failed (HTTP ${response.status}${code ? `, code ${code}` : ''})${message ? `: ${message}` : ''}`);
|
|
71
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { registerAuthCommands } from './commands/auth/index.js';
|
|
3
|
+
import { parseCommanderUsageError } from './utils/usageError.js';
|
|
4
|
+
import { registerBatchCommand } from './commands/batch.js';
|
|
5
|
+
import { registerMcpCommand } from './commands/mcp.js';
|
|
3
6
|
import { registerBoxesCommand } from './commands/boxes/index.js';
|
|
4
7
|
import { registerConfigCommands } from './commands/config/index.js';
|
|
5
8
|
import { registerDaemonCommands } from './daemon/index.js';
|
|
@@ -21,8 +24,18 @@ import { registerTagsCommand } from './commands/tags/index.js';
|
|
|
21
24
|
import { registerTemplateCommands } from './commands/templates/index.js';
|
|
22
25
|
import { registerTodoCommands } from './commands/todo/index.js';
|
|
23
26
|
import { getPackageVersion } from './utils/version.js';
|
|
27
|
+
import { validateRequestId } from './requests/store.js';
|
|
28
|
+
import { parseFieldSelection } from './utils/select.js';
|
|
29
|
+
import { setProgressReporting } from './utils/progress.js';
|
|
24
30
|
import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
|
|
25
|
-
import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
31
|
+
import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedFieldSelection, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
32
|
+
/** `note.export` for `dino note export`. */
|
|
33
|
+
function commandPath(command) {
|
|
34
|
+
const names = [];
|
|
35
|
+
for (let current = command; current?.parent; current = current.parent)
|
|
36
|
+
names.unshift(current.name());
|
|
37
|
+
return names.join('.');
|
|
38
|
+
}
|
|
26
39
|
export function createCli() {
|
|
27
40
|
clearRequestedStructuredOutputFormat();
|
|
28
41
|
const program = new Command();
|
|
@@ -33,9 +46,10 @@ export function createCli() {
|
|
|
33
46
|
.version(getPackageVersion())
|
|
34
47
|
.showHelpAfterError()
|
|
35
48
|
.configureOutput({
|
|
49
|
+
writeOut: writeToStdout,
|
|
36
50
|
writeErr: (text) => {
|
|
37
51
|
if (!structuredCommanderErrorWritten) {
|
|
38
|
-
|
|
52
|
+
writeToStderr(text);
|
|
39
53
|
}
|
|
40
54
|
},
|
|
41
55
|
outputError: (text, write) => {
|
|
@@ -53,10 +67,14 @@ export function createCli() {
|
|
|
53
67
|
return;
|
|
54
68
|
}
|
|
55
69
|
structuredCommanderErrorWritten = true;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
70
|
+
const args = program.rawArgs?.slice(2) ?? [];
|
|
71
|
+
const usage = parseCommanderUsageError(text, args, program);
|
|
72
|
+
const error = new DinoxError(usage.message, {
|
|
58
73
|
code: 'USAGE_ERROR',
|
|
59
74
|
recoverable: true,
|
|
75
|
+
details: usage.details,
|
|
76
|
+
suggestion: usage.suggestion,
|
|
77
|
+
suggestedAction: usage.suggestedAction,
|
|
60
78
|
});
|
|
61
79
|
printError(buildStructuredErrorPayload(error, false), format);
|
|
62
80
|
},
|
|
@@ -68,15 +86,23 @@ export function createCli() {
|
|
|
68
86
|
.option('--offline', 'skip connect/sync and only use local cache')
|
|
69
87
|
.option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
|
|
70
88
|
.option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
|
|
89
|
+
.option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
|
|
90
|
+
.option('--select <fields>', 'keep only these comma-separated output fields (dot paths, e.g. notes.id,notes.title)')
|
|
91
|
+
.option('--progress', 'report progress of long-running commands (sync, note export) on stderr')
|
|
71
92
|
.option('--verbose', 'enable verbose logging');
|
|
72
93
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
73
94
|
const options = actionCommand.optsWithGlobals?.() ?? {};
|
|
74
95
|
setRequestedStructuredOutputFormat(resolveStructuredOutputFormat(options));
|
|
96
|
+
setRequestedFieldSelection(options.select === undefined ? null : parseFieldSelection(options.select));
|
|
97
|
+
setProgressReporting(options.progress === true ? commandPath(actionCommand) : null);
|
|
75
98
|
});
|
|
76
99
|
program.hook('postAction', () => {
|
|
77
100
|
clearRequestedStructuredOutputFormat();
|
|
101
|
+
setRequestedFieldSelection(null);
|
|
102
|
+
setProgressReporting(null);
|
|
78
103
|
});
|
|
79
104
|
registerAuthCommands(program);
|
|
105
|
+
registerBatchCommand(program);
|
|
80
106
|
registerBoxesCommand(program);
|
|
81
107
|
registerConfigCommands(program);
|
|
82
108
|
registerDaemonCommands(program);
|
|
@@ -84,6 +110,7 @@ export function createCli() {
|
|
|
84
110
|
registerDoctorCommand(program);
|
|
85
111
|
registerGraphCommands(program);
|
|
86
112
|
registerInfoCommand(program);
|
|
113
|
+
registerMcpCommand(program);
|
|
87
114
|
registerMetaCommands(program);
|
|
88
115
|
registerSyncCommand(program);
|
|
89
116
|
registerUpdateCommand(program);
|
|
@@ -3,13 +3,14 @@ import { fetchUserInfoWithAuthorization } from '../../auth/userInfo.js';
|
|
|
3
3
|
import { loadConfig, updateConfig } from '../../config/store.js';
|
|
4
4
|
import { resolveConfig } from '../../config/resolve.js';
|
|
5
5
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked } from '../../daemon/lifecycle.js';
|
|
6
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
6
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
7
7
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
8
8
|
import type { CommandGroupDescriptor } from '../descriptor/types.js';
|
|
9
9
|
import { printHumanReadableWarning } from '../../utils/output.js';
|
|
10
10
|
export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
|
|
11
11
|
type AuthLoginDependencies = {
|
|
12
12
|
acquireDaemonStartLock: typeof acquireDaemonStartLock;
|
|
13
|
+
acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
13
14
|
loadConfig: typeof loadConfig;
|
|
14
15
|
resolveConfig: typeof resolveConfig;
|
|
15
16
|
fetchCredentials: typeof fetchCredentialsWithAuthorization;
|
|
@@ -10,7 +10,7 @@ import { getDaemonInfo } from '../../daemon/client.js';
|
|
|
10
10
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
|
|
11
11
|
import { stopDaemonLocked } from '../../daemon/index.js';
|
|
12
12
|
import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
|
|
13
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
13
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
14
14
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
15
15
|
import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
|
|
16
16
|
import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
|
|
@@ -22,6 +22,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
|
|
|
22
22
|
import { redactAuthorization } from '../../utils/redact.js';
|
|
23
23
|
import { withAsyncRelease } from '../../utils/asyncRelease.js';
|
|
24
24
|
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
25
|
+
import { assertStdinAvailable } from '../../utils/requestContext.js';
|
|
25
26
|
const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
26
27
|
const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
27
28
|
const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
|
|
@@ -80,6 +81,7 @@ export async function releaseOwnerLeases(releases) {
|
|
|
80
81
|
return errors;
|
|
81
82
|
}
|
|
82
83
|
async function readAuthTokenFromStdin() {
|
|
84
|
+
assertStdinAvailable();
|
|
83
85
|
if (process.stdin.isTTY) {
|
|
84
86
|
throw new DinoxError('--token-stdin requires piped stdin', {
|
|
85
87
|
code: 'USAGE_ERROR',
|
|
@@ -100,6 +102,7 @@ async function readAuthTokenFromStdin() {
|
|
|
100
102
|
}
|
|
101
103
|
const authLoginDependencies = {
|
|
102
104
|
acquireDaemonStartLock,
|
|
105
|
+
acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
|
|
103
106
|
loadConfig,
|
|
104
107
|
resolveConfig,
|
|
105
108
|
fetchCredentials: fetchCredentialsWithAuthorization,
|
|
@@ -117,6 +120,15 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
117
120
|
const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
|
|
118
121
|
let previousConfig = null;
|
|
119
122
|
let suspended = null;
|
|
123
|
+
// Held from stopping the daemon until the new login is saved, so a daemon
|
|
124
|
+
// the service manager respawns meanwhile starts with the new identity.
|
|
125
|
+
// Released before any restore, which waits for the daemon to come up.
|
|
126
|
+
let releaseIntentLock = null;
|
|
127
|
+
const releaseIntent = async () => {
|
|
128
|
+
const release = releaseIntentLock;
|
|
129
|
+
releaseIntentLock = null;
|
|
130
|
+
await release?.();
|
|
131
|
+
};
|
|
120
132
|
let lifecycleLockReleaseError;
|
|
121
133
|
let outcome;
|
|
122
134
|
try {
|
|
@@ -138,6 +150,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
138
150
|
});
|
|
139
151
|
const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
|
|
140
152
|
const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
|
|
153
|
+
releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
|
|
141
154
|
suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
|
|
142
155
|
await dependencies.updateConfig((current) => {
|
|
143
156
|
if (!persistedAuthStateMatches(current, previousConfig)) {
|
|
@@ -149,6 +162,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
149
162
|
}
|
|
150
163
|
return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
|
|
151
164
|
}, { timeoutMs });
|
|
165
|
+
await releaseIntent();
|
|
152
166
|
outcome = {
|
|
153
167
|
ok: true,
|
|
154
168
|
value: {
|
|
@@ -164,6 +178,9 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
164
178
|
}
|
|
165
179
|
catch (error) {
|
|
166
180
|
outcome = { ok: false, error };
|
|
181
|
+
await releaseIntent().catch((releaseError) => {
|
|
182
|
+
dependencies.printWarning(`warning: failed to release daemon intent lock: ${authLoginErrorMessage(releaseError)}`);
|
|
183
|
+
});
|
|
167
184
|
if (previousConfig && suspended) {
|
|
168
185
|
try {
|
|
169
186
|
const current = await dependencies.loadConfig();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CommandExecuteResult } from '../daemon/types.js';
|
|
3
|
+
import { executeCommandLine } from '../requests/execute.js';
|
|
4
|
+
import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
|
|
5
|
+
export declare const MAX_BATCH_ITEMS = 100;
|
|
6
|
+
export type BatchItem = {
|
|
7
|
+
index: number;
|
|
8
|
+
id: string | null;
|
|
9
|
+
args: string[];
|
|
10
|
+
};
|
|
11
|
+
export type BatchItemResult = {
|
|
12
|
+
index: number;
|
|
13
|
+
id: string | null;
|
|
14
|
+
args: string[];
|
|
15
|
+
ok: boolean;
|
|
16
|
+
exit_code: number;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
notice?: unknown;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
};
|
|
21
|
+
/** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
|
|
22
|
+
export declare function parseBatchItems(text: string): BatchItem[];
|
|
23
|
+
/** Adds the batch's own sync options to an item that does not set them itself. */
|
|
24
|
+
export declare function itemArgv(args: string[], globals: Record<string, unknown>): string[];
|
|
25
|
+
export declare function toBatchItemResult(item: BatchItem, result: CommandExecuteResult): BatchItemResult;
|
|
26
|
+
declare function readStdin(): Promise<string>;
|
|
27
|
+
declare const batchDependencies: {
|
|
28
|
+
readStdin: typeof readStdin;
|
|
29
|
+
readFile: (file: string) => Promise<string>;
|
|
30
|
+
execute: typeof executeCommandLine;
|
|
31
|
+
};
|
|
32
|
+
export declare function runBatch(context: CommandExecutionContext, overrides?: Partial<typeof batchDependencies>): Promise<void>;
|
|
33
|
+
export declare const batchCommandDescriptor: CommandDescriptor;
|
|
34
|
+
export declare function registerBatchCommand(program: Command): void;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { commandExecutionTarget } from '../daemon/forward.js';
|
|
3
|
+
import { executeCommandLine } from '../requests/execute.js';
|
|
4
|
+
import { validateRequestId } from '../requests/store.js';
|
|
5
|
+
import { DinoxError } from '../utils/errors.js';
|
|
6
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
7
|
+
import { assertStdinAvailable, resolveUserPath } from '../utils/requestContext.js';
|
|
8
|
+
import { registerStandaloneCommand } from './descriptor/register.js';
|
|
9
|
+
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
|
|
10
|
+
export const MAX_BATCH_ITEMS = 100;
|
|
11
|
+
/** Options each item may not set: the batch owns output format and help. */
|
|
12
|
+
const RESERVED_ITEM_OPTIONS = ['--format', '--json', '--help', '-h', '--version', '-V'];
|
|
13
|
+
function invalidInput(message, details) {
|
|
14
|
+
return new DinoxError(message, {
|
|
15
|
+
code: 'INVALID_ARGUMENT',
|
|
16
|
+
recoverable: true,
|
|
17
|
+
details,
|
|
18
|
+
suggestion: 'Pass a JSON array whose items are argument arrays, e.g. [["note","search","AI"],{"id":"t1","args":["todo","search"]}].',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
|
|
22
|
+
export function parseBatchItems(text) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(text);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw invalidInput(`Batch input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
31
|
+
throw invalidInput('Batch input must be a non-empty JSON array');
|
|
32
|
+
if (parsed.length > MAX_BATCH_ITEMS)
|
|
33
|
+
throw invalidInput(`Batch input has ${parsed.length} items; the limit is ${MAX_BATCH_ITEMS}`);
|
|
34
|
+
return parsed.map((entry, index) => {
|
|
35
|
+
const record = Array.isArray(entry) ? { args: entry } : entry;
|
|
36
|
+
const args = record && typeof record === 'object' ? record.args : undefined;
|
|
37
|
+
if (!Array.isArray(args) || args.length === 0 || !args.every((arg) => typeof arg === 'string')) {
|
|
38
|
+
throw invalidInput(`Batch item ${index} must be a non-empty array of strings or an object with "args"`, { index });
|
|
39
|
+
}
|
|
40
|
+
const id = record.id;
|
|
41
|
+
if (id !== undefined && typeof id !== 'string')
|
|
42
|
+
throw invalidInput(`Batch item ${index} has a non-string "id"`, { index });
|
|
43
|
+
const reserved = args.find((arg) => RESERVED_ITEM_OPTIONS.includes(arg) || arg.startsWith('--format='));
|
|
44
|
+
if (reserved)
|
|
45
|
+
throw invalidInput(`Batch item ${index} may not set ${reserved}; the batch returns JSON for every item`, { index });
|
|
46
|
+
if (commandExecutionTarget(args) !== 'daemon') {
|
|
47
|
+
throw invalidInput(`Batch item ${index} (${args.join(' ')}) is not a data command; only note, todo, tag, box, view, prop, template, prompt, graph, storage and meta commands can be batched`, { index });
|
|
48
|
+
}
|
|
49
|
+
const requestId = record.request_id;
|
|
50
|
+
if (requestId !== undefined) {
|
|
51
|
+
if (typeof requestId !== 'string')
|
|
52
|
+
throw invalidInput(`Batch item ${index} has a non-string "request_id"`, { index });
|
|
53
|
+
validateRequestId(requestId);
|
|
54
|
+
if (args.includes('--request-id'))
|
|
55
|
+
throw invalidInput(`Batch item ${index} sets both "request_id" and --request-id`, { index });
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
index,
|
|
59
|
+
id: typeof id === 'string' ? id : null,
|
|
60
|
+
args: requestId === undefined ? [...args] : [...args, '--request-id', requestId],
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Adds the batch's own sync options to an item that does not set them itself. */
|
|
65
|
+
export function itemArgv(args, globals) {
|
|
66
|
+
const argv = [...args];
|
|
67
|
+
if (globals.offline && !argv.includes('--offline'))
|
|
68
|
+
argv.push('--offline');
|
|
69
|
+
if (globals.requireSync && !argv.includes('--require-sync'))
|
|
70
|
+
argv.push('--require-sync');
|
|
71
|
+
if (typeof globals.syncTimeout === 'string' && !argv.some((arg) => arg === '--sync-timeout' || arg.startsWith('--sync-timeout='))) {
|
|
72
|
+
argv.push('--sync-timeout', globals.syncTimeout);
|
|
73
|
+
}
|
|
74
|
+
return [...argv, '--format', 'json'];
|
|
75
|
+
}
|
|
76
|
+
/** Reads the JSON document a command printed, skipping any plain-text lines before it. */
|
|
77
|
+
function parseJsonOutput(text) {
|
|
78
|
+
const start = text.search(/^[{[]/m);
|
|
79
|
+
if (start === -1)
|
|
80
|
+
return null;
|
|
81
|
+
try {
|
|
82
|
+
const value = JSON.parse(text.slice(start));
|
|
83
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function toBatchItemResult(item, result) {
|
|
90
|
+
const base = { index: item.index, id: item.id, args: item.args, exit_code: result.exitCode };
|
|
91
|
+
if (result.exitCode === 0) {
|
|
92
|
+
const envelope = parseJsonOutput(result.stdout);
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
ok: true,
|
|
96
|
+
data: envelope && 'data' in envelope ? envelope.data : (envelope ?? result.stdout),
|
|
97
|
+
...(envelope?._notice ? { notice: envelope._notice } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const payload = parseJsonOutput(result.stderr);
|
|
101
|
+
return {
|
|
102
|
+
...base,
|
|
103
|
+
ok: false,
|
|
104
|
+
error: payload?.error ?? payload ?? { code: 'COMMAND_FAILED', message: result.stderr.trim() || `exit code ${result.exitCode}` },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function errorResult(item, error) {
|
|
108
|
+
const dinox = error instanceof DinoxError ? error : null;
|
|
109
|
+
return {
|
|
110
|
+
index: item.index,
|
|
111
|
+
id: item.id,
|
|
112
|
+
args: item.args,
|
|
113
|
+
ok: false,
|
|
114
|
+
exit_code: dinox?.exitCode ?? 1,
|
|
115
|
+
error: {
|
|
116
|
+
code: dinox?.code ?? 'INTERNAL_ERROR',
|
|
117
|
+
message: error instanceof Error ? error.message : String(error),
|
|
118
|
+
...(dinox?.suggestion ? { suggestion: dinox.suggestion } : {}),
|
|
119
|
+
...(dinox?.details !== undefined ? { details: dinox.details } : {}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function readStdin() {
|
|
124
|
+
assertStdinAvailable();
|
|
125
|
+
if (process.stdin.isTTY) {
|
|
126
|
+
throw invalidInput('dino batch reads a JSON array from stdin; pipe it in or pass --input <file>');
|
|
127
|
+
}
|
|
128
|
+
const chunks = [];
|
|
129
|
+
for await (const chunk of process.stdin)
|
|
130
|
+
chunks.push(Buffer.from(chunk));
|
|
131
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
132
|
+
}
|
|
133
|
+
const batchDependencies = {
|
|
134
|
+
readStdin,
|
|
135
|
+
readFile: (file) => fs.readFile(resolveUserPath(file), 'utf8'),
|
|
136
|
+
execute: executeCommandLine,
|
|
137
|
+
};
|
|
138
|
+
export async function runBatch(context, overrides = {}) {
|
|
139
|
+
const deps = { ...batchDependencies, ...overrides };
|
|
140
|
+
const input = typeof context.options.input === 'string' && context.options.input !== '-'
|
|
141
|
+
? await deps.readFile(context.options.input)
|
|
142
|
+
: await deps.readStdin();
|
|
143
|
+
const items = parseBatchItems(input);
|
|
144
|
+
const stopOnError = Boolean(context.options.stopOnError);
|
|
145
|
+
const results = [];
|
|
146
|
+
for (const item of items) {
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = toBatchItemResult(item, await deps.execute(itemArgv(item.args, context.globals)));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
result = errorResult(item, error);
|
|
153
|
+
}
|
|
154
|
+
results.push(result);
|
|
155
|
+
if (!result.ok && stopOnError)
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
const failed = results.filter((result) => !result.ok).length;
|
|
159
|
+
const summary = {
|
|
160
|
+
total: items.length,
|
|
161
|
+
succeeded: results.length - failed,
|
|
162
|
+
failed,
|
|
163
|
+
skipped: items.length - results.length,
|
|
164
|
+
};
|
|
165
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
166
|
+
printSuccess({ ok: failed === 0, summary, results });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const result of results) {
|
|
170
|
+
const label = result.id ? `${result.id} ` : '';
|
|
171
|
+
const error = result.error;
|
|
172
|
+
printHumanReadableLine(`#${result.index} ${label}${result.ok ? 'ok' : `failed (${error?.code ?? 'ERROR'}: ${error?.message ?? ''})`}\t${result.args.join(' ')}`);
|
|
173
|
+
}
|
|
174
|
+
printHumanReadableLine(`${summary.succeeded} succeeded, ${summary.failed} failed, ${summary.skipped} skipped of ${summary.total}.`);
|
|
175
|
+
}
|
|
176
|
+
export const batchCommandDescriptor = {
|
|
177
|
+
name: 'batch',
|
|
178
|
+
description: 'Run several data commands from one JSON array on stdin or a file and return every result',
|
|
179
|
+
options: [
|
|
180
|
+
{ flags: '--input <file>', description: 'Read the JSON array from this file instead of stdin (- for stdin)' },
|
|
181
|
+
{ flags: '--stop-on-error', description: 'Stop at the first failed item; later items are reported as skipped', type: 'boolean' },
|
|
182
|
+
],
|
|
183
|
+
schema: {
|
|
184
|
+
path: 'batch',
|
|
185
|
+
risk: 'write',
|
|
186
|
+
auth: 'required',
|
|
187
|
+
globalOptions: [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
|
|
188
|
+
output: {
|
|
189
|
+
type: 'structured',
|
|
190
|
+
summary: 'Per-item results in input order with a success summary; each item carries the command\'s own data or structured error',
|
|
191
|
+
fields: ['ok', 'summary', 'results'],
|
|
192
|
+
},
|
|
193
|
+
examples: [
|
|
194
|
+
'echo \'[["note","search","AI","--limit","5"],["todo","search","--status","uncompleted"]]\' | dino batch --format json',
|
|
195
|
+
'dino batch --input ops.json --stop-on-error --format json',
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
action: (context) => runBatch(context),
|
|
199
|
+
};
|
|
200
|
+
export function registerBatchCommand(program) {
|
|
201
|
+
registerStandaloneCommand(program, batchCommandDescriptor);
|
|
202
|
+
}
|