@dinoxx/dinox-cli 1.0.22 → 1.1.1

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 (147) hide show
  1. package/README.md +33 -10
  2. package/dist/auth/userInfo.d.ts +2 -0
  3. package/dist/auth/userInfo.js +55 -14
  4. package/dist/cli.js +36 -2
  5. package/dist/commands/auth/index.d.ts +35 -0
  6. package/dist/commands/auth/index.js +382 -83
  7. package/dist/commands/boxes/index.js +2 -6
  8. package/dist/commands/boxes/model.d.ts +3 -0
  9. package/dist/commands/boxes/model.js +16 -1
  10. package/dist/commands/boxes/operations.js +18 -8
  11. package/dist/commands/boxes/repo.js +13 -11
  12. package/dist/commands/boxes/store.js +1 -1
  13. package/dist/commands/config/index.js +7 -5
  14. package/dist/commands/descriptor/register.js +9 -0
  15. package/dist/commands/doctor.d.ts +123 -0
  16. package/dist/commands/doctor.js +539 -0
  17. package/dist/commands/graph/index.d.ts +1 -1
  18. package/dist/commands/graph/index.js +35 -21
  19. package/dist/commands/graph/repo.js +92 -17
  20. package/dist/commands/meta/index.js +4 -4
  21. package/dist/commands/meta/repo.js +4 -3
  22. package/dist/commands/notes/actions.d.ts +1 -1
  23. package/dist/commands/notes/actions.js +121 -78
  24. package/dist/commands/notes/bulk.d.ts +1 -0
  25. package/dist/commands/notes/bulk.js +23 -17
  26. package/dist/commands/notes/contentEngine.js +69 -11
  27. package/dist/commands/notes/contentPatch.d.ts +4 -1
  28. package/dist/commands/notes/contentPatch.js +105 -41
  29. package/dist/commands/notes/contentPatchCommand.js +28 -11
  30. package/dist/commands/notes/descriptors.js +50 -20
  31. package/dist/commands/notes/dryRun.d.ts +1 -0
  32. package/dist/commands/notes/dryRun.js +4 -7
  33. package/dist/commands/notes/export.d.ts +6 -0
  34. package/dist/commands/notes/export.js +26 -6
  35. package/dist/commands/notes/metadataMutation.d.ts +1 -0
  36. package/dist/commands/notes/metadataMutation.js +14 -3
  37. package/dist/commands/notes/repo.d.ts +46 -4
  38. package/dist/commands/notes/repo.js +113 -28
  39. package/dist/commands/notes/runtime.d.ts +2 -1
  40. package/dist/commands/notes/runtime.js +17 -1
  41. package/dist/commands/notes/searchSql.js +13 -6
  42. package/dist/commands/notes/writeReceipt.d.ts +20 -0
  43. package/dist/commands/notes/writeReceipt.js +82 -0
  44. package/dist/commands/prompt/index.js +3 -3
  45. package/dist/commands/prompt/repo.d.ts +4 -1
  46. package/dist/commands/prompt/repo.js +17 -7
  47. package/dist/commands/runtime.d.ts +14 -2
  48. package/dist/commands/runtime.js +89 -19
  49. package/dist/commands/schema/renderSkills.js +5 -2
  50. package/dist/commands/schema/spec.js +4 -0
  51. package/dist/commands/storage/index.js +8 -2
  52. package/dist/commands/storage/repo.d.ts +4 -0
  53. package/dist/commands/storage/repo.js +356 -92
  54. package/dist/commands/sync.d.ts +23 -1
  55. package/dist/commands/sync.js +105 -60
  56. package/dist/commands/tags/content.js +9 -0
  57. package/dist/commands/tags/index.js +2 -6
  58. package/dist/commands/tags/operations.js +15 -10
  59. package/dist/commands/tags/repo.d.ts +2 -0
  60. package/dist/commands/tags/repo.js +18 -5
  61. package/dist/commands/tags/store.js +1 -1
  62. package/dist/commands/todo/contentModel.d.ts +4 -3
  63. package/dist/commands/todo/contentModel.js +78 -28
  64. package/dist/commands/todo/dryRun.d.ts +1 -0
  65. package/dist/commands/todo/dryRun.js +17 -13
  66. package/dist/commands/todo/index.d.ts +6 -0
  67. package/dist/commands/todo/index.js +89 -123
  68. package/dist/commands/todo/repo.d.ts +36 -1
  69. package/dist/commands/todo/repo.js +146 -37
  70. package/dist/commands/todo/taskIdentity.d.ts +2 -0
  71. package/dist/commands/todo/taskIdentity.js +64 -0
  72. package/dist/commands/todo/taskModel.js +27 -17
  73. package/dist/commands/update.d.ts +13 -1
  74. package/dist/commands/update.js +135 -23
  75. package/dist/commands/writeDryRun.d.ts +3 -2
  76. package/dist/commands/writeDryRun.js +4 -3
  77. package/dist/commands/writeDurability.d.ts +5 -0
  78. package/dist/commands/writeDurability.js +22 -0
  79. package/dist/config/paths.js +6 -1
  80. package/dist/config/resolve.d.ts +2 -0
  81. package/dist/config/resolve.js +23 -9
  82. package/dist/config/store.d.ts +3 -0
  83. package/dist/config/store.js +61 -14
  84. package/dist/config/types.d.ts +4 -0
  85. package/dist/daemon/client.d.ts +26 -1
  86. package/dist/daemon/client.js +236 -12
  87. package/dist/daemon/ensure.js +74 -12
  88. package/dist/daemon/index.d.ts +42 -0
  89. package/dist/daemon/index.js +202 -52
  90. package/dist/daemon/lifecycle.d.ts +44 -0
  91. package/dist/daemon/lifecycle.js +118 -0
  92. package/dist/daemon/pid.d.ts +19 -6
  93. package/dist/daemon/pid.js +147 -13
  94. package/dist/daemon/route.d.ts +14 -0
  95. package/dist/daemon/route.js +40 -0
  96. package/dist/daemon/server.d.ts +11 -0
  97. package/dist/daemon/server.js +201 -50
  98. package/dist/daemon/types.d.ts +29 -1
  99. package/dist/daemon/types.js +1 -0
  100. package/dist/dinox.js +25 -1
  101. package/dist/powersync/connector.js +31 -16
  102. package/dist/powersync/exclusiveRuntime.d.ts +20 -0
  103. package/dist/powersync/exclusiveRuntime.js +48 -0
  104. package/dist/powersync/runtime.d.ts +25 -4
  105. package/dist/powersync/runtime.js +255 -35
  106. package/dist/powersync/syncGate.d.ts +4 -1
  107. package/dist/powersync/syncGate.js +11 -2
  108. package/dist/powersync/syncPass.d.ts +2 -0
  109. package/dist/powersync/syncPass.js +15 -4
  110. package/dist/powersync/tokenIndex.d.ts +13 -0
  111. package/dist/powersync/tokenIndex.js +216 -37
  112. package/dist/powersync/uploader.js +107 -52
  113. package/dist/utils/abort.js +0 -4
  114. package/dist/utils/asyncRelease.d.ts +2 -0
  115. package/dist/utils/asyncRelease.js +25 -0
  116. package/dist/utils/bidlinkCanonicalization.js +3 -0
  117. package/dist/utils/dryRun.d.ts +1 -1
  118. package/dist/utils/errors.d.ts +15 -1
  119. package/dist/utils/errors.js +223 -5
  120. package/dist/utils/output.d.ts +1 -0
  121. package/dist/utils/output.js +51 -15
  122. package/dist/utils/redact.js +1 -1
  123. package/dist/utils/sqliteLease.d.ts +8 -0
  124. package/dist/utils/sqliteLease.js +59 -0
  125. package/dist/utils/syncTimeout.d.ts +7 -1
  126. package/dist/utils/syncTimeout.js +8 -7
  127. package/dist/utils/terminalSanitization.js +6 -2
  128. package/dist/utils/tiptapMarkdown.js +49 -3
  129. package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
  130. package/dist/utils/tiptapMarkdownSyntax.js +249 -0
  131. package/dist/utils/tokenize.js +24 -2
  132. package/dist/utils/updateNotice.js +60 -12
  133. package/package.json +14 -10
  134. package/dist/commands/mcp/auth.d.ts +0 -4
  135. package/dist/commands/mcp/auth.js +0 -75
  136. package/dist/commands/mcp/index.d.ts +0 -2
  137. package/dist/commands/mcp/index.js +0 -86
  138. package/dist/commands/mcp/noteContentTools.d.ts +0 -222
  139. package/dist/commands/mcp/noteContentTools.js +0 -390
  140. package/dist/commands/mcp/server.d.ts +0 -34
  141. package/dist/commands/mcp/server.js +0 -424
  142. package/dist/commands/mcp/tools.d.ts +0 -2
  143. package/dist/commands/mcp/tools.js +0 -32
  144. package/dist/commands/mcp/types.d.ts +0 -39
  145. package/dist/commands/mcp/types.js +0 -1
  146. package/dist/utils/noteContentEngine.d.ts +0 -333
  147. package/dist/utils/noteContentEngine.js +0 -740
package/dist/dinox.js CHANGED
@@ -18,9 +18,26 @@ async function main() {
18
18
  function isVerbose(argv) {
19
19
  return argv.includes('--verbose');
20
20
  }
21
+ function isCommanderError(error) {
22
+ return Boolean(error)
23
+ && typeof error === 'object'
24
+ && typeof error.code === 'string'
25
+ && error.code.startsWith('commander.')
26
+ && typeof error.exitCode === 'number';
27
+ }
21
28
  main().catch((error) => {
29
+ if (isCommanderError(error)) {
30
+ process.exitCode = error.exitCode === 0 ? 0 : 2;
31
+ return;
32
+ }
22
33
  const argv = normalizeArgv(process.argv.slice(2));
23
- const format = parseStructuredOutputFormatFromArgv(argv);
34
+ let format = null;
35
+ try {
36
+ format = parseStructuredOutputFormatFromArgv(argv);
37
+ }
38
+ catch {
39
+ format = null;
40
+ }
24
41
  const verbose = isVerbose(argv);
25
42
  const payload = buildStructuredErrorPayload(error, verbose);
26
43
  const exitCode = isDinoxError(error) ? error.exitCode : 1;
@@ -32,6 +49,13 @@ main().catch((error) => {
32
49
  return;
33
50
  }
34
51
  printHumanReadableError(payload.message);
52
+ if (payload.suggested_action) {
53
+ printHumanReadableError(`suggested action: ${payload.suggested_action.command}`);
54
+ printHumanReadableError(`reason: ${payload.suggested_action.reason}`);
55
+ }
56
+ else if (payload.suggestion) {
57
+ printHumanReadableError(`suggestion: ${payload.suggestion}`);
58
+ }
35
59
  if (verbose && payload.stack) {
36
60
  printHumanReadableError(payload.stack);
37
61
  }
@@ -39,9 +39,8 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
39
39
  ? createTimeoutSignal(timeoutMs)
40
40
  : null;
41
41
  const requestSignal = mergeAbortSignals([signal, timeout?.signal]);
42
- let response;
43
42
  try {
44
- response = await fetch(credentialsUrl, {
43
+ const response = await fetch(credentialsUrl, {
45
44
  method: 'GET',
46
45
  headers: {
47
46
  Authorization: toAuthorizationHeaderValue(authorization),
@@ -49,6 +48,36 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
49
48
  },
50
49
  signal: requestSignal,
51
50
  });
51
+ if (!response.ok) {
52
+ let errorText = '';
53
+ try {
54
+ errorText = await response.text();
55
+ }
56
+ catch (error) {
57
+ if (requestSignal?.aborted) {
58
+ throw error;
59
+ }
60
+ }
61
+ throw new Error(`Received ${response.status} from ${credentialsUrl}: ${errorText}`);
62
+ }
63
+ let payload = null;
64
+ try {
65
+ payload = await response.json();
66
+ }
67
+ catch (error) {
68
+ if (requestSignal?.aborted) {
69
+ throw error;
70
+ }
71
+ }
72
+ const token = payload?.token ?? payload?.data?.token ?? null;
73
+ const endpointFromPayload = payload?.endpoint ?? payload?.data?.endpoint ?? null;
74
+ if (!token) {
75
+ throw new Error('PowerSync credential response did not include a token');
76
+ }
77
+ return {
78
+ endpoint: endpointFromPayload ?? endpoint,
79
+ token,
80
+ };
52
81
  }
53
82
  catch (error) {
54
83
  if (timeout?.signal.aborted && !signal?.aborted) {
@@ -59,18 +88,4 @@ export async function fetchCredentialsWithAuthorization({ authorization, endpoin
59
88
  finally {
60
89
  timeout?.cancel();
61
90
  }
62
- if (!response.ok) {
63
- const errorText = await response.text().catch(() => '');
64
- throw new Error(`Received ${response.status} from ${credentialsUrl}: ${errorText}`);
65
- }
66
- const payload = await response.json().catch(() => null);
67
- const token = payload?.token ?? payload?.data?.token ?? null;
68
- const endpointFromPayload = payload?.endpoint ?? payload?.data?.endpoint ?? null;
69
- if (!token) {
70
- throw new Error('PowerSync credential response did not include a token');
71
- }
72
- return {
73
- endpoint: endpointFromPayload ?? endpoint,
74
- token,
75
- };
76
91
  }
@@ -0,0 +1,20 @@
1
+ import type { ResolvedDinoxConfig } from '../config/types.js';
2
+ import { withDaemonExclusiveAccess } from '../daemon/lifecycle.js';
3
+ import { printHumanReadableWarning } from '../utils/output.js';
4
+ import { closePowerSyncAndReleaseOwner, connectPowerSync, type ConnectResult } from './runtime.js';
5
+ type ExclusivePowerSyncDependencies = {
6
+ withDaemonExclusiveAccess: typeof withDaemonExclusiveAccess;
7
+ connectPowerSync: typeof connectPowerSync;
8
+ closePowerSyncAndReleaseOwner: typeof closePowerSyncAndReleaseOwner;
9
+ printWarning: typeof printHumanReadableWarning;
10
+ };
11
+ export declare function withExclusivePowerSync<T>(args: {
12
+ config: ResolvedDinoxConfig;
13
+ offline: boolean;
14
+ timeoutMs: number;
15
+ deadlineAtMs?: number;
16
+ waitForInitialSync?: boolean;
17
+ expectedTokenFingerprint?: string | null;
18
+ cleanupFailureMode?: 'warn' | 'throw';
19
+ }, run: (connected: ConnectResult) => Promise<T>, overrides?: Partial<ExclusivePowerSyncDependencies>): Promise<T>;
20
+ export {};
@@ -0,0 +1,48 @@
1
+ import { withDaemonExclusiveAccess } from '../daemon/lifecycle.js';
2
+ import { printHumanReadableWarning } from '../utils/output.js';
3
+ import { closePowerSyncAndReleaseOwner, connectPowerSync, } from './runtime.js';
4
+ const exclusivePowerSyncDependencies = {
5
+ withDaemonExclusiveAccess,
6
+ connectPowerSync,
7
+ closePowerSyncAndReleaseOwner,
8
+ printWarning: printHumanReadableWarning,
9
+ };
10
+ function errorMessage(error) {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+ export async function withExclusivePowerSync(args, run, overrides = {}) {
14
+ const dependencies = { ...exclusivePowerSyncDependencies, ...overrides };
15
+ const { cleanupFailureMode, deadlineAtMs, ...connectArgs } = args;
16
+ const remainingTimeoutMs = () => deadlineAtMs === undefined
17
+ ? args.timeoutMs
18
+ : Math.max(1, Math.min(args.timeoutMs, deadlineAtMs - Date.now()));
19
+ return dependencies.withDaemonExclusiveAccess(args.config, remainingTimeoutMs(), async (daemonLifecycle) => {
20
+ const connected = await dependencies.connectPowerSync({
21
+ ...connectArgs,
22
+ timeoutMs: remainingTimeoutMs(),
23
+ });
24
+ let outcome;
25
+ try {
26
+ outcome = { ok: true, value: await run(connected) };
27
+ }
28
+ catch (error) {
29
+ outcome = { ok: false, error };
30
+ }
31
+ let cleanupError;
32
+ try {
33
+ await dependencies.closePowerSyncAndReleaseOwner(connected.db, connected.releaseOwner);
34
+ }
35
+ catch (error) {
36
+ cleanupError = error;
37
+ daemonLifecycle.suppressRestore();
38
+ dependencies.printWarning(`warning: failed to close the local PowerSync runtime safely: ${errorMessage(error)}`);
39
+ }
40
+ if (!outcome.ok) {
41
+ throw outcome.error;
42
+ }
43
+ if (cleanupError && cleanupFailureMode === 'throw') {
44
+ throw cleanupError;
45
+ }
46
+ return outcome.value;
47
+ }, {}, { deadlineAtMs });
48
+ }
@@ -5,7 +5,13 @@ export type ConnectResult = {
5
5
  db: PowerSyncDatabase;
6
6
  dbPath: string;
7
7
  stale: boolean;
8
+ releaseOwner: () => Promise<void>;
8
9
  };
10
+ type ClosablePowerSyncDatabase = {
11
+ close: () => Promise<void>;
12
+ };
13
+ export declare function acquirePowerSyncOwnerLease(dbPath: string, timeoutMs: number): Promise<() => Promise<void>>;
14
+ export declare function closePowerSyncAndReleaseOwner(db: ClosablePowerSyncDatabase, releaseOwner: () => Promise<void>): Promise<void>;
9
15
  export declare function getStatusSnapshot(db: AbstractPowerSyncDatabase): {
10
16
  connected: boolean;
11
17
  connecting: boolean;
@@ -25,14 +31,29 @@ export declare function getStatusSnapshot(db: AbstractPowerSyncDatabase): {
25
31
  };
26
32
  raw: SyncStatusOptions;
27
33
  };
28
- export declare function openPowerSyncDatabase(userId?: string | null): Promise<{
29
- db: PowerSyncDatabase;
30
- dbPath: string;
31
- }>;
32
34
  export declare function connectPowerSync(args: {
33
35
  config: ResolvedDinoxConfig;
34
36
  offline: boolean;
35
37
  timeoutMs: number;
38
+ waitForInitialSync?: boolean;
39
+ expectedTokenFingerprint?: string | null;
36
40
  }): Promise<ConnectResult>;
37
41
  export declare function waitForIdleDataFlow(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<boolean>;
42
+ export declare function waitForDownloadIdle(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<boolean>;
43
+ export declare function waitForLastSyncedAfter(db: AbstractPowerSyncDatabase, startedAtIso: string, timeoutMs: number): Promise<boolean>;
44
+ export type UploadQueueWaitResult = {
45
+ ok: boolean;
46
+ pendingCount: number | null;
47
+ uploadError?: {
48
+ name: string;
49
+ message: string;
50
+ };
51
+ queueError?: {
52
+ name: string;
53
+ message: string;
54
+ };
55
+ timedOut: boolean;
56
+ };
57
+ export declare function waitForUploadQueueEmpty(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<UploadQueueWaitResult>;
38
58
  export declare function withPowerSyncWriteTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
59
+ export {};
@@ -2,11 +2,87 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { PowerSyncDatabase } from '@powersync/node';
4
4
  import { getDataDir, getPowerSyncDbPath } from '../config/paths.js';
5
+ import { resolveConfig } from '../config/resolve.js';
6
+ import { loadConfig } from '../config/store.js';
5
7
  import { DinoxError } from '../utils/errors.js';
6
8
  import { createTimeoutSignal } from '../utils/abort.js';
9
+ import { acquireExclusiveSqliteLease } from '../utils/sqliteLease.js';
7
10
  import { AppSchema } from './schema/index.js';
8
11
  import { PowerSyncConnector, fetchCredentialsWithAuthorization } from './connector.js';
9
12
  import { createPowerSyncUploadHandler } from './uploader.js';
13
+ const OWNER_LEASE_WAIT_MAX_MS = 10_000;
14
+ const retainedFailedPowerSyncClosures = new Set();
15
+ function retainFailedPowerSyncClosure(db, releaseOwner, dbClosed) {
16
+ for (const retained of retainedFailedPowerSyncClosures) {
17
+ if (retained.db === db && retained.releaseOwner === releaseOwner) {
18
+ retained.dbClosed ||= dbClosed;
19
+ return;
20
+ }
21
+ }
22
+ retainedFailedPowerSyncClosures.add({ db, releaseOwner, dbClosed });
23
+ }
24
+ function findFailedPowerSyncClosure(db, releaseOwner) {
25
+ for (const retained of retainedFailedPowerSyncClosures) {
26
+ if (retained.db === db && retained.releaseOwner === releaseOwner) {
27
+ return retained;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ function forgetFailedPowerSyncClosure(db, releaseOwner) {
33
+ for (const retained of retainedFailedPowerSyncClosures) {
34
+ if (retained.db === db && retained.releaseOwner === releaseOwner) {
35
+ retainedFailedPowerSyncClosures.delete(retained);
36
+ }
37
+ }
38
+ }
39
+ function ownerLeasePath(dbPath) {
40
+ return `${dbPath}.owner.sqlite`;
41
+ }
42
+ export async function acquirePowerSyncOwnerLease(dbPath, timeoutMs) {
43
+ const lockPath = ownerLeasePath(dbPath);
44
+ return acquireExclusiveSqliteLease({
45
+ lockPath,
46
+ timeoutMs,
47
+ maxWaitMs: OWNER_LEASE_WAIT_MAX_MS,
48
+ createBusyError: () => new DinoxError('Another Dinox process owns the PowerSync runtime for this account.', {
49
+ code: 'POWERSYNC_OWNER_BUSY',
50
+ details: { dbPath },
51
+ suggestion: 'Wait for the other Dinox command to finish, then retry.',
52
+ recoverable: true,
53
+ }),
54
+ });
55
+ }
56
+ export async function closePowerSyncAndReleaseOwner(db, releaseOwner) {
57
+ const retained = findFailedPowerSyncClosure(db, releaseOwner);
58
+ if (!retained?.dbClosed) {
59
+ try {
60
+ await db.close();
61
+ }
62
+ catch (error) {
63
+ retainFailedPowerSyncClosure(db, releaseOwner, false);
64
+ throw error;
65
+ }
66
+ }
67
+ try {
68
+ await releaseOwner();
69
+ }
70
+ catch (error) {
71
+ retainFailedPowerSyncClosure(db, releaseOwner, true);
72
+ throw error;
73
+ }
74
+ forgetFailedPowerSyncClosure(db, releaseOwner);
75
+ }
76
+ async function throwAfterPowerSyncCleanup(operationError, db, releaseOwner) {
77
+ try {
78
+ await closePowerSyncAndReleaseOwner(db, releaseOwner);
79
+ }
80
+ catch {
81
+ // Preserve the actionable operation failure. The cleanup helper keeps the
82
+ // owner lease held whenever database close did not complete safely.
83
+ }
84
+ throw operationError;
85
+ }
10
86
  async function ensureDataDir() {
11
87
  const dir = getDataDir();
12
88
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
@@ -34,7 +110,7 @@ export function getStatusSnapshot(db) {
34
110
  raw,
35
111
  };
36
112
  }
37
- export async function openPowerSyncDatabase(userId) {
113
+ async function createPowerSyncDatabase(userId) {
38
114
  await ensureDataDir();
39
115
  const dbPath = getPowerSyncDbPath(userId);
40
116
  await fs.mkdir(path.dirname(dbPath), { recursive: true, mode: 0o700 });
@@ -44,60 +120,162 @@ export async function openPowerSyncDatabase(userId) {
44
120
  dbFilename: dbPath,
45
121
  },
46
122
  });
47
- // Ensure initialization errors surface early.
48
- await db.init();
49
123
  return { db, dbPath };
50
124
  }
51
125
  function assertConfigured(config) {
52
126
  if (!config.auth.authorization) {
53
- throw new DinoxError('Not logged in. Run: dino auth login <token>');
127
+ throw new DinoxError('Not logged in. Set DINOX_TOKEN or pipe a token to `dino auth login --token-stdin`.');
54
128
  }
55
129
  }
56
130
  export async function connectPowerSync(args) {
57
- const { db, dbPath } = await openPowerSyncDatabase(args.config.auth.userId);
58
- if (args.offline) {
59
- return { db, dbPath, stale: true };
60
- }
61
- assertConfigured(args.config);
62
- const uploader = args.config.powersync.uploadBaseUrl
63
- ? createPowerSyncUploadHandler({
64
- authorization: args.config.auth.authorization,
65
- uploadBaseUrl: args.config.powersync.uploadBaseUrl,
66
- uploadV4Path: args.config.powersync.uploadV4Path,
67
- uploadV2Path: args.config.powersync.uploadV2Path,
68
- timeoutMs: args.timeoutMs,
69
- })
70
- : undefined;
71
- const connector = new PowerSyncConnector(async () => {
72
- return fetchCredentialsWithAuthorization({
73
- authorization: args.config.auth.authorization,
74
- endpoint: args.config.powersync.endpoint,
75
- credentialsUrl: args.config.powersync.tokenEndpoint,
76
- timeoutMs: args.timeoutMs,
77
- });
78
- }, uploader);
79
- await db.connect(connector);
80
- const { signal, cancel } = createTimeoutSignal(args.timeoutMs);
131
+ if (!args.offline) {
132
+ assertConfigured(args.config);
133
+ }
134
+ const dbPath = getPowerSyncDbPath(args.config.auth.userId);
135
+ await fs.mkdir(path.dirname(dbPath), { recursive: true, mode: 0o700 });
136
+ const releaseOwner = await acquirePowerSyncOwnerLease(dbPath, args.timeoutMs);
137
+ if (args.expectedTokenFingerprint !== undefined) {
138
+ try {
139
+ const currentAuth = resolveConfig(await loadConfig()).auth;
140
+ const currentTokenFingerprint = currentAuth.tokenFingerprint ?? null;
141
+ const expectedUserId = args.config.auth.userId ?? null;
142
+ const currentUserId = currentAuth.userId ?? null;
143
+ const persistedUserChanged = currentUserId !== null && currentUserId !== expectedUserId;
144
+ if (currentTokenFingerprint !== args.expectedTokenFingerprint || persistedUserChanged) {
145
+ throw new DinoxError('Authentication changed while waiting for the local database.', {
146
+ code: 'AUTH_STATE_CHANGED',
147
+ details: { dbPath, expectedUserId, currentUserId },
148
+ suggestion: 'Retry the command so it uses the current login.',
149
+ recoverable: true,
150
+ });
151
+ }
152
+ }
153
+ catch (error) {
154
+ try {
155
+ await releaseOwner();
156
+ }
157
+ catch {
158
+ // Preserve the authentication/configuration failure that triggered cleanup.
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+ let db = null;
81
164
  try {
82
- await db.waitForFirstSync(signal);
83
- return { db, dbPath, stale: signal.aborted };
165
+ ({ db } = await createPowerSyncDatabase(args.config.auth.userId));
166
+ await db.init();
84
167
  }
85
168
  catch (error) {
86
- if (signal.aborted) {
87
- return { db, dbPath, stale: true };
169
+ if (!db) {
170
+ try {
171
+ await releaseOwner();
172
+ }
173
+ catch {
174
+ // Preserve the database construction failure that triggered cleanup.
175
+ }
176
+ throw error;
88
177
  }
89
- throw error;
178
+ return throwAfterPowerSyncCleanup(error, db, releaseOwner);
179
+ }
180
+ if (args.offline) {
181
+ return { db, dbPath, stale: true, releaseOwner };
182
+ }
183
+ try {
184
+ const uploader = args.config.powersync.uploadBaseUrl
185
+ ? createPowerSyncUploadHandler({
186
+ authorization: args.config.auth.authorization,
187
+ uploadBaseUrl: args.config.powersync.uploadBaseUrl,
188
+ uploadV4Path: args.config.powersync.uploadV4Path,
189
+ uploadV2Path: args.config.powersync.uploadV2Path,
190
+ timeoutMs: args.timeoutMs,
191
+ })
192
+ : undefined;
193
+ const connector = new PowerSyncConnector(async () => {
194
+ return fetchCredentialsWithAuthorization({
195
+ authorization: args.config.auth.authorization,
196
+ endpoint: args.config.powersync.endpoint,
197
+ credentialsUrl: args.config.powersync.tokenEndpoint,
198
+ timeoutMs: args.timeoutMs,
199
+ });
200
+ }, uploader);
201
+ await db.connect(connector);
202
+ if (args.waitForInitialSync === false) {
203
+ const status = getStatusSnapshot(db);
204
+ return { db, dbPath, stale: !status.connected || !status.hasSynced, releaseOwner };
205
+ }
206
+ const { signal, cancel } = createTimeoutSignal(args.timeoutMs);
207
+ try {
208
+ await db.waitForFirstSync(signal);
209
+ const status = getStatusSnapshot(db);
210
+ return { db, dbPath, stale: signal.aborted || !status.connected, releaseOwner };
211
+ }
212
+ catch (error) {
213
+ if (signal.aborted) {
214
+ return { db, dbPath, stale: true, releaseOwner };
215
+ }
216
+ throw error;
217
+ }
218
+ finally {
219
+ cancel();
220
+ }
221
+ }
222
+ catch (error) {
223
+ return throwAfterPowerSyncCleanup(error, db, releaseOwner);
224
+ }
225
+ }
226
+ export async function waitForIdleDataFlow(db, timeoutMs) {
227
+ const { signal, cancel } = createTimeoutSignal(timeoutMs);
228
+ try {
229
+ await db.waitForStatus(status => {
230
+ const flow = status.dataFlowStatus;
231
+ return Boolean(status.connected) && !flow.downloading && !flow.uploading;
232
+ }, signal);
233
+ return !signal.aborted;
234
+ }
235
+ catch {
236
+ return false;
90
237
  }
91
238
  finally {
92
239
  cancel();
93
240
  }
94
241
  }
95
- export async function waitForIdleDataFlow(db, timeoutMs) {
242
+ export async function waitForDownloadIdle(db, timeoutMs) {
96
243
  const { signal, cancel } = createTimeoutSignal(timeoutMs);
97
244
  try {
98
245
  await db.waitForStatus(status => {
99
246
  const flow = status.dataFlowStatus;
100
- return !flow.downloading && !flow.uploading;
247
+ return Boolean(status.connected) && Boolean(status.hasSynced) && !flow.downloading;
248
+ }, signal);
249
+ return !signal.aborted;
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ finally {
255
+ cancel();
256
+ }
257
+ }
258
+ export async function waitForLastSyncedAfter(db, startedAtIso, timeoutMs) {
259
+ const startedAtMs = Date.parse(startedAtIso);
260
+ if (!Number.isFinite(startedAtMs)) {
261
+ return false;
262
+ }
263
+ // PowerSync reports checkpoint timestamps with whole-second precision.
264
+ const startedAtCheckpointPrecisionMs = Math.floor(startedAtMs / 1_000) * 1_000;
265
+ const status = getStatusSnapshot(db);
266
+ if (status.connected
267
+ && status.lastSyncedAt
268
+ && Date.parse(status.lastSyncedAt) >= startedAtCheckpointPrecisionMs) {
269
+ return true;
270
+ }
271
+ const { signal, cancel } = createTimeoutSignal(timeoutMs);
272
+ try {
273
+ await db.waitForStatus((nextStatus) => {
274
+ const value = nextStatus.lastSyncedAt;
275
+ const lastSyncedAt = value instanceof Date ? value.toISOString() : value ?? null;
276
+ return Boolean(nextStatus.connected
277
+ && lastSyncedAt
278
+ && Date.parse(lastSyncedAt) >= startedAtCheckpointPrecisionMs);
101
279
  }, signal);
102
280
  return !signal.aborted;
103
281
  }
@@ -108,6 +286,48 @@ export async function waitForIdleDataFlow(db, timeoutMs) {
108
286
  cancel();
109
287
  }
110
288
  }
289
+ export async function waitForUploadQueueEmpty(db, timeoutMs) {
290
+ const deadline = Date.now() + Math.max(0, timeoutMs);
291
+ let pendingCount = Number.POSITIVE_INFINITY;
292
+ while (Date.now() <= deadline) {
293
+ const status = getStatusSnapshot(db);
294
+ try {
295
+ const stats = await db.getUploadQueueStats(false);
296
+ pendingCount = stats.count;
297
+ if (status.dataFlowStatus.uploadError) {
298
+ return {
299
+ ok: false,
300
+ pendingCount,
301
+ uploadError: status.dataFlowStatus.uploadError,
302
+ timedOut: false,
303
+ };
304
+ }
305
+ if (stats.count === 0) {
306
+ return { ok: true, pendingCount: 0, timedOut: false };
307
+ }
308
+ }
309
+ catch (error) {
310
+ return {
311
+ ok: false,
312
+ pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
313
+ ...(status.dataFlowStatus.uploadError
314
+ ? { uploadError: status.dataFlowStatus.uploadError }
315
+ : {}),
316
+ queueError: {
317
+ name: error instanceof Error ? error.name : 'Error',
318
+ message: error instanceof Error ? error.message : String(error),
319
+ },
320
+ timedOut: false,
321
+ };
322
+ }
323
+ await new Promise((resolve) => setTimeout(resolve, 100));
324
+ }
325
+ return {
326
+ ok: false,
327
+ pendingCount: Number.isFinite(pendingCount) ? pendingCount : null,
328
+ timedOut: true,
329
+ };
330
+ }
111
331
  export async function withPowerSyncWriteTransaction(db, run) {
112
332
  const txDb = db;
113
333
  if (typeof txDb.writeTransaction === 'function') {
@@ -1,6 +1,6 @@
1
1
  import type { SyncStatusOptions } from '@powersync/common';
2
2
  import { DinoxError } from '../utils/errors.js';
3
- export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'data-flow-timeout' | 'not-synced' | 'download-error';
3
+ export type SyncGateFailureReason = 'offline' | 'first-sync-timeout' | 'checkpoint-timeout' | 'data-flow-timeout' | 'index-timeout' | 'not-connected' | 'not-synced' | 'download-error';
4
4
  export type SyncGateResult = {
5
5
  ok: boolean;
6
6
  reasons: SyncGateFailureReason[];
@@ -9,7 +9,10 @@ export type SyncGateInput = {
9
9
  offline: boolean;
10
10
  firstSyncStale: boolean;
11
11
  idle: boolean;
12
+ indexComplete?: boolean;
13
+ recentSync?: boolean;
12
14
  status: {
15
+ connected: boolean;
13
16
  hasSynced: boolean;
14
17
  dataFlowStatus: {
15
18
  downloadError?: {
@@ -7,12 +7,21 @@ export function evaluateSyncGate(input) {
7
7
  reasons: ['offline'],
8
8
  };
9
9
  }
10
- if (input.firstSyncStale) {
10
+ if (input.firstSyncStale && !input.status.hasSynced) {
11
11
  reasons.push('first-sync-timeout');
12
12
  }
13
+ if (input.recentSync === false) {
14
+ reasons.push('checkpoint-timeout');
15
+ }
13
16
  if (!input.idle) {
14
17
  reasons.push('data-flow-timeout');
15
18
  }
19
+ if (input.indexComplete === false) {
20
+ reasons.push('index-timeout');
21
+ }
22
+ if (!input.status.connected) {
23
+ reasons.push('not-connected');
24
+ }
16
25
  if (!input.status.hasSynced) {
17
26
  reasons.push('not-synced');
18
27
  }
@@ -31,6 +40,6 @@ export function createSyncRequiredError(gate, details) {
31
40
  reasons: gate.reasons,
32
41
  ...(details !== undefined ? { status: details } : {}),
33
42
  },
34
- suggestion: 'Run `dino sync --strict --sync-timeout 600000 --format json`, then retry the command.',
43
+ suggestion: 'Run `dino sync --strict --sync-timeout 20000 --format json`, then retry the command.',
35
44
  });
36
45
  }
@@ -2,6 +2,8 @@ import type { AbstractPowerSyncDatabase } from '@powersync/common';
2
2
  import { type NoteTokenIndexResult } from './tokenIndex.js';
3
3
  export type SyncPassResult = {
4
4
  idle: boolean;
5
+ downloadIdle: boolean;
6
+ uploadIdle: boolean;
5
7
  tokenIndex: NoteTokenIndexResult;
6
8
  };
7
9
  export declare function runSingleSyncPass(db: AbstractPowerSyncDatabase, timeoutMs: number): Promise<SyncPassResult>;
@@ -1,10 +1,21 @@
1
- import { waitForIdleDataFlow } from './runtime.js';
1
+ import { createTimeoutSignal } from '../utils/abort.js';
2
+ import { getStatusSnapshot, waitForDownloadIdle } from './runtime.js';
2
3
  import { syncNoteTokenIndex } from './tokenIndex.js';
3
4
  export async function runSingleSyncPass(db, timeoutMs) {
4
- const idle = await waitForIdleDataFlow(db, Math.min(timeoutMs, 60_000));
5
- const tokenIndex = await syncNoteTokenIndex(db);
5
+ const deadline = Date.now() + Math.max(1, timeoutMs);
6
+ const remainingMs = () => Math.max(0, deadline - Date.now());
7
+ const downloadIdle = await waitForDownloadIdle(db, Math.max(1, Math.min(remainingMs(), 60_000)));
8
+ const indexTimeoutMs = remainingMs();
9
+ const timeout = indexTimeoutMs > 0 ? createTimeoutSignal(indexTimeoutMs) : null;
10
+ const tokenIndex = await syncNoteTokenIndex(db, {
11
+ signal: timeout?.signal ?? AbortSignal.abort(),
12
+ }).finally(() => timeout?.cancel());
13
+ const status = getStatusSnapshot(db);
14
+ const uploadIdle = !status.dataFlowStatus.uploading;
6
15
  return {
7
- idle,
16
+ idle: downloadIdle && uploadIdle,
17
+ downloadIdle,
18
+ uploadIdle,
8
19
  tokenIndex,
9
20
  };
10
21
  }