@dinoxx/dinox-cli 2.9.0 → 2.11.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/README.md +30 -32
- package/dist/auth/devicePair.d.ts +62 -0
- package/dist/auth/devicePair.js +377 -0
- package/dist/commands/auth/index.js +90 -8
- package/dist/commands/gateway/index.js +16 -10
- package/dist/commands/graph/index.js +2 -0
- package/dist/commands/schema/renderSkills.js +2 -2
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +61 -11
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/ensure.js +7 -0
- package/dist/daemon/route.js +2 -0
- package/dist/gateway/config.d.ts +1 -5
- package/dist/gateway/config.js +23 -2
- package/dist/gateway/detect.d.ts +26 -3
- package/dist/gateway/detect.js +126 -13
- package/dist/utils/errorCodes.d.ts +12 -0
- package/dist/utils/errorCodes.js +3 -0
- package/package.json +3 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { authApiBaseFromTokenEndpoint, DevicePairCancelled, deviceNameFromHostname, pollDevicePairLogin, prepareCliDevice, renderTerminalQr, revokeDeviceBinding, saveDevicePairResult, } from '../../auth/devicePair.js';
|
|
2
5
|
import { buildPendingAuthConfig, fetchUserInfoWithAuthorization, hydrateResolvedAuthIdentity, mergeAuthIdentityIntoConfig, persistedAuthStateMatches, rotatePersistedAuthStateRevision, } from '../../auth/userInfo.js';
|
|
3
6
|
import { getLegacyPowerSyncDbPath, getPowerSyncDbPath } from '../../config/paths.js';
|
|
4
7
|
import { loadConfig, updateConfig } from '../../config/store.js';
|
|
@@ -227,6 +230,45 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
227
230
|
}
|
|
228
231
|
return outcome.value;
|
|
229
232
|
}
|
|
233
|
+
async function loginWithDeviceQr() {
|
|
234
|
+
const config = await loadConfig();
|
|
235
|
+
const resolved = resolveConfig(config);
|
|
236
|
+
const apiBase = authApiBaseFromTokenEndpoint(resolved.powersync.tokenEndpoint);
|
|
237
|
+
const store = { load: loadConfig, update: updateConfig };
|
|
238
|
+
const prepared = await prepareCliDevice({
|
|
239
|
+
store,
|
|
240
|
+
apiBase,
|
|
241
|
+
authorization: normalizeAuthToken(config.auth?.authorization),
|
|
242
|
+
deviceBindingId: config.auth?.deviceBindingId,
|
|
243
|
+
randomUUID: crypto.randomUUID,
|
|
244
|
+
});
|
|
245
|
+
if (prepared.revokeWarning)
|
|
246
|
+
printHumanReadableWarning(prepared.revokeWarning);
|
|
247
|
+
const abort = new AbortController();
|
|
248
|
+
const onSigint = () => abort.abort();
|
|
249
|
+
process.once('SIGINT', onSigint);
|
|
250
|
+
try {
|
|
251
|
+
const paired = await pollDevicePairLogin({
|
|
252
|
+
apiBase,
|
|
253
|
+
deviceId: prepared.deviceId,
|
|
254
|
+
deviceName: deviceNameFromHostname(os.hostname()),
|
|
255
|
+
renderQr: renderTerminalQr,
|
|
256
|
+
writeStderr: (text) => { process.stderr.write(text); },
|
|
257
|
+
signal: abort.signal,
|
|
258
|
+
});
|
|
259
|
+
return { token: paired.token, deviceBindingId: paired.deviceBindingId };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof DevicePairCancelled) {
|
|
263
|
+
process.exitCode = 130;
|
|
264
|
+
process.exit(130);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
process.off('SIGINT', onSigint);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
230
272
|
async function runAuthLogin(context) {
|
|
231
273
|
const options = context.options;
|
|
232
274
|
if (context.args.token && options.tokenStdin) {
|
|
@@ -234,17 +276,35 @@ async function runAuthLogin(context) {
|
|
|
234
276
|
code: 'USAGE_ERROR',
|
|
235
277
|
});
|
|
236
278
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
throw new DinoxError('Token is required');
|
|
241
|
-
}
|
|
279
|
+
const explicitToken = options.tokenStdin ? await readAuthTokenFromStdin() : context.args.token;
|
|
280
|
+
let rawToken = normalizeAuthToken(explicitToken);
|
|
281
|
+
let pairedDeviceBindingId;
|
|
242
282
|
const envToken = readEnvAuthToken();
|
|
283
|
+
if (options.tokenStdin || context.args.token) {
|
|
284
|
+
if (!rawToken) {
|
|
285
|
+
throw new DinoxError('Token is required');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// A scanned login can never equal DINOX_TOKEN, so refuse before the phone binds this computer.
|
|
290
|
+
if (envToken) {
|
|
291
|
+
throw new DinoxError('DINOX_TOKEN is set, so a QR login would be overridden by it. Unset DINOX_TOKEN before running `dino auth login`, or log in with that token via `--token-stdin`.', { code: 'USAGE_ERROR' });
|
|
292
|
+
}
|
|
293
|
+
const paired = await loginWithDeviceQr();
|
|
294
|
+
rawToken = paired.token;
|
|
295
|
+
pairedDeviceBindingId = paired.deviceBindingId;
|
|
296
|
+
}
|
|
243
297
|
if (envToken && envToken !== rawToken) {
|
|
244
298
|
throw new DinoxError('DINOX_TOKEN is set and differs from the login token. Unset DINOX_TOKEN before running `dino auth login`, or use the same token.');
|
|
245
299
|
}
|
|
246
300
|
const structuredOutput = getRequestedStructuredOutputFormat() !== null;
|
|
247
301
|
const result = await performAuthLogin(rawToken, context.globals.syncTimeout);
|
|
302
|
+
if (pairedDeviceBindingId) {
|
|
303
|
+
await saveDevicePairResult({
|
|
304
|
+
store: { load: loadConfig, update: updateConfig },
|
|
305
|
+
deviceBindingId: pairedDeviceBindingId,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
248
308
|
// After the login lock is released: the service starts the daemon under the new login.
|
|
249
309
|
const daemonService = await autoInstallDaemonService();
|
|
250
310
|
if (structuredOutput) {
|
|
@@ -269,6 +329,22 @@ async function runAuthLogoutLocked(options) {
|
|
|
269
329
|
const config = await loadConfig();
|
|
270
330
|
const resolved = resolveConfig(config);
|
|
271
331
|
const envTokenActive = Boolean(readEnvAuthToken());
|
|
332
|
+
const savedToken = normalizeAuthToken(config.auth?.authorization);
|
|
333
|
+
const deviceBindingId = config.auth?.deviceBindingId?.trim();
|
|
334
|
+
if (deviceBindingId) {
|
|
335
|
+
if (!savedToken) {
|
|
336
|
+
printHumanReadableWarning(`warning: could not revoke paired device ${deviceBindingId} (no saved token). Continuing.`);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const revoked = await revokeDeviceBinding({
|
|
340
|
+
apiBase: authApiBaseFromTokenEndpoint(resolved.powersync.tokenEndpoint),
|
|
341
|
+
deviceBindingId,
|
|
342
|
+
authorization: savedToken,
|
|
343
|
+
});
|
|
344
|
+
if (revoked.warning)
|
|
345
|
+
printHumanReadableWarning(revoked.warning);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
272
348
|
const daemonInfo = await getDaemonInfo();
|
|
273
349
|
let initiallyResolvedUserId = resolved.auth.userId;
|
|
274
350
|
if (!initiallyResolvedUserId
|
|
@@ -340,6 +416,8 @@ async function runAuthLogoutLocked(options) {
|
|
|
340
416
|
username: undefined,
|
|
341
417
|
email: undefined,
|
|
342
418
|
userInfoSyncedAt: undefined,
|
|
419
|
+
deviceBindingId: undefined,
|
|
420
|
+
deviceId: undefined,
|
|
343
421
|
},
|
|
344
422
|
});
|
|
345
423
|
}, { timeoutMs: resolved.sync.timeoutMs });
|
|
@@ -489,8 +567,8 @@ export const authCommandGroup = {
|
|
|
489
567
|
commands: [
|
|
490
568
|
{
|
|
491
569
|
name: 'login',
|
|
492
|
-
description: '
|
|
493
|
-
arguments: [{ syntax: '[token]', description: 'Login token (omit when using --token-stdin)' }],
|
|
570
|
+
description: 'Log in by scanning a QR code, or save a token and verify PowerSync connectivity',
|
|
571
|
+
arguments: [{ syntax: '[token]', description: 'Login token (omit to scan a QR code, or when using --token-stdin)' }],
|
|
494
572
|
options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
|
|
495
573
|
schema: {
|
|
496
574
|
path: 'auth.login',
|
|
@@ -502,7 +580,11 @@ export const authCommandGroup = {
|
|
|
502
580
|
summary: 'Login verification result',
|
|
503
581
|
fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId', 'daemonService'],
|
|
504
582
|
},
|
|
505
|
-
examples: [
|
|
583
|
+
examples: [
|
|
584
|
+
'dino auth login',
|
|
585
|
+
'dino auth login --format json',
|
|
586
|
+
'dino auth login --token-stdin --format json',
|
|
587
|
+
],
|
|
506
588
|
},
|
|
507
589
|
action: runAuthLogin,
|
|
508
590
|
},
|
|
@@ -116,8 +116,23 @@ async function runAdd(context) {
|
|
|
116
116
|
printHumanReadableLine(`Added gateway "${gateway.name}" as device ${gateway.deviceId}.`);
|
|
117
117
|
printStatuses(statuses);
|
|
118
118
|
}
|
|
119
|
+
function formatDetectedAgent(agent) {
|
|
120
|
+
const launch = [agent.command, ...agent.args].join(' ');
|
|
121
|
+
const label = agent.verified
|
|
122
|
+
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
123
|
+
: agent.installHint
|
|
124
|
+
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
125
|
+
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
126
|
+
return `${agent.name}: ${label} · ${launch}`;
|
|
127
|
+
}
|
|
119
128
|
async function runDetect(context) {
|
|
120
|
-
const
|
|
129
|
+
const structured = Boolean(getRequestedStructuredOutputFormat());
|
|
130
|
+
if (!structured)
|
|
131
|
+
printHumanReadableLine('Checking installed ACP agents...');
|
|
132
|
+
const detected = await detectAcpAgents({
|
|
133
|
+
probe: true,
|
|
134
|
+
onDetected: structured ? undefined : (agent) => printHumanReadableLine(formatDetectedAgent(agent)),
|
|
135
|
+
});
|
|
121
136
|
const added = [];
|
|
122
137
|
const updated = [];
|
|
123
138
|
if (context.options.add) {
|
|
@@ -146,15 +161,6 @@ async function runDetect(context) {
|
|
|
146
161
|
printHumanReadableLine('No ACP agents found on PATH.');
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
|
-
for (const agent of detected) {
|
|
150
|
-
const launch = [agent.command, ...agent.args].join(' ');
|
|
151
|
-
const label = agent.verified
|
|
152
|
-
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
153
|
-
: agent.installHint
|
|
154
|
-
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
155
|
-
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
156
|
-
printHumanReadableLine(`${agent.name}: ${label} · ${launch}`);
|
|
157
|
-
}
|
|
158
164
|
for (const gateway of added)
|
|
159
165
|
printHumanReadableLine(`Added gateway "${gateway.name}" (${gateway.deviceId}).`);
|
|
160
166
|
for (const gateway of updated) {
|
|
@@ -48,6 +48,8 @@ async function runGraphCommand(context, onlineAction, offlineAction) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
|
+
if (error instanceof DinoxError && error.code === 'AUTH_REQUIRED')
|
|
52
|
+
throw error;
|
|
51
53
|
const message = error instanceof Error ? error.message : String(error);
|
|
52
54
|
throw new DinoxError(`Daemon execution failed: ${message}`, {
|
|
53
55
|
code: 'DAEMON_UNAVAILABLE',
|
|
@@ -242,7 +242,7 @@ export function renderBundledSkillReference() {
|
|
|
242
242
|
lines.push('```');
|
|
243
243
|
lines.push('');
|
|
244
244
|
if (section.title === 'Auth') {
|
|
245
|
-
lines.push('Agent safety: the positional login-token form is legacy compatibility and is intentionally omitted from this agent-facing signature. Agent workflows must never place a Dinox authentication token in argv;
|
|
245
|
+
lines.push('Agent safety: the positional login-token form is legacy compatibility and is intentionally omitted from this agent-facing signature. Agent workflows must never place a Dinox authentication token in argv. Prefer `dino auth login` in the user\'s terminal (QR code; it blocks for up to 10 minutes; in Claude Code the user can type `! dino auth login`). Use `--token-stdin` only from the user\'s own shell or secret store.');
|
|
246
246
|
lines.push('');
|
|
247
247
|
}
|
|
248
248
|
}
|
|
@@ -287,7 +287,7 @@ export function renderPortableRuntimeContract() {
|
|
|
287
287
|
'- On a nonzero exit, parse structured stderr. Use top-level `code`, `recoverable`, `exit_code`, and `suggested_action`; inspect `error` for details. `dino schema errors --format json` lists every code and its exit status.',
|
|
288
288
|
'- A direct user request for an exact write authorizes that write when the dry-run matches. Ask again before delete, overwrite, merge, bulk mutation, local-cache removal, repair, global CLI update, or whenever targets or impact are ambiguous.',
|
|
289
289
|
'- A dry-run does not apply the planned mutation, but an online PowerSync runtime may flush older queued writes. Use `--offline` only when the user accepts stale local-cache semantics.',
|
|
290
|
-
'- Do not ask for Dinox authentication credentials in chat or place a Dinox authentication token in argv.
|
|
290
|
+
'- Do not ask for Dinox authentication credentials in chat or place a Dinox authentication token in argv. For persistent login, have the user run `dino auth login` in their own terminal and scan the QR code with the Dinox app. In Claude Code they can type `! dino auth login`. That command blocks for up to 10 minutes; say so before an agent runs it. Inherited `DINOX_TOKEN` and `dino auth login --token-stdin` remain for CI and secret stores.',
|
|
291
291
|
'- Execute `dino` in an environment that can access the intended CLI binary, Dinox config/cache, and injected secret. Host-turn secret injection does not automatically cross into a sandbox or remote execution backend; never copy a secret through prompts to bridge that boundary.',
|
|
292
292
|
'- If the host times out before receiving a structured envelope, treat the outcome as unknown. Verify freshness or the write postcondition before retrying.',
|
|
293
293
|
'- Before claiming complete, current, all, none, latest, or an exact count, verify the command\'s freshness field is false (usually `data.stale`; note search uses `data.meta.stale`), inspect `_notice`, and check pagination or truncation fields returned by the command.',
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandDescriptor } from './descriptor/types.js';
|
|
3
|
+
import { type MissingAcpAdapter } from '../gateway/detect.js';
|
|
3
4
|
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
4
5
|
type LogFn = (...parts: unknown[]) => void;
|
|
6
|
+
export type AcpAdapterInstallResult = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
package: string;
|
|
10
|
+
status: 'installed' | 'failed';
|
|
11
|
+
error: string | null;
|
|
12
|
+
};
|
|
5
13
|
type UpdateCommandDeps = {
|
|
6
14
|
detectPackageManager: () => PackageManager;
|
|
7
15
|
runUpdate: (pm: PackageManager, options: {
|
|
8
16
|
structuredOutput: boolean;
|
|
9
17
|
}) => Promise<void>;
|
|
18
|
+
installAcpAdapters: (pm: PackageManager, options: {
|
|
19
|
+
structuredOutput: boolean;
|
|
20
|
+
}) => Promise<AcpAdapterInstallResult[]>;
|
|
10
21
|
getVersion: () => string;
|
|
11
22
|
getLatestVersion: () => Promise<string | null>;
|
|
12
23
|
log: LogFn;
|
|
@@ -20,7 +31,12 @@ type UpdateSpawnSpec = {
|
|
|
20
31
|
args: string[];
|
|
21
32
|
shell: boolean;
|
|
22
33
|
};
|
|
34
|
+
export declare function resolvePackageManagerSpawn(pm: PackageManager, args: string[], platform?: NodeJS.Platform): UpdateSpawnSpec;
|
|
23
35
|
export declare function resolveUpdateSpawnSpec(pm: PackageManager, platform?: NodeJS.Platform): UpdateSpawnSpec;
|
|
36
|
+
/** Installs ACP adapters for local agent CLIs that do not already have one. */
|
|
37
|
+
export declare function installMissingAcpAdapters(pm: PackageManager, options: {
|
|
38
|
+
structuredOutput: boolean;
|
|
39
|
+
}, missing?: MissingAcpAdapter[]): Promise<AcpAdapterInstallResult[]>;
|
|
24
40
|
export declare function createUpdateCommandDescriptor(overrides?: Partial<UpdateCommandDeps>): CommandDescriptor;
|
|
25
41
|
export declare const updateCommandDescriptor: CommandDescriptor;
|
|
26
42
|
export declare function registerUpdateCommand(program: Command, overrides?: Partial<UpdateCommandDeps>): void;
|
package/dist/commands/update.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getSkillsReleaseMetadata } from '../constants/links.js';
|
|
|
6
6
|
import { RETIRED_SKILLS, RETIRED_SKILLS_CLEANUP, formatRetiredSkillsSummary, } from '../skills/retired.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
8
8
|
import { DinoxError } from '../utils/errors.js';
|
|
9
|
+
import { findMissingAcpAdapters } from '../gateway/detect.js';
|
|
9
10
|
import { fetchLatestPublishedVersion } from '../utils/updateNotice.js';
|
|
10
11
|
import { getPackageVersion } from '../utils/version.js';
|
|
11
12
|
const PACKAGE_NAME = '@dinoxx/dinox-cli';
|
|
@@ -51,21 +52,31 @@ const PM_COMMANDS = {
|
|
|
51
52
|
yarn: { bin: 'yarn', args: ['global', 'add', `${PACKAGE_NAME}@latest`] },
|
|
52
53
|
bun: { bin: 'bun', args: ['add', '--global', `${PACKAGE_NAME}@latest`] },
|
|
53
54
|
};
|
|
54
|
-
export function
|
|
55
|
-
const
|
|
56
|
-
const usesWindowsCommandShim = platform === 'win32' && bin !== 'bun';
|
|
55
|
+
export function resolvePackageManagerSpawn(pm, args, platform = process.platform) {
|
|
56
|
+
const usesWindowsCommandShim = platform === 'win32' && pm !== 'bun';
|
|
57
57
|
if (usesWindowsCommandShim) {
|
|
58
58
|
return {
|
|
59
|
-
command: [`${
|
|
59
|
+
command: [`${pm}.cmd`, ...args].join(' '),
|
|
60
60
|
args: [],
|
|
61
61
|
shell: true,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
return { command: pm, args: [...args], shell: false };
|
|
65
|
+
}
|
|
66
|
+
export function resolveUpdateSpawnSpec(pm, platform = process.platform) {
|
|
67
|
+
return resolvePackageManagerSpawn(pm, PM_COMMANDS[pm].args, platform);
|
|
68
|
+
}
|
|
69
|
+
function globalInstallArgs(pm, packageName) {
|
|
70
|
+
switch (pm) {
|
|
71
|
+
case 'npm':
|
|
72
|
+
return ['install', '--global', packageName];
|
|
73
|
+
case 'pnpm':
|
|
74
|
+
return ['add', '--global', packageName];
|
|
75
|
+
case 'yarn':
|
|
76
|
+
return ['global', 'add', packageName];
|
|
77
|
+
case 'bun':
|
|
78
|
+
return ['add', '--global', packageName];
|
|
79
|
+
}
|
|
69
80
|
}
|
|
70
81
|
function appendBoundedOutput(current, chunk) {
|
|
71
82
|
if (Buffer.byteLength(current, 'utf8') >= MAX_CAPTURED_UPDATE_OUTPUT_BYTES) {
|
|
@@ -74,8 +85,8 @@ function appendBoundedOutput(current, chunk) {
|
|
|
74
85
|
const remaining = MAX_CAPTURED_UPDATE_OUTPUT_BYTES - Buffer.byteLength(current, 'utf8');
|
|
75
86
|
return current + chunk.subarray(0, remaining).toString('utf8');
|
|
76
87
|
}
|
|
77
|
-
function
|
|
78
|
-
const { command, args, shell } =
|
|
88
|
+
function runPackageManager(pm, packageArgs, options) {
|
|
89
|
+
const { command, args, shell } = resolvePackageManagerSpawn(pm, packageArgs);
|
|
79
90
|
const invocation = [command, ...args].join(' ');
|
|
80
91
|
return new Promise((resolve, reject) => {
|
|
81
92
|
let stdout = '';
|
|
@@ -120,10 +131,38 @@ function runUpdate(pm, options) {
|
|
|
120
131
|
});
|
|
121
132
|
});
|
|
122
133
|
}
|
|
134
|
+
function runUpdate(pm, options) {
|
|
135
|
+
return runPackageManager(pm, PM_COMMANDS[pm].args, options);
|
|
136
|
+
}
|
|
137
|
+
function adapterErrorMessage(error) {
|
|
138
|
+
return error instanceof Error ? error.message : String(error);
|
|
139
|
+
}
|
|
140
|
+
/** Installs ACP adapters for local agent CLIs that do not already have one. */
|
|
141
|
+
export async function installMissingAcpAdapters(pm, options, missing) {
|
|
142
|
+
const adapters = missing ?? await findMissingAcpAdapters();
|
|
143
|
+
const results = [];
|
|
144
|
+
for (const adapter of adapters) {
|
|
145
|
+
try {
|
|
146
|
+
await runPackageManager(pm, globalInstallArgs(pm, adapter.packageName), options);
|
|
147
|
+
results.push({ id: adapter.id, name: adapter.name, package: adapter.packageName, status: 'installed', error: null });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
results.push({
|
|
151
|
+
id: adapter.id,
|
|
152
|
+
name: adapter.name,
|
|
153
|
+
package: adapter.packageName,
|
|
154
|
+
status: 'failed',
|
|
155
|
+
error: adapterErrorMessage(error),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
123
161
|
function resolveDeps(overrides) {
|
|
124
162
|
return {
|
|
125
163
|
detectPackageManager,
|
|
126
164
|
runUpdate,
|
|
165
|
+
installAcpAdapters: (pm, options) => installMissingAcpAdapters(pm, options),
|
|
127
166
|
getVersion: getPackageVersion,
|
|
128
167
|
getLatestVersion: fetchLatestPublishedVersion,
|
|
129
168
|
log: logHumanReadable,
|
|
@@ -185,6 +224,7 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
185
224
|
'skillsInstallCommand',
|
|
186
225
|
'retiredSkills',
|
|
187
226
|
'retiredSkillsCleanup',
|
|
227
|
+
'acpAdapters',
|
|
188
228
|
],
|
|
189
229
|
},
|
|
190
230
|
examples: ['dino update', 'dino update --package-manager pnpm --format json'],
|
|
@@ -199,6 +239,7 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
199
239
|
deps.log(`updating ${PACKAGE_NAME} via ${pm}...`);
|
|
200
240
|
}
|
|
201
241
|
await deps.runUpdate(pm, { structuredOutput });
|
|
242
|
+
const acpAdapters = await deps.installAcpAdapters(pm, { structuredOutput });
|
|
202
243
|
const observedVersion = deps.getVersion();
|
|
203
244
|
const version = observedVersion !== previousVersion
|
|
204
245
|
? observedVersion
|
|
@@ -219,10 +260,19 @@ export function createUpdateCommandDescriptor(overrides = {}) {
|
|
|
219
260
|
skillsInstallCommand: skills.installCommand,
|
|
220
261
|
retiredSkills: RETIRED_SKILLS,
|
|
221
262
|
retiredSkillsCleanup: RETIRED_SKILLS_CLEANUP,
|
|
263
|
+
acpAdapters,
|
|
222
264
|
});
|
|
223
265
|
return;
|
|
224
266
|
}
|
|
225
267
|
deps.log(`done: ${version}`);
|
|
268
|
+
for (const adapter of acpAdapters) {
|
|
269
|
+
if (adapter.status === 'installed') {
|
|
270
|
+
deps.log(`installed ACP adapter ${adapter.package} for ${adapter.name}`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
deps.log(`warning: could not install ACP adapter ${adapter.package} for ${adapter.name} (${adapter.error})`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
226
276
|
printSkillsReminder(deps.log, version);
|
|
227
277
|
},
|
|
228
278
|
};
|
package/dist/config/types.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export type DinoxConfig = {
|
|
|
13
13
|
userInfoSyncedAt?: string;
|
|
14
14
|
/** Internal CAS token changed by every persisted login/logout transition. */
|
|
15
15
|
stateRevision?: string;
|
|
16
|
+
/** Stable id for QR login (`cli-` + uuid). Reused for the same account on this machine. */
|
|
17
|
+
deviceId?: string;
|
|
18
|
+
/** `c_user_device.id` from the last successful QR login. */
|
|
19
|
+
deviceBindingId?: string;
|
|
16
20
|
};
|
|
17
21
|
powersync?: {
|
|
18
22
|
/** PowerSync sync service endpoint (downloads/subscriptions). */
|
package/dist/daemon/ensure.js
CHANGED
|
@@ -31,6 +31,13 @@ function matchesRuntimeTokenBinding(info, version, config) {
|
|
|
31
31
|
async function ensureDaemonRunningUnlocked() {
|
|
32
32
|
const expectedVersion = getPackageVersion();
|
|
33
33
|
const baseConfig = resolveConfig(await loadConfig());
|
|
34
|
+
// A daemon cannot serve anything without a login; say so now instead of
|
|
35
|
+
// waiting for a launch that cannot become ready.
|
|
36
|
+
if (!baseConfig.auth.authorization) {
|
|
37
|
+
throw new DinoxError('Not logged in. Set DINOX_TOKEN or pipe a token to `dino auth login --token-stdin`.', {
|
|
38
|
+
code: 'AUTH_REQUIRED',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
34
41
|
const info = await getDaemonInfo();
|
|
35
42
|
if (info && matchesRuntimeTokenBinding(info, expectedVersion, baseConfig)) {
|
|
36
43
|
return;
|
package/dist/daemon/route.js
CHANGED
|
@@ -25,6 +25,8 @@ export async function routeRead(globals, daemonMethod, daemonParams, localRun) {
|
|
|
25
25
|
return (await sendDaemonRequest(daemonMethod, daemonParams));
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
|
+
if (error instanceof DinoxError && error.code === 'AUTH_REQUIRED')
|
|
29
|
+
throw error;
|
|
28
30
|
const message = error instanceof Error ? error.message : String(error);
|
|
29
31
|
throw new DinoxError(`Daemon execution failed: ${message}`, {
|
|
30
32
|
code: 'DAEMON_UNAVAILABLE',
|
package/dist/gateway/config.d.ts
CHANGED
|
@@ -32,16 +32,12 @@ export declare function addGateway(userId: string, input: {
|
|
|
32
32
|
cwd?: string;
|
|
33
33
|
env?: Record<string, string>;
|
|
34
34
|
}): Promise<GatewayDefinition>;
|
|
35
|
-
/**
|
|
36
|
-
* Hosts a detected agent once. An agent already hosted, matched by type (or, for
|
|
37
|
-
* gateways from before types were recorded, by executable name and arguments),
|
|
38
|
-
* is updated in place when its executable moved, e.g. after a Node upgrade.
|
|
39
|
-
*/
|
|
40
35
|
export declare function adoptDetectedAgent(userId: string, agent: {
|
|
41
36
|
id: string;
|
|
42
37
|
name: string;
|
|
43
38
|
command: string;
|
|
44
39
|
args: string[];
|
|
40
|
+
env?: Record<string, string>;
|
|
45
41
|
}): Promise<{
|
|
46
42
|
gateway: GatewayDefinition;
|
|
47
43
|
change: 'added' | 'updated' | 'unchanged';
|
package/dist/gateway/config.js
CHANGED
|
@@ -214,6 +214,19 @@ function sameLaunch(a, b) {
|
|
|
214
214
|
* gateways from before types were recorded, by executable name and arguments),
|
|
215
215
|
* is updated in place when its executable moved, e.g. after a Node upgrade.
|
|
216
216
|
*/
|
|
217
|
+
function mergeRequiredEnv(existing, required) {
|
|
218
|
+
if (!required)
|
|
219
|
+
return { env: existing, changed: false };
|
|
220
|
+
const env = { ...(existing ?? {}) };
|
|
221
|
+
let changed = false;
|
|
222
|
+
for (const [key, value] of Object.entries(required)) {
|
|
223
|
+
if (env[key] === undefined) {
|
|
224
|
+
env[key] = value;
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { env, changed };
|
|
229
|
+
}
|
|
217
230
|
export async function adoptDetectedAgent(userId, agent) {
|
|
218
231
|
const gateways = await loadGateways(userId);
|
|
219
232
|
const existing = gateways.find((gateway) => gateway.agentId === agent.id) ??
|
|
@@ -223,10 +236,17 @@ export async function adoptDetectedAgent(userId, agent) {
|
|
|
223
236
|
path.basename(gateway.command) === path.basename(agent.command) &&
|
|
224
237
|
JSON.stringify(gateway.args ?? []) === JSON.stringify(agent.args));
|
|
225
238
|
if (existing) {
|
|
226
|
-
|
|
239
|
+
const requiredEnv = mergeRequiredEnv(existing.env, agent.env);
|
|
240
|
+
if (existing.agentId === agent.id && sameLaunch(existing, agent) && !requiredEnv.changed) {
|
|
227
241
|
return { gateway: existing, change: 'unchanged' };
|
|
228
242
|
}
|
|
229
|
-
const updated = {
|
|
243
|
+
const updated = {
|
|
244
|
+
...existing,
|
|
245
|
+
agentId: agent.id,
|
|
246
|
+
command: agent.command,
|
|
247
|
+
args: agent.args,
|
|
248
|
+
...(requiredEnv.env ? { env: requiredEnv.env } : {}),
|
|
249
|
+
};
|
|
230
250
|
await saveGateways(userId, gateways.map((gateway) => (gateway.id === existing.id ? updated : gateway)));
|
|
231
251
|
return { gateway: updated, change: 'updated' };
|
|
232
252
|
}
|
|
@@ -240,6 +260,7 @@ export async function adoptDetectedAgent(userId, agent) {
|
|
|
240
260
|
agentId: agent.id,
|
|
241
261
|
command: agent.command,
|
|
242
262
|
args: agent.args,
|
|
263
|
+
...(agent.env ? { env: agent.env } : {}),
|
|
243
264
|
});
|
|
244
265
|
return { gateway, change: 'added' };
|
|
245
266
|
}
|
package/dist/gateway/detect.d.ts
CHANGED
|
@@ -7,14 +7,22 @@ export declare const KNOWN_ACP_AGENTS: ReadonlyArray<{
|
|
|
7
7
|
id: string;
|
|
8
8
|
name: string;
|
|
9
9
|
candidates: string[][];
|
|
10
|
-
/**
|
|
10
|
+
/** Passed to the agent process. Saved on the gateway when this launch is adopted. */
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
/** The agent's own CLI, and the global package that provides ACP when that CLI has none. */
|
|
11
13
|
adapter?: {
|
|
12
14
|
baseBinary: string;
|
|
13
|
-
|
|
15
|
+
packageName: string;
|
|
14
16
|
};
|
|
15
17
|
}>;
|
|
16
18
|
/** The known agent a manual launch command starts, by executable name and arguments. */
|
|
17
19
|
export declare function inferAgentId(command: string | undefined, args?: string[]): string | undefined;
|
|
20
|
+
/** A launch stayed up but never completed the ACP handshake. Further flags of that agent are not tried. */
|
|
21
|
+
export declare class AcpProbeTimeoutError extends Error {
|
|
22
|
+
constructor();
|
|
23
|
+
}
|
|
24
|
+
/** A quick failure can be a wrong flag. A timeout means that agent is not going to answer. */
|
|
25
|
+
export declare function shouldTryNextAcpCandidate(error: unknown): boolean;
|
|
18
26
|
export type DetectedAgent = {
|
|
19
27
|
id: string;
|
|
20
28
|
name: string;
|
|
@@ -27,14 +35,29 @@ export type DetectedAgent = {
|
|
|
27
35
|
error?: string;
|
|
28
36
|
/** Set when the agent's CLI is installed but its ACP adapter is not. */
|
|
29
37
|
installHint?: string;
|
|
38
|
+
/** Environment the handshake needed. Stored on the gateway when adopted. */
|
|
39
|
+
env?: Record<string, string>;
|
|
30
40
|
};
|
|
31
41
|
export declare function findExecutable(name: string, envPath?: string): Promise<string | null>;
|
|
32
42
|
/** Starts the agent, runs `initialize`, and stops it. Never leaves the process running. */
|
|
33
|
-
export declare function probeAcpAgent(command: string, args: string[], timeoutMs?: number): Promise<{
|
|
43
|
+
export declare function probeAcpAgent(command: string, args: string[], timeoutMs?: number, env?: Record<string, string>): Promise<{
|
|
34
44
|
agentName: string | null;
|
|
35
45
|
agentVersion: string | null;
|
|
36
46
|
}>;
|
|
37
47
|
/** Finds installed ACP agents; with `probe`, keeps only launch variants that handshake. */
|
|
38
48
|
export declare function detectAcpAgents(options?: {
|
|
39
49
|
probe?: boolean;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
/** Called as soon as one agent finishes, so a slow handshake does not hide the ones already done. */
|
|
52
|
+
onDetected?: (agent: DetectedAgent) => void;
|
|
40
53
|
}): Promise<DetectedAgent[]>;
|
|
54
|
+
export type MissingAcpAdapter = {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
packageName: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Agents whose own CLI is on PATH but none of the ACP launch commands are.
|
|
61
|
+
* Those need a global adapter package before a gateway can start them.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findMissingAcpAdapters(envPath?: string): Promise<MissingAcpAdapter[]>;
|