@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.
Files changed (58) hide show
  1. package/dist/cli.js +5 -2
  2. package/dist/commands/auth/index.d.ts +2 -7
  3. package/dist/commands/auth/index.js +26 -51
  4. package/dist/commands/doctor.d.ts +4 -1
  5. package/dist/commands/doctor.js +20 -1
  6. package/dist/commands/graph/index.js +2 -1
  7. package/dist/commands/notes/contentPatch.js +2 -1
  8. package/dist/commands/notes/export.js +3 -2
  9. package/dist/commands/runtime.d.ts +8 -0
  10. package/dist/commands/runtime.js +49 -1
  11. package/dist/commands/schema/globalOptions.d.ts +1 -0
  12. package/dist/commands/schema/globalOptions.js +1 -0
  13. package/dist/commands/schema/renderSkills.js +1 -0
  14. package/dist/commands/schema/spec.js +9 -1
  15. package/dist/commands/storage/repo/operations.js +3 -1
  16. package/dist/commands/sync.d.ts +2 -1
  17. package/dist/commands/sync.js +24 -5
  18. package/dist/daemon/client.js +32 -2
  19. package/dist/daemon/connectivity.d.ts +20 -0
  20. package/dist/daemon/connectivity.js +42 -0
  21. package/dist/daemon/ensure.js +48 -12
  22. package/dist/daemon/forward.d.ts +34 -0
  23. package/dist/daemon/forward.js +146 -0
  24. package/dist/daemon/fresh.d.ts +4 -0
  25. package/dist/daemon/fresh.js +10 -0
  26. package/dist/daemon/index.d.ts +11 -6
  27. package/dist/daemon/index.js +90 -25
  28. package/dist/daemon/launch.d.ts +13 -1
  29. package/dist/daemon/launch.js +31 -2
  30. package/dist/daemon/lifecycle.d.ts +3 -1
  31. package/dist/daemon/lifecycle.js +27 -3
  32. package/dist/daemon/pid.d.ts +8 -0
  33. package/dist/daemon/pid.js +20 -0
  34. package/dist/daemon/route.js +2 -1
  35. package/dist/daemon/server.d.ts +44 -0
  36. package/dist/daemon/server.js +175 -3
  37. package/dist/daemon/supervise.d.ts +6 -6
  38. package/dist/daemon/supervise.js +12 -10
  39. package/dist/daemon/supervisor.d.ts +21 -5
  40. package/dist/daemon/supervisor.js +125 -6
  41. package/dist/daemon/types.d.ts +16 -0
  42. package/dist/dinox.js +41 -64
  43. package/dist/gateway/mcpCommandTools.d.ts +9 -0
  44. package/dist/gateway/mcpCommandTools.js +40 -10
  45. package/dist/powersync/runtime.d.ts +2 -0
  46. package/dist/powersync/runtime.js +3 -1
  47. package/dist/requests/store.d.ts +10 -0
  48. package/dist/requests/store.js +190 -0
  49. package/dist/runCli.d.ts +2 -0
  50. package/dist/runCli.js +64 -0
  51. package/dist/utils/argValue.js +2 -1
  52. package/dist/utils/errorCodes.d.ts +20 -0
  53. package/dist/utils/errorCodes.js +5 -0
  54. package/dist/utils/output.d.ts +2 -0
  55. package/dist/utils/output.js +31 -7
  56. package/dist/utils/requestContext.d.ts +24 -0
  57. package/dist/utils/requestContext.js +13 -0
  58. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
1
  import { request as httpRequest } from 'node:http';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
+ import { getPackageVersion } from '../utils/version.js';
4
5
  import { remainingDeadlineMs } from '../utils/deadline.js';
5
6
  import { DinoxError, wireErrorCode } from '../utils/errors.js';
6
7
  const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
@@ -34,6 +35,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
34
35
  };
35
36
  return new Promise((resolve, reject) => {
36
37
  let settled = false;
38
+ let connected = false;
37
39
  let deadlineTimer;
38
40
  const finalize = (error, value) => {
39
41
  if (settled)
@@ -43,6 +45,8 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
43
45
  clearTimeout(deadlineTimer);
44
46
  }
45
47
  if (error) {
48
+ if (error instanceof Error)
49
+ Object.assign(error, { requestWritten: connected });
46
50
  reject(error);
47
51
  return;
48
52
  }
@@ -66,7 +70,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
66
70
  let totalBytes = 0;
67
71
  res.on('data', (chunk) => {
68
72
  totalBytes += chunk.length;
69
- if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
73
+ if (method !== 'command.execute' && totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
70
74
  req.destroy(new Error('Daemon response too large'));
71
75
  return;
72
76
  }
@@ -92,10 +96,19 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
92
96
  }
93
97
  });
94
98
  });
99
+ req.on('socket', socket => {
100
+ if (!socket.connecting)
101
+ connected = true;
102
+ socket.once('connect', () => { connected = true; });
103
+ });
95
104
  req.on('timeout', () => {
96
105
  req.destroy(new Error('Daemon request timed out'));
97
106
  });
98
107
  req.on('error', (error) => finalize(error));
108
+ req.on('response', res => {
109
+ res.on('aborted', () => finalize(new Error('Daemon response interrupted')));
110
+ res.on('error', error => finalize(error));
111
+ });
99
112
  deadlineTimer = setTimeout(() => {
100
113
  req.destroy(new Error('Daemon request timed out'));
101
114
  }, timeoutMs);
@@ -282,6 +295,16 @@ export async function waitForFreshDaemon(expectedVersion, expectedUserId, expect
282
295
  }
283
296
  }
284
297
  }
298
+ /** Daemon log context for a stop request; never carries credentials. */
299
+ function redactRequesterCommand(argv) {
300
+ return argv.join(' ')
301
+ .replace(/(auth\s+login)\b[\s\S]*/i, '$1 [REDACTED]')
302
+ .replace(/((?:--)?[\w-]*(?:token|authorization|secret|password|api[-_]?key)[\w-]*[=\s]+)(?:Bearer\s+)?[^\s]+/gi, '$1[REDACTED]')
303
+ .replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]')
304
+ .replace(/\beyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){1,2}\b/g, '[REDACTED]')
305
+ .replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
306
+ .slice(0, 200);
307
+ }
285
308
  /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
286
309
  export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
287
310
  if (!expectedInstanceId) {
@@ -290,7 +313,14 @@ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_00
290
313
  recoverable: true,
291
314
  });
292
315
  }
293
- const result = await sendDaemonRequest('shutdown', { expectedInstanceId }, { timeoutMs });
316
+ const result = await sendDaemonRequest('shutdown', {
317
+ expectedInstanceId,
318
+ requester: {
319
+ pid: process.pid,
320
+ version: getPackageVersion(),
321
+ command: redactRequesterCommand(process.argv.slice(2)),
322
+ },
323
+ }, { timeoutMs });
294
324
  if (!result
295
325
  || typeof result !== 'object'
296
326
  || result.stopping !== true
@@ -0,0 +1,20 @@
1
+ /** How often the daemon checks for a wake from sleep or a network change. */
2
+ export declare const CONNECTIVITY_CHECK_INTERVAL_MS = 10000;
3
+ /** A tick this late means the machine was asleep (timers do not run during sleep). */
4
+ export declare const WAKE_DRIFT_THRESHOLD_MS = 30000;
5
+ export type ConnectivityChange = 'wake' | 'network-change';
6
+ /** Addresses of the non-internal interfaces; changes on Wi-Fi switch, VPN, or link loss. */
7
+ export declare function networkSignature(): string;
8
+ /**
9
+ * Detects the events after which a long-lived sync stream may be a dead
10
+ * connection that still reports "connected": a wake from sleep and a change
11
+ * of network. Call `tick()` every CONNECTIVITY_CHECK_INTERVAL_MS.
12
+ */
13
+ export declare function createConnectivityWatcher(options: {
14
+ onChange: (change: ConnectivityChange) => void;
15
+ now?: () => number;
16
+ signature?: () => string;
17
+ intervalMs?: number;
18
+ }): {
19
+ tick: () => void;
20
+ };
@@ -0,0 +1,42 @@
1
+ import os from 'node:os';
2
+ /** How often the daemon checks for a wake from sleep or a network change. */
3
+ export const CONNECTIVITY_CHECK_INTERVAL_MS = 10_000;
4
+ /** A tick this late means the machine was asleep (timers do not run during sleep). */
5
+ export const WAKE_DRIFT_THRESHOLD_MS = 30_000;
6
+ /** Addresses of the non-internal interfaces; changes on Wi-Fi switch, VPN, or link loss. */
7
+ export function networkSignature() {
8
+ const addresses = [];
9
+ for (const [name, entries] of Object.entries(os.networkInterfaces())) {
10
+ for (const entry of entries ?? []) {
11
+ if (!entry.internal)
12
+ addresses.push(`${name}:${entry.address}`);
13
+ }
14
+ }
15
+ return addresses.sort().join(',');
16
+ }
17
+ /**
18
+ * Detects the events after which a long-lived sync stream may be a dead
19
+ * connection that still reports "connected": a wake from sleep and a change
20
+ * of network. Call `tick()` every CONNECTIVITY_CHECK_INTERVAL_MS.
21
+ */
22
+ export function createConnectivityWatcher(options) {
23
+ const now = options.now ?? Date.now;
24
+ const signature = options.signature ?? networkSignature;
25
+ const intervalMs = options.intervalMs ?? CONNECTIVITY_CHECK_INTERVAL_MS;
26
+ let lastTickAt = now();
27
+ let lastSignature = signature();
28
+ return {
29
+ tick() {
30
+ const tickAt = now();
31
+ const drift = tickAt - lastTickAt - intervalMs;
32
+ lastTickAt = tickAt;
33
+ const currentSignature = signature();
34
+ const networkChanged = currentSignature !== lastSignature;
35
+ lastSignature = currentSignature;
36
+ if (drift >= WAKE_DRIFT_THRESHOLD_MS)
37
+ options.onChange('wake');
38
+ else if (networkChanged)
39
+ options.onChange('network-change');
40
+ },
41
+ };
42
+ }
@@ -1,9 +1,12 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import { getDaemonInfo, isDaemonFresh, waitForFreshDaemon } from './client.js';
2
- import { stopDaemonLocked } from './index.js';
3
+ import { matchesRuntimeTokenBinding } from './fresh.js';
4
+ import { restartDaemonLocked, stopDaemonLocked } from './index.js';
3
5
  import { DEFAULT_DAEMON_PORT } from './types.js';
4
- import { launchDaemon } from './launch.js';
6
+ import { daemonServiceSpec, foreignServiceOwner, launchDaemon, usableDaemonService } from './launch.js';
5
7
  import { getDaemonLogPath } from './log.js';
6
- import { acquireDaemonStartLock, } from './pid.js';
8
+ import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from './pid.js';
9
+ import { getDaemonService } from './supervisor.js';
7
10
  import { loadConfig } from '../config/store.js';
8
11
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
9
12
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
@@ -20,13 +23,22 @@ async function hydrateDaemonIdentity(baseConfig) {
20
23
  runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
21
24
  };
22
25
  }
23
- function matchesRuntimeTokenBinding(info, version, config) {
24
- return Boolean(config.auth.authorization
25
- && info.authenticated
26
- && info.version === version
27
- && info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
28
- && info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
29
- && (!config.auth.userId || info.userId === config.auth.userId));
26
+ /**
27
+ * Refuses to restart or rewrite a service another dino install owns. The data
28
+ * command then runs in-process; `dino daemon install` (or start/restart)
29
+ * switches the service to this CLI.
30
+ */
31
+ async function assertServiceNotForeign() {
32
+ const service = await usableDaemonService();
33
+ const owner = service ? foreignServiceOwner(service) : null;
34
+ if (!service || !owner)
35
+ return;
36
+ throw new DinoxError(`The ${service.kind} daemon service belongs to another dino install (${owner}).`, {
37
+ code: 'DAEMON_SERVICE_FOREIGN',
38
+ recoverable: true,
39
+ details: { service: service.kind, owner, definitionPath: service.definitionPath },
40
+ suggestion: 'Run `dino daemon install` with the dino you want the service to use.',
41
+ });
30
42
  }
31
43
  async function ensureDaemonRunningUnlocked() {
32
44
  const expectedVersion = getPackageVersion();
@@ -43,15 +55,22 @@ async function ensureDaemonRunningUnlocked() {
43
55
  return;
44
56
  }
45
57
  const expectedIdentity = await hydrateDaemonIdentity(baseConfig);
58
+ if (info && isDaemonFresh(info, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
59
+ return;
60
+ }
61
+ await assertServiceNotForeign();
46
62
  if (info) {
47
63
  // A daemon is reachable — make sure it is the *right* one. It holds a long-lived
48
64
  // PowerSync connection bound to whatever account/version it started with, so after
49
65
  // `auth login` (new userId) or `dino update` (new version) the running daemon would
50
66
  // otherwise keep serving stale or wrong-account data. Restart it when it drifts.
51
- if (isDaemonFresh(info, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
67
+ printHumanReadableWarning('Restarting daemon (version or account changed)...');
68
+ const service = getDaemonService();
69
+ if (service?.isInstalled() && info.supervisor === service.kind
70
+ && service.isCurrent(daemonServiceSpec(service, DEFAULT_DAEMON_PORT))) {
71
+ await restartDaemonLocked(DEFAULT_DAEMON_PORT, { quiet: true });
52
72
  return;
53
73
  }
54
- printHumanReadableWarning('Restarting daemon (version or account changed)...');
55
74
  await stopDaemonLocked().catch((error) => {
56
75
  const message = error instanceof Error ? error.message : String(error);
57
76
  printHumanReadableWarning(`warning: failed to stop stale daemon: ${message}`);
@@ -65,6 +84,23 @@ async function ensureDaemonRunningUnlocked() {
65
84
  }
66
85
  }
67
86
  else {
87
+ // A live process that owns the socket but does not answer is hung; only
88
+ // launchd / systemd can replace it safely (they know the exact process).
89
+ const pid = readPid();
90
+ const service = getDaemonService();
91
+ if (pid && isProcessRunning(pid.pid) && existsSync(getDaemonSocketPath())
92
+ && service?.isInstalled() && service.kind !== 'schtasks') {
93
+ printHumanReadableWarning('warning: daemon is not responding; restarting the service');
94
+ await service.restart();
95
+ if (await waitForFreshDaemon(expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint, DAEMON_READY_TIMEOUT_MS)) {
96
+ return;
97
+ }
98
+ throw new DinoxError('The daemon service is still not responding after a restart.', {
99
+ code: 'DAEMON_START_FAILED',
100
+ details: { service: service.kind, logPath: getDaemonLogPath() },
101
+ suggestion: `Inspect ${getDaemonLogPath()} for the daemon's startup error.`,
102
+ });
103
+ }
68
104
  printHumanReadableWarning('Starting daemon...');
69
105
  }
70
106
  const launch = await launchDaemon(DEFAULT_DAEMON_PORT);
@@ -0,0 +1,34 @@
1
+ import { loadConfig } from '../config/store.js';
2
+ import { resolveConfig } from '../config/resolve.js';
3
+ import { printHumanReadableWarning } from '../utils/output.js';
4
+ import { getPackageVersion } from '../utils/version.js';
5
+ import { sendDaemonRequest } from './client.js';
6
+ import { envTokenOverridesLogin } from './launch.js';
7
+ import type { CommandExecuteResult } from './types.js';
8
+ /**
9
+ * Where a CLI invocation runs. Help, version, unknown words and every command
10
+ * that manages the local install (auth, config, daemon, gateway, doctor, ...)
11
+ * stay local; `meta schema` is introspection and needs no database.
12
+ */
13
+ export declare function commandExecutionTarget(input: string[]): 'daemon' | 'local';
14
+ declare const forwardDependencies: {
15
+ loadConfig: typeof loadConfig;
16
+ resolveConfig: typeof resolveConfig;
17
+ isDaemonFresh: (config: ReturnType<typeof resolveConfig>, version: string) => Promise<boolean>;
18
+ ensureDaemonRunning: () => Promise<void>;
19
+ sendDaemonRequest: typeof sendDaemonRequest;
20
+ envTokenOverridesLogin: typeof envTokenOverridesLogin;
21
+ version: typeof getPackageVersion;
22
+ cwd: () => string;
23
+ env: NodeJS.ProcessEnv;
24
+ note: typeof printHumanReadableWarning;
25
+ };
26
+ export type ForwardDependencies = typeof forwardDependencies;
27
+ /**
28
+ * Runs a data command inside the daemon. Returns null when it should run
29
+ * in-process instead: forwarding is disabled or impossible, or failed before
30
+ * the daemon could have received the request. A failure after that point
31
+ * throws DAEMON_UNAVAILABLE, never falls back, so a write is not applied twice.
32
+ */
33
+ export declare function tryForwardToDaemon(argv: string[], overrides?: Partial<ForwardDependencies>): Promise<CommandExecuteResult | null>;
34
+ export {};
@@ -0,0 +1,146 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { loadConfig } from '../config/store.js';
3
+ import { resolveConfig } from '../config/resolve.js';
4
+ import { DinoxError } from '../utils/errors.js';
5
+ import { printHumanReadableWarning } from '../utils/output.js';
6
+ import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
7
+ import { getPackageVersion } from '../utils/version.js';
8
+ import { AUTO_REQUEST_ID_PREFIX, requestIdFromArgv } from '../requests/store.js';
9
+ import { matchesRuntimeTokenBinding } from './fresh.js';
10
+ import { getDaemonInfo, sendDaemonRequest } from './client.js';
11
+ import { envTokenOverridesLogin } from './launch.js';
12
+ /** Top-level commands that only need the database; they run inside the daemon. */
13
+ const DAEMON_COMMAND_GROUPS = new Set([
14
+ 'note', 'tag', 'box', 'todo', 'view', 'prop', 'template', 'prompt', 'graph', 'storage', 'meta', 'sync',
15
+ ]);
16
+ /**
17
+ * Where a CLI invocation runs. Help, version, unknown words and every command
18
+ * that manages the local install (auth, config, daemon, gateway, doctor, ...)
19
+ * stay local; `meta schema` is introspection and needs no database.
20
+ */
21
+ export function commandExecutionTarget(input) {
22
+ const argv = input[0] === '--' ? input.slice(1) : input;
23
+ if (argv.some(arg => ['--help', '-h', '--version', '-V'].includes(arg)))
24
+ return 'local';
25
+ const words = [];
26
+ for (let i = 0; i < argv.length; i++) {
27
+ const arg = argv[i];
28
+ if (arg === '--') {
29
+ words.push(...argv.slice(i + 1));
30
+ break;
31
+ }
32
+ if (arg === '--format' || arg === '--sync-timeout' || arg === '--request-id') {
33
+ i++;
34
+ continue;
35
+ }
36
+ if (arg.startsWith('-'))
37
+ continue;
38
+ words.push(arg);
39
+ }
40
+ const group = words[0] ?? '';
41
+ if (!DAEMON_COMMAND_GROUPS.has(group) || (group === 'meta' && words[1] === 'schema'))
42
+ return 'local';
43
+ return 'daemon';
44
+ }
45
+ const forwardDependencies = {
46
+ loadConfig,
47
+ resolveConfig,
48
+ isDaemonFresh: async (config, version) => {
49
+ const info = await getDaemonInfo();
50
+ return Boolean(info && matchesRuntimeTokenBinding(info, version, config));
51
+ },
52
+ ensureDaemonRunning: async () => {
53
+ await (await import('./ensure.js')).ensureDaemonRunning();
54
+ },
55
+ sendDaemonRequest,
56
+ envTokenOverridesLogin,
57
+ version: getPackageVersion,
58
+ cwd: () => process.cwd(),
59
+ env: process.env,
60
+ note: printHumanReadableWarning,
61
+ };
62
+ /**
63
+ * Runs a data command inside the daemon. Returns null when it should run
64
+ * in-process instead: forwarding is disabled or impossible, or failed before
65
+ * the daemon could have received the request. A failure after that point
66
+ * throws DAEMON_UNAVAILABLE, never falls back, so a write is not applied twice.
67
+ */
68
+ export async function tryForwardToDaemon(argv, overrides = {}) {
69
+ const deps = { ...forwardDependencies, ...overrides };
70
+ if (commandExecutionTarget(argv) !== 'daemon' || /^(0|false|off|no)$/i.test(deps.env.DINOX_DAEMON ?? ''))
71
+ return null;
72
+ const fallback = (error) => {
73
+ const reason = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, ' ');
74
+ deps.note(`note: daemon unavailable, running in-process (${reason})`);
75
+ return null;
76
+ };
77
+ const suppliedRequestId = requestIdFromArgv(argv);
78
+ const requestId = suppliedRequestId ?? `${AUTO_REQUEST_ID_PREFIX}${randomUUID()}`;
79
+ let timeoutMs;
80
+ let config;
81
+ const ensureReady = async () => {
82
+ if (!await deps.isDaemonFresh(config, deps.version()))
83
+ await deps.ensureDaemonRunning();
84
+ };
85
+ try {
86
+ const raw = await deps.loadConfig();
87
+ if (deps.envTokenOverridesLogin(raw, deps.env) || !raw.auth?.authorization)
88
+ return null;
89
+ let timeout;
90
+ for (let i = 0; i < argv.length; i++) {
91
+ if (argv[i] === '--')
92
+ break;
93
+ if (argv[i] === '--sync-timeout')
94
+ timeout = argv[++i];
95
+ else if (argv[i]?.startsWith('--sync-timeout='))
96
+ timeout = argv[i].slice(15);
97
+ }
98
+ config = deps.resolveConfig(raw);
99
+ timeoutMs = (parseSyncTimeoutMs(timeout) ?? config.sync.timeoutMs) + 60_000;
100
+ await ensureReady();
101
+ }
102
+ catch (error) {
103
+ if (error instanceof DinoxError && error.code === 'AUTH_REQUIRED')
104
+ return null;
105
+ return fallback(error);
106
+ }
107
+ const execute = async () => {
108
+ const result = await deps.sendDaemonRequest('command.execute', {
109
+ argv, cwd: deps.cwd(), clientVersion: deps.version(), requestId,
110
+ }, { timeoutMs });
111
+ if (!result || typeof result.stdout !== 'string' || typeof result.stderr !== 'string' || !Number.isInteger(result.exitCode)) {
112
+ throw new Error('Invalid command.execute response');
113
+ }
114
+ return result;
115
+ };
116
+ const requestError = (error) => error instanceof DinoxError
117
+ && ['REQUEST_ID_CONFLICT', 'REQUEST_IN_PROGRESS', 'REQUEST_INTERRUPTED', 'INVALID_ARGUMENT'].includes(error.code);
118
+ try {
119
+ return await execute();
120
+ }
121
+ catch (error) {
122
+ if (requestError(error))
123
+ throw error;
124
+ if ((error instanceof Error && /^Unknown method: command.execute$/i.test(error.message))
125
+ || error?.requestWritten === false) {
126
+ return fallback(error);
127
+ }
128
+ // Once delivery is uncertain, neither retry failures nor startup failures
129
+ // may fall back to in-process execution.
130
+ try {
131
+ await ensureReady();
132
+ return await execute();
133
+ }
134
+ catch (retryError) {
135
+ if (requestError(retryError))
136
+ throw retryError;
137
+ throw new DinoxError('Daemon command response unavailable; the command may have executed.', {
138
+ code: 'DAEMON_UNAVAILABLE', cause: retryError, recoverable: true,
139
+ details: { requestId },
140
+ suggestion: suppliedRequestId
141
+ ? `Rerun the exact same command with --request-id ${requestId} to fetch the outcome.`
142
+ : 'Verify the result before retrying the command.',
143
+ });
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,4 @@
1
+ import type { ResolvedDinoxConfig } from '../config/types.js';
2
+ import type { getDaemonInfo } from './client.js';
3
+ /** Checks the inexpensive, already-resolved runtime binding. */
4
+ export declare function matchesRuntimeTokenBinding(info: NonNullable<Awaited<ReturnType<typeof getDaemonInfo>>>, version: string, config: ResolvedDinoxConfig): boolean;
@@ -0,0 +1,10 @@
1
+ import { fingerprintDaemonRuntimeConfig } from '../config/resolve.js';
2
+ /** Checks the inexpensive, already-resolved runtime binding. */
3
+ export function matchesRuntimeTokenBinding(info, version, config) {
4
+ return Boolean(config.auth.authorization
5
+ && info.authenticated
6
+ && info.version === version
7
+ && info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
8
+ && info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
9
+ && (!config.auth.userId || info.userId === config.auth.userId));
10
+ }
@@ -14,6 +14,8 @@ type DaemonStartResult = {
14
14
  type DaemonStartOptions = {
15
15
  quiet?: boolean;
16
16
  timeoutMs?: number;
17
+ /** Rewrite a service definition another dino install owns (explicit start/restart only). */
18
+ takeover?: boolean;
17
19
  };
18
20
  type DaemonStopResult = {
19
21
  stopped: boolean;
@@ -38,9 +40,7 @@ export declare function startDaemonLocked(port: number, detached: boolean, optio
38
40
  /** Caller must hold the daemon start lock. */
39
41
  export declare function stopDaemonLocked(): Promise<DaemonStopResult>;
40
42
  export declare function stopDaemon(): Promise<DaemonStopResult>;
41
- export declare function restartDaemonLocked(port?: number, options?: {
42
- quiet?: boolean;
43
- }): Promise<{
43
+ export declare function restartDaemonLocked(port?: number, options?: DaemonStartOptions): Promise<{
44
44
  stopped: boolean;
45
45
  running: boolean;
46
46
  port: number;
@@ -50,9 +50,7 @@ export declare function restartDaemonLocked(port?: number, options?: {
50
50
  alreadyRunning: boolean;
51
51
  service: DaemonServiceKind | null;
52
52
  }>;
53
- export declare function restartDaemon(port?: number, options?: {
54
- quiet?: boolean;
55
- }): Promise<{
53
+ export declare function restartDaemon(port?: number, options?: DaemonStartOptions): Promise<{
56
54
  stopped: boolean;
57
55
  running: boolean;
58
56
  port: number;
@@ -70,6 +68,13 @@ export declare function restartDaemon(port?: number, options?: {
70
68
  export declare function installDaemonService(port?: number): Promise<DaemonInstallResult>;
71
69
  /** Stops the managed daemon and removes its service definition. */
72
70
  export declare function uninstallDaemonService(): Promise<DaemonUninstallResult>;
71
+ export declare function waitForSavedDaemonLogin(overrides?: Partial<{
72
+ hasAuth: () => Promise<boolean>;
73
+ watchConfig: (check: () => void) => () => void;
74
+ exit: (code: number) => void;
75
+ onSignal: (handler: () => void) => () => void;
76
+ pollMs: number;
77
+ }>): Promise<void>;
73
78
  export declare const daemonCommandGroup: CommandGroupDescriptor;
74
79
  export declare function registerDaemonCommands(program: Command): void;
75
80
  export {};