@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
package/dist/cli.js CHANGED
@@ -21,8 +21,9 @@ import { registerTagsCommand } from './commands/tags/index.js';
21
21
  import { registerTemplateCommands } from './commands/templates/index.js';
22
22
  import { registerTodoCommands } from './commands/todo/index.js';
23
23
  import { getPackageVersion } from './utils/version.js';
24
+ import { validateRequestId } from './requests/store.js';
24
25
  import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
25
- import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
26
+ import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
26
27
  export function createCli() {
27
28
  clearRequestedStructuredOutputFormat();
28
29
  const program = new Command();
@@ -33,9 +34,10 @@ export function createCli() {
33
34
  .version(getPackageVersion())
34
35
  .showHelpAfterError()
35
36
  .configureOutput({
37
+ writeOut: writeToStdout,
36
38
  writeErr: (text) => {
37
39
  if (!structuredCommanderErrorWritten) {
38
- process.stderr.write(text);
40
+ writeToStderr(text);
39
41
  }
40
42
  },
41
43
  outputError: (text, write) => {
@@ -68,6 +70,7 @@ export function createCli() {
68
70
  .option('--offline', 'skip connect/sync and only use local cache')
69
71
  .option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
70
72
  .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
73
+ .option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
71
74
  .option('--verbose', 'enable verbose logging');
72
75
  program.hook('preAction', (_thisCommand, actionCommand) => {
73
76
  const options = actionCommand.optsWithGlobals?.() ?? {};
@@ -3,24 +3,19 @@ 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
- import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot } from '../../powersync/runtime.js';
9
- import { runSingleSyncPass } from '../../powersync/syncPass.js';
10
8
  import type { CommandGroupDescriptor } from '../descriptor/types.js';
11
9
  import { printHumanReadableWarning } from '../../utils/output.js';
12
10
  export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
13
11
  type AuthLoginDependencies = {
14
12
  acquireDaemonStartLock: typeof acquireDaemonStartLock;
13
+ acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
15
14
  loadConfig: typeof loadConfig;
16
15
  resolveConfig: typeof resolveConfig;
17
16
  fetchCredentials: typeof fetchCredentialsWithAuthorization;
18
17
  fetchUserInfo: typeof fetchUserInfoWithAuthorization;
19
18
  suspendDaemon: typeof suspendDaemonForExclusiveAccessLocked;
20
- connectPowerSync: typeof connectPowerSync;
21
- runSingleSyncPass: typeof runSingleSyncPass;
22
- getStatusSnapshot: typeof getStatusSnapshot;
23
- closePowerSyncAndReleaseOwner: typeof closePowerSyncAndReleaseOwner;
24
19
  updateConfig: typeof updateConfig;
25
20
  restoreDaemon: typeof restoreSuspendedDaemonLocked;
26
21
  printWarning: typeof printHumanReadableWarning;
@@ -10,11 +10,10 @@ 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
- import { acquirePowerSyncOwnerLease, closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, } from '../../powersync/runtime.js';
17
- import { runSingleSyncPass } from '../../powersync/syncPass.js';
16
+ import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
18
17
  import { registerCommandGroup } from '../descriptor/register.js';
19
18
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
20
19
  import { normalizeAuthToken, readEnvAuthToken } from '../../utils/authToken.js';
@@ -23,6 +22,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
23
22
  import { redactAuthorization } from '../../utils/redact.js';
24
23
  import { withAsyncRelease } from '../../utils/asyncRelease.js';
25
24
  import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
25
+ import { assertStdinAvailable } from '../../utils/requestContext.js';
26
26
  const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
27
27
  const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
28
28
  const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
@@ -81,6 +81,7 @@ export async function releaseOwnerLeases(releases) {
81
81
  return errors;
82
82
  }
83
83
  async function readAuthTokenFromStdin() {
84
+ assertStdinAvailable();
84
85
  if (process.stdin.isTTY) {
85
86
  throw new DinoxError('--token-stdin requires piped stdin', {
86
87
  code: 'USAGE_ERROR',
@@ -101,15 +102,12 @@ async function readAuthTokenFromStdin() {
101
102
  }
102
103
  const authLoginDependencies = {
103
104
  acquireDaemonStartLock,
105
+ acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
104
106
  loadConfig,
105
107
  resolveConfig,
106
108
  fetchCredentials: fetchCredentialsWithAuthorization,
107
109
  fetchUserInfo: fetchUserInfoWithAuthorization,
108
110
  suspendDaemon: suspendDaemonForExclusiveAccessLocked,
109
- connectPowerSync,
110
- runSingleSyncPass,
111
- getStatusSnapshot,
112
- closePowerSyncAndReleaseOwner,
113
111
  updateConfig,
114
112
  restoreDaemon: restoreSuspendedDaemonLocked,
115
113
  printWarning: printHumanReadableWarning,
@@ -122,7 +120,15 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
122
120
  const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
123
121
  let previousConfig = null;
124
122
  let suspended = null;
125
- let restoreAllowed = true;
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
+ };
126
132
  let lifecycleLockReleaseError;
127
133
  let outcome;
128
134
  try {
@@ -144,38 +150,8 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
144
150
  });
145
151
  const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
146
152
  const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
153
+ releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
147
154
  suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
148
- const { db, dbPath, stale, releaseOwner } = await dependencies.connectPowerSync({
149
- config: verifiedResolved,
150
- offline: false,
151
- timeoutMs,
152
- });
153
- let syncOutcome;
154
- try {
155
- await dependencies.runSingleSyncPass(db, timeoutMs);
156
- syncOutcome = {
157
- ok: true,
158
- lastSyncedAt: dependencies.getStatusSnapshot(db).lastSyncedAt,
159
- };
160
- }
161
- catch (error) {
162
- syncOutcome = { ok: false, error };
163
- }
164
- let cleanupError;
165
- try {
166
- await dependencies.closePowerSyncAndReleaseOwner(db, releaseOwner);
167
- }
168
- catch (error) {
169
- cleanupError = error;
170
- restoreAllowed = false;
171
- dependencies.printWarning(`warning: failed to close the login verification runtime safely: ${authLoginErrorMessage(error)}`);
172
- }
173
- if (!syncOutcome.ok) {
174
- throw syncOutcome.error;
175
- }
176
- if (cleanupError) {
177
- throw cleanupError;
178
- }
179
155
  await dependencies.updateConfig((current) => {
180
156
  if (!persistedAuthStateMatches(current, previousConfig)) {
181
157
  throw new DinoxError('Authentication changed while login was being verified.', {
@@ -186,22 +162,26 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
186
162
  }
187
163
  return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
188
164
  }, { timeoutMs });
165
+ await releaseIntent();
189
166
  outcome = {
190
167
  ok: true,
191
168
  value: {
192
169
  ok: true,
193
170
  verified: true,
194
171
  endpoint: verifiedResolved.powersync.endpoint,
195
- dbPath,
196
- stale,
197
- lastSyncedAt: syncOutcome.lastSyncedAt,
172
+ dbPath: getPowerSyncDbPath(identity.userId),
173
+ stale: true,
174
+ lastSyncedAt: null,
198
175
  userId: identity.userId,
199
176
  },
200
177
  };
201
178
  }
202
179
  catch (error) {
203
180
  outcome = { ok: false, error };
204
- if (previousConfig && suspended && restoreAllowed) {
181
+ await releaseIntent().catch((releaseError) => {
182
+ dependencies.printWarning(`warning: failed to release daemon intent lock: ${authLoginErrorMessage(releaseError)}`);
183
+ });
184
+ if (previousConfig && suspended) {
205
185
  try {
206
186
  const current = await dependencies.loadConfig();
207
187
  if (persistedAuthStateMatches(current, previousConfig)) {
@@ -315,12 +295,7 @@ async function runAuthLogin(context) {
315
295
  printHumanReadableLine(`- endpoint: ${result.endpoint}`);
316
296
  printHumanReadableLine(`- db: ${result.dbPath}`);
317
297
  printHumanReadableLine(`- userId: ${result.userId}`);
318
- if (result.lastSyncedAt) {
319
- printHumanReadableLine(`- lastSyncedAt: ${result.lastSyncedAt}`);
320
- }
321
- if (result.stale) {
322
- printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
323
- }
298
+ printHumanReadableLine('- initial sync continues in the background; run `dino sync` to wait for it');
324
299
  const serviceNote = describeDaemonServiceAutoInstall(daemonService);
325
300
  if (serviceNote)
326
301
  printHumanReadableWarning(serviceNote);
@@ -567,7 +542,7 @@ export const authCommandGroup = {
567
542
  commands: [
568
543
  {
569
544
  name: 'login',
570
- description: 'Log in by scanning a QR code, or save a token and verify PowerSync connectivity',
545
+ description: 'Log in by scanning a QR code or with a token; the initial sync continues in the background',
571
546
  arguments: [{ syntax: '[token]', description: 'Login token (omit to scan a QR code, or when using --token-stdin)' }],
572
547
  options: [{ flags: '--token-stdin', description: 'Read the login token from piped stdin instead of argv', type: 'boolean' }],
573
548
  schema: {
@@ -577,7 +552,7 @@ export const authCommandGroup = {
577
552
  globalOptions: AUTH_LOGIN_GLOBAL_OPTIONS,
578
553
  output: {
579
554
  type: 'structured',
580
- summary: 'Login verification result',
555
+ summary: 'Login verification result (initial sync is not awaited)',
581
556
  fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId', 'daemonService'],
582
557
  },
583
558
  examples: [
@@ -5,7 +5,7 @@ import { resolveConfig } from '../config/resolve.js';
5
5
  import type { ResolvedDinoxConfig } from '../config/types.js';
6
6
  import { shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
7
7
  import { restartDaemonLocked } from '../daemon/index.js';
8
- import { acquireDaemonStartLock } from '../daemon/pid.js';
8
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../daemon/pid.js';
9
9
  import type { DaemonPidInfo } from '../daemon/pid.js';
10
10
  import { type DaemonPingResult } from '../daemon/types.js';
11
11
  import { connectPowerSync } from '../powersync/runtime.js';
@@ -89,6 +89,8 @@ type DoctorRuntime = {
89
89
  daemonBefore: DoctorDaemonSnapshot;
90
90
  daemonWasStopped: boolean;
91
91
  releaseDaemonStartLock: () => Promise<void>;
92
+ /** Held while doctor owns the database; see acquireDaemonExclusiveIntentLock. */
93
+ releaseIntentLock?: () => Promise<void>;
92
94
  daemonStartLockReleased: boolean;
93
95
  released: boolean;
94
96
  };
@@ -102,6 +104,7 @@ type DoctorPreparationDependencies = {
102
104
  resolveConfig: typeof resolveConfig;
103
105
  hydrateAuthIdentity: typeof hydrateResolvedAuthIdentity;
104
106
  acquireDaemonStartLock: typeof acquireDaemonStartLock;
107
+ acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
105
108
  captureDaemonSnapshot: typeof captureDaemonSnapshot;
106
109
  shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
107
110
  connectPowerSync: typeof connectPowerSync;
@@ -5,7 +5,7 @@ import { loadConfig } from '../config/store.js';
5
5
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
6
6
  import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
7
7
  import { restartDaemonLocked } from '../daemon/index.js';
8
- import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
8
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
9
9
  import { DEFAULT_DAEMON_PORT } from '../daemon/types.js';
10
10
  import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
11
11
  import { inspectNoteTokenIndex, rebuildNoteTokenIndex, } from '../powersync/tokenIndex.js';
@@ -20,6 +20,7 @@ const doctorPreparationDependencies = {
20
20
  resolveConfig,
21
21
  hydrateAuthIdentity: hydrateResolvedAuthIdentity,
22
22
  acquireDaemonStartLock,
23
+ acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
23
24
  captureDaemonSnapshot,
24
25
  shutdownDaemon: shutdownDaemonForExclusiveAccess,
25
26
  connectPowerSync,
@@ -39,6 +40,14 @@ function doctorTimeoutMs(globals, config) {
39
40
  export async function prepareDoctorRuntime(context, overrides = {}) {
40
41
  const dependencies = { ...doctorPreparationDependencies, ...overrides };
41
42
  const releaseDaemonStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(context.globals.syncTimeout) ?? DEFAULT_DOCTOR_TIMEOUT_MS);
43
+ let releaseIntentLock;
44
+ const releaseIntent = async () => {
45
+ const release = releaseIntentLock;
46
+ releaseIntentLock = undefined;
47
+ await release?.().catch((error) => {
48
+ printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
49
+ });
50
+ };
42
51
  try {
43
52
  const rawConfig = await dependencies.loadConfig();
44
53
  const baseConfig = dependencies.resolveConfig(rawConfig);
@@ -56,6 +65,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
56
65
  authError = errorMessage(error);
57
66
  }
58
67
  }
68
+ releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
59
69
  const daemonBefore = await dependencies.captureDaemonSnapshot();
60
70
  const daemonDbPath = daemonBefore.info ? getPowerSyncDbPath(daemonBefore.info.userId) : null;
61
71
  const targetDbPath = getPowerSyncDbPath(config.auth.userId);
@@ -83,6 +93,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
83
93
  daemonBefore,
84
94
  daemonWasStopped,
85
95
  releaseDaemonStartLock,
96
+ releaseIntentLock,
86
97
  daemonStartLockReleased: false,
87
98
  released: false,
88
99
  };
@@ -109,16 +120,19 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
109
120
  daemonBefore,
110
121
  daemonWasStopped,
111
122
  releaseDaemonStartLock,
123
+ releaseIntentLock,
112
124
  daemonStartLockReleased: false,
113
125
  released: false,
114
126
  };
115
127
  }
116
128
  catch (error) {
129
+ await releaseIntent();
117
130
  await restoreStoppedDoctorDaemon(config, daemonBefore, daemonWasStopped, dependencies.restartDaemon);
118
131
  throw error;
119
132
  }
120
133
  }
121
134
  catch (error) {
135
+ await releaseIntent();
122
136
  await releaseDaemonStartLock().catch((releaseError) => {
123
137
  printHumanReadableWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(releaseError)}`);
124
138
  });
@@ -357,6 +371,11 @@ export async function releaseDoctorRuntime(runtime) {
357
371
  }
358
372
  await closePowerSyncAndReleaseOwner(runtime.db, runtime.releaseOwner);
359
373
  runtime.released = true;
374
+ const releaseIntentLock = runtime.releaseIntentLock;
375
+ runtime.releaseIntentLock = undefined;
376
+ await releaseIntentLock?.().catch((error) => {
377
+ printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
378
+ });
360
379
  }
361
380
  export async function releaseDoctorDaemonStartLock(runtime) {
362
381
  if (runtime.daemonStartLockReleased) {
@@ -4,6 +4,7 @@ import { registerCommandGroup } from '../descriptor/register.js';
4
4
  import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
5
5
  import { DinoxError } from '../../utils/errors.js';
6
6
  import { printSuccess } from '../../utils/output.js';
7
+ import { localCommandExecution, requestContext } from '../../utils/requestContext.js';
7
8
  import { commandStale, syncBeforeCommand, withCommandRuntime } from '../runtime.js';
8
9
  import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
9
10
  const GRAPH_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
@@ -26,7 +27,7 @@ function parseDepth(value) {
26
27
  }
27
28
  async function runGraphCommand(context, onlineAction, offlineAction) {
28
29
  const globals = context.globals;
29
- if (Boolean(globals.offline) || Boolean(globals.requireSync)) {
30
+ if (requestContext.getStore()?.daemon || localCommandExecution.getStore() || Boolean(globals.offline) || Boolean(globals.requireSync)) {
30
31
  await withCommandRuntime(context, async (runtime) => {
31
32
  await syncBeforeCommand(runtime);
32
33
  printSuccess({
@@ -11,6 +11,7 @@ import { nowIsoUtc } from '../../utils/time.js';
11
11
  import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
12
12
  import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
13
13
  import { parseStoredTags } from './repo.js';
14
+ import { resolveUserPath } from '../../utils/requestContext.js';
14
15
  const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
15
16
  const MAX_STORED_READ_TOKENS = 500;
16
17
  const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
@@ -111,7 +112,7 @@ async function loadTokenStore(nowMs = Date.now()) {
111
112
  const filePath = tokenStorePath();
112
113
  let tokens = [];
113
114
  try {
114
- const raw = await fs.readFile(filePath, 'utf8');
115
+ const raw = await fs.readFile(resolveUserPath(filePath), 'utf8');
115
116
  const parsed = JSON.parse(raw);
116
117
  if (Array.isArray(parsed.tokens)) {
117
118
  tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
@@ -6,6 +6,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
6
6
  import { commandStale, } from '../runtime.js';
7
7
  import { getNoteExportItems, } from './repo.js';
8
8
  import { parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
9
+ import { resolveUserPath } from '../../utils/requestContext.js';
9
10
  export function parseExportFormat(value) {
10
11
  if (value === undefined || value === null || value === '') {
11
12
  return 'markdown';
@@ -107,7 +108,7 @@ async function pathExists(filePath) {
107
108
  }
108
109
  async function resolveOutputTargets(args) {
109
110
  const outputLooksLikeDirectory = /[\\/]$/.test(args.outputPath);
110
- const outputPath = path.resolve(args.outputPath);
111
+ const outputPath = resolveUserPath(args.outputPath);
111
112
  if (args.notes.length === 1) {
112
113
  if (outputLooksLikeDirectory) {
113
114
  return [{
@@ -160,7 +161,7 @@ export async function writeExportFiles(args) {
160
161
  }
161
162
  }
162
163
  if (args.notes.length > 1) {
163
- await fs.mkdir(path.resolve(args.outputPath), { recursive: true, mode: 0o700 });
164
+ await fs.mkdir(resolveUserPath(args.outputPath), { recursive: true, mode: 0o700 });
164
165
  }
165
166
  else {
166
167
  await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
@@ -43,4 +43,12 @@ export declare function waitForMutationUpload(runtime: CommandRuntime, options?:
43
43
  durability?: unknown;
44
44
  }): Promise<MutationUploadReceipt>;
45
45
  export declare function printStaleCacheWarning(runtime: CommandRuntime, message?: string): void;
46
+ /**
47
+ * Freshness on the daemon's long-lived stream. The server pushes every change,
48
+ * so a connected, error-free stream is current without a new checkpoint; an
49
+ * idle stream may not produce one before the timeout. After a reconnect (wake,
50
+ * network change) it may have been a dead connection, so then a checkpoint
51
+ * completed after the reconnect is required.
52
+ */
53
+ export declare function waitForDaemonCurrent(db: AbstractPowerSyncDatabase, _startedAt: string, timeoutMs: number): Promise<boolean>;
46
54
  export {};
@@ -8,6 +8,7 @@ import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
8
8
  import { createTimeoutSignal } from '../utils/abort.js';
9
9
  import { DinoxError } from '../utils/errors.js';
10
10
  import { printHumanReadableWarning } from '../utils/output.js';
11
+ import { requestContext } from '../utils/requestContext.js';
11
12
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
12
13
  import { parseWriteDurability } from './writeDurability.js';
13
14
  const PRE_SYNC_TIMEOUT_MS = 10_000;
@@ -44,6 +45,22 @@ export function resolveWriteDurabilityForRuntime(runtime, value) {
44
45
  export async function withCommandRuntime(source, run) {
45
46
  const startedAtIso = new Date().toISOString();
46
47
  const globals = resolveGlobals(source);
48
+ const daemon = requestContext.getStore()?.daemon;
49
+ if (daemon) {
50
+ const offline = Boolean(globals.offline);
51
+ const status = getStatusSnapshot(daemon.db);
52
+ const stale = offline || !status.connected || !status.hasSynced || Boolean(daemon.syncUnconfirmedSince?.());
53
+ return run({
54
+ db: daemon.db,
55
+ config: daemon.config,
56
+ rawConfig: {},
57
+ offline,
58
+ timeoutMs: parseSyncTimeoutMs(globals.syncTimeout) ?? daemon.config.sync.timeoutMs,
59
+ stale,
60
+ requireSync: Boolean(globals.requireSync),
61
+ sync: { firstSyncStale: stale, preSyncIdle: null, recentSync: null, gate: null, startedAtIso },
62
+ });
63
+ }
47
64
  const rawConfig = await loadConfig();
48
65
  const baseConfig = resolveConfig(rawConfig);
49
66
  const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
@@ -99,7 +116,7 @@ export async function syncBeforeCommand(runtime, options = {}) {
99
116
  const deadlineAtMs = Date.now() + timeoutMs;
100
117
  const remainingTimeoutMs = () => Math.max(0, deadlineAtMs - Date.now());
101
118
  const recentSync = runtime.requireSync
102
- ? await waitForLastSyncedAfter(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
119
+ ? await (requestContext.getStore()?.daemon ? waitForDaemonCurrent : waitForLastSyncedAfter)(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
103
120
  : true;
104
121
  runtime.sync.recentSync = recentSync;
105
122
  const downloadIdle = await waitForDownloadIdle(runtime.db, Math.max(1, remainingTimeoutMs()));
@@ -178,3 +195,34 @@ export function printStaleCacheWarning(runtime, message = 'warning: sync timed o
178
195
  printHumanReadableWarning(message);
179
196
  }
180
197
  }
198
+ /**
199
+ * Freshness on the daemon's long-lived stream. The server pushes every change,
200
+ * so a connected, error-free stream is current without a new checkpoint; an
201
+ * idle stream may not produce one before the timeout. After a reconnect (wake,
202
+ * network change) it may have been a dead connection, so then a checkpoint
203
+ * completed after the reconnect is required.
204
+ */
205
+ export async function waitForDaemonCurrent(db, _startedAt, timeoutMs) {
206
+ const unconfirmedSince = requestContext.getStore()?.daemon?.syncUnconfirmedSince?.();
207
+ if (unconfirmedSince) {
208
+ return await waitForLastSyncedAfter(db, unconfirmedSince, timeoutMs)
209
+ && !getStatusSnapshot(db).dataFlowStatus.downloadError;
210
+ }
211
+ const current = () => {
212
+ const status = getStatusSnapshot(db);
213
+ return status.connected && status.hasSynced && !status.dataFlowStatus.downloadError;
214
+ };
215
+ if (current())
216
+ return true;
217
+ const timeout = createTimeoutSignal(timeoutMs);
218
+ try {
219
+ await db.waitForStatus(() => current(), timeout.signal);
220
+ return !timeout.signal.aborted && current();
221
+ }
222
+ catch {
223
+ return false;
224
+ }
225
+ finally {
226
+ timeout.cancel();
227
+ }
228
+ }
@@ -2,3 +2,4 @@ export declare const FORMAT_OPTIONS: string[];
2
2
  export declare const OFFLINE_OPTIONS: string[];
3
3
  export declare const REQUIRE_SYNC_OPTIONS: string[];
4
4
  export declare const SYNC_TIMEOUT_OPTIONS: string[];
5
+ export declare const REQUEST_ID_OPTIONS: string[];
@@ -2,3 +2,4 @@ export const FORMAT_OPTIONS = ['--format <yaml|json>', '--json'];
2
2
  export const OFFLINE_OPTIONS = ['--offline'];
3
3
  export const REQUIRE_SYNC_OPTIONS = ['--require-sync'];
4
4
  export const SYNC_TIMEOUT_OPTIONS = ['--sync-timeout <ms>'];
5
+ export const REQUEST_ID_OPTIONS = ['--request-id <id>'];
@@ -33,6 +33,7 @@ const GLOBAL_OPTIONS = [
33
33
  ['`--offline`', 'Skip sync, use local cache only'],
34
34
  ['`--require-sync`', 'Fail if the local PowerSync cache cannot be confirmed fresh before reading'],
35
35
  ['`--sync-timeout <ms>`', 'Override default 300000 ms (5 min) sync/connect timeout'],
36
+ ['`--request-id <id>`', 'Idempotency key for data commands: rerunning with the same id within 24h replays the stored result'],
36
37
  ['`--verbose`', 'Enable verbose logging'],
37
38
  ];
38
39
  function buildCommandSignature(schema) {
@@ -1,5 +1,6 @@
1
1
  import { authCommandGroup } from '../auth/index.js';
2
2
  import { boxCommandGroup } from '../boxes/index.js';
3
+ import { commandExecutionTarget } from '../../daemon/forward.js';
3
4
  import { commandSchemasFromGroupDescriptor } from '../descriptor/register.js';
4
5
  import { standaloneCommandSchemaFromDescriptor } from '../descriptor/register.js';
5
6
  import { configCommandGroup } from '../config/index.js';
@@ -14,6 +15,7 @@ import { promptCommandGroup } from '../prompt/index.js';
14
15
  import { propertyCommandGroup } from '../properties/index.js';
15
16
  import { savedViewCommandGroup } from '../savedViews/index.js';
16
17
  import { schemaCommandMetadata } from './commandMetadata.js';
18
+ import { REQUEST_ID_OPTIONS } from './globalOptions.js';
17
19
  import { skillsCommandGroup } from '../skills/index.js';
18
20
  import { syncCommandDescriptor } from '../sync.js';
19
21
  import { storageCommandGroup } from '../storage/index.js';
@@ -21,6 +23,12 @@ import { tagCommandGroup } from '../tags/index.js';
21
23
  import { templateCommandGroup } from '../templates/index.js';
22
24
  import { todoCommandGroup } from '../todo/index.js';
23
25
  import { updateCommandDescriptor } from '../update.js';
26
+ /** Every command that runs inside the daemon accepts an idempotency key. */
27
+ function withRequestId(schema) {
28
+ if (commandExecutionTarget(schema.path.split('.')) !== 'daemon')
29
+ return schema;
30
+ return { ...schema, globalOptions: [...schema.globalOptions, ...REQUEST_ID_OPTIONS] };
31
+ }
24
32
  export const COMMAND_SCHEMAS = [
25
33
  ...commandSchemasFromGroupDescriptor(authCommandGroup),
26
34
  ...commandSchemasFromGroupDescriptor(configCommandGroup),
@@ -43,7 +51,7 @@ export const COMMAND_SCHEMAS = [
43
51
  ...commandSchemasFromGroupDescriptor(noteCommandGroup),
44
52
  ...commandSchemasFromGroupDescriptor(graphCommandGroup),
45
53
  ...commandSchemasFromGroupDescriptor(todoCommandGroup),
46
- ];
54
+ ].map(withRequestId);
47
55
  const COMMAND_SCHEMA_MAP = new Map(COMMAND_SCHEMAS.map((schema) => [schema.path, schema]));
48
56
  export function listCommandSchemas() {
49
57
  return [...COMMAND_SCHEMAS];
@@ -1,6 +1,7 @@
1
1
  // Storage commands: inspect/test/upload orchestration with cleanup on partial failure.
2
2
  import fsp from 'node:fs/promises';
3
3
  import { DinoxError } from '../../../utils/errors.js';
4
+ import { resolveUserPath } from '../../../utils/requestContext.js';
4
5
  import { buildStorageTestPlan, buildStorageUploadPlan, getActiveCustomStorageConfigId, listStoredCustomStorageConfigs, resolveStorageSelection, } from './configs.js';
5
6
  import { createStorageCollisionError, createStorageUploadOutcomeUnknownError, defaultDeleteObject, defaultUploadBytes, defaultUploadObject, generateThumbnailBuffer, isObjectCollisionError, loadSharp, } from './remote.js';
6
7
  import { normalizeText, } from './shared.js';
@@ -8,7 +9,8 @@ import { persistUploadedResource } from './resources.js';
8
9
  import { buildStorageTarget, generateThumbnailKey } from './targets.js';
9
10
  export async function inspectStorageUpload(db, input, deps) {
10
11
  const statFile = deps?.statFile ?? fsp.stat;
11
- const filePath = input.filePath.trim();
12
+ const suppliedPath = input.filePath.trim();
13
+ const filePath = suppliedPath ? resolveUserPath(suppliedPath) : suppliedPath;
12
14
  if (!filePath) {
13
15
  throw new DinoxError('File path is required.');
14
16
  }
@@ -5,7 +5,7 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
5
5
  import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
6
6
  import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
7
7
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../utils/output.js';
8
- import { getStatusSnapshot, waitForLastSyncedAfter } from '../powersync/runtime.js';
8
+ import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty } from '../powersync/runtime.js';
9
9
  import { runSingleSyncPass } from '../powersync/syncPass.js';
10
10
  type SyncCommandDependencies = {
11
11
  loadConfig: typeof loadConfig;
@@ -15,6 +15,7 @@ type SyncCommandDependencies = {
15
15
  getRequestedStructuredOutputFormat: typeof getRequestedStructuredOutputFormat;
16
16
  getStatusSnapshot: typeof getStatusSnapshot;
17
17
  waitForLastSyncedAfter: typeof waitForLastSyncedAfter;
18
+ waitForUploadQueueEmpty: typeof waitForUploadQueueEmpty;
18
19
  runSingleSyncPass: typeof runSingleSyncPass;
19
20
  printHumanReadableLine: typeof printHumanReadableLine;
20
21
  printSuccess: typeof printSuccess;
@@ -1,12 +1,15 @@
1
+ import { getPowerSyncDbPath } from '../config/paths.js';
1
2
  import { loadConfig } from '../config/store.js';
2
3
  import { resolveConfig } from '../config/resolve.js';
3
4
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
4
5
  import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
6
+ import { requestContext } from '../utils/requestContext.js';
5
7
  import { registerStandaloneCommand } from './descriptor/register.js';
8
+ import { waitForDaemonCurrent } from './runtime.js';
6
9
  import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
7
10
  import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
8
11
  import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
9
- import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
12
+ import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
10
13
  import { runSingleSyncPass } from '../powersync/syncPass.js';
11
14
  import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
12
15
  const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
@@ -18,6 +21,7 @@ const syncCommandDependencies = {
18
21
  getRequestedStructuredOutputFormat,
19
22
  getStatusSnapshot,
20
23
  waitForLastSyncedAfter,
24
+ waitForUploadQueueEmpty,
21
25
  runSingleSyncPass,
22
26
  printHumanReadableLine,
23
27
  printSuccess,
@@ -27,18 +31,26 @@ export async function executeSyncCommand(context, overrides = {}) {
27
31
  const dependencies = { ...syncCommandDependencies, ...overrides };
28
32
  const startedAtMs = dependencies.now();
29
33
  const startedAtIso = new Date(startedAtMs).toISOString();
30
- const baseConfig = dependencies.resolveConfig(await dependencies.loadConfig());
34
+ const daemon = requestContext.getStore()?.daemon;
35
+ const baseConfig = daemon?.config ?? dependencies.resolveConfig(await dependencies.loadConfig());
31
36
  const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
32
37
  const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
33
38
  const deadlineAtMs = startedAtMs + timeoutMs;
34
39
  const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
35
40
  const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
36
41
  const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
37
- const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
42
+ const { config } = daemon ? { config: daemon.config } : await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
38
43
  timeoutMs: remainingTimeoutMs(),
39
44
  });
40
45
  const strict = Boolean(context.options.strict);
41
- const result = await dependencies.withExclusivePowerSync({
46
+ const withRuntime = daemon
47
+ ? async (_args, run) => {
48
+ const status = dependencies.getStatusSnapshot(daemon.db);
49
+ return run({ db: daemon.db, dbPath: getPowerSyncDbPath(config.auth.userId),
50
+ stale: !status.connected || !status.hasSynced, releaseOwner: async () => { } });
51
+ }
52
+ : dependencies.withExclusivePowerSync;
53
+ const result = await withRuntime({
42
54
  config,
43
55
  offline: false,
44
56
  timeoutMs: remainingTimeoutMs(),
@@ -49,9 +61,16 @@ export async function executeSyncCommand(context, overrides = {}) {
49
61
  cleanupFailureMode: 'throw',
50
62
  }, async ({ db, dbPath, stale }) => {
51
63
  const recentSync = strict
52
- ? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
64
+ ? await (daemon ? waitForDaemonCurrent : dependencies.waitForLastSyncedAfter)(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
53
65
  : true;
54
66
  const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
67
+ const uploadQueue = daemon
68
+ ? await dependencies.waitForUploadQueueEmpty(db, remainingTimeoutMs(cleanupReserveMs))
69
+ : null;
70
+ if (uploadQueue) {
71
+ syncPass.uploadIdle = uploadQueue.ok;
72
+ syncPass.idle = syncPass.downloadIdle && uploadQueue.ok;
73
+ }
55
74
  const status = dependencies.getStatusSnapshot(db);
56
75
  const gate = evaluateSyncGate({
57
76
  offline: false,