@dinoxx/dinox-cli 1.1.1 → 2.1.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 (141) hide show
  1. package/README.md +44 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/auth/index.js +1 -2
  4. package/dist/commands/boxes/index.js +37 -13
  5. package/dist/commands/boxes/operations.d.ts +3 -3
  6. package/dist/commands/boxes/operations.js +51 -28
  7. package/dist/commands/info/index.js +30 -5
  8. package/dist/commands/meta/repo.js +2 -1
  9. package/dist/commands/notes/actions.js +20 -37
  10. package/dist/commands/notes/bulk.d.ts +2 -2
  11. package/dist/commands/notes/bulk.js +3 -18
  12. package/dist/commands/notes/contentPatch.d.ts +5 -1
  13. package/dist/commands/notes/contentPatch.js +12 -1
  14. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  15. package/dist/commands/notes/contentPatchCommand.js +29 -8
  16. package/dist/commands/notes/descriptors.js +19 -16
  17. package/dist/commands/notes/export.js +3 -0
  18. package/dist/commands/notes/repo/read.d.ts +8 -0
  19. package/dist/commands/notes/repo/read.js +321 -0
  20. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  21. package/dist/commands/notes/repo/resolve.js +187 -0
  22. package/dist/commands/notes/repo/search.d.ts +42 -0
  23. package/dist/commands/notes/repo/search.js +513 -0
  24. package/dist/commands/notes/repo/shared.d.ts +190 -0
  25. package/dist/commands/notes/repo/shared.js +102 -0
  26. package/dist/commands/notes/repo/write.d.ts +24 -0
  27. package/dist/commands/notes/repo/write.js +251 -0
  28. package/dist/commands/notes/repo.d.ts +7 -177
  29. package/dist/commands/notes/repo.js +7 -1310
  30. package/dist/commands/notes/searchOutput.d.ts +2 -1
  31. package/dist/commands/notes/searchOutput.js +2 -1
  32. package/dist/commands/notes/searchSql.js +9 -3
  33. package/dist/commands/prompt/index.d.ts +1 -0
  34. package/dist/commands/prompt/index.js +26 -11
  35. package/dist/commands/savedViews/index.d.ts +7 -0
  36. package/dist/commands/savedViews/index.js +458 -0
  37. package/dist/commands/savedViews/model.d.ts +87 -0
  38. package/dist/commands/savedViews/model.js +306 -0
  39. package/dist/commands/savedViews/repo.d.ts +123 -0
  40. package/dist/commands/savedViews/repo.js +512 -0
  41. package/dist/commands/savedViews/semantics.d.ts +46 -0
  42. package/dist/commands/savedViews/semantics.js +738 -0
  43. package/dist/commands/schema/commandMetadata.js +4 -4
  44. package/dist/commands/schema/index.d.ts +1 -0
  45. package/dist/commands/schema/index.js +23 -2
  46. package/dist/commands/schema/renderSkills.d.ts +6 -0
  47. package/dist/commands/schema/renderSkills.js +109 -11
  48. package/dist/commands/schema/spec.js +4 -0
  49. package/dist/commands/skills/index.d.ts +80 -0
  50. package/dist/commands/skills/index.js +572 -0
  51. package/dist/commands/storage/index.js +2 -2
  52. package/dist/commands/storage/repo/configs.d.ts +30 -0
  53. package/dist/commands/storage/repo/configs.js +259 -0
  54. package/dist/commands/storage/repo/operations.d.ts +25 -0
  55. package/dist/commands/storage/repo/operations.js +337 -0
  56. package/dist/commands/storage/repo/remote.d.ts +32 -0
  57. package/dist/commands/storage/repo/remote.js +132 -0
  58. package/dist/commands/storage/repo/resources.d.ts +12 -0
  59. package/dist/commands/storage/repo/resources.js +134 -0
  60. package/dist/commands/storage/repo/shared.d.ts +181 -0
  61. package/dist/commands/storage/repo/shared.js +36 -0
  62. package/dist/commands/storage/repo/targets.d.ts +12 -0
  63. package/dist/commands/storage/repo/targets.js +178 -0
  64. package/dist/commands/storage/repo.d.ts +3 -166
  65. package/dist/commands/storage/repo.js +4 -1034
  66. package/dist/commands/sync.js +2 -1
  67. package/dist/commands/tags/index.js +30 -11
  68. package/dist/commands/tags/operations.d.ts +3 -3
  69. package/dist/commands/tags/operations.js +45 -20
  70. package/dist/commands/templates/model.d.ts +50 -0
  71. package/dist/commands/templates/model.js +239 -0
  72. package/dist/commands/todo/index.js +3 -3
  73. package/dist/commands/update.d.ts +1 -0
  74. package/dist/commands/update.js +46 -9
  75. package/dist/commands/writeConfirmation.d.ts +13 -0
  76. package/dist/commands/writeConfirmation.js +54 -0
  77. package/dist/config/resolve.js +5 -10
  78. package/dist/config/types.d.ts +3 -6
  79. package/dist/constants/links.d.ts +18 -1
  80. package/dist/constants/links.js +26 -1
  81. package/dist/contract/snapshot.d.ts +30 -0
  82. package/dist/contract/snapshot.js +114 -0
  83. package/dist/daemon/client.d.ts +2 -2
  84. package/dist/daemon/client.js +108 -54
  85. package/dist/daemon/index.d.ts +5 -3
  86. package/dist/daemon/index.js +28 -5
  87. package/dist/daemon/lifecycle.d.ts +7 -2
  88. package/dist/daemon/lifecycle.js +53 -13
  89. package/dist/daemon/pid.d.ts +1 -1
  90. package/dist/daemon/pid.js +6 -3
  91. package/dist/dinox.js +9 -1
  92. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  93. package/dist/powersync/exclusiveRuntime.js +26 -8
  94. package/dist/powersync/runtime.d.ts +3 -2
  95. package/dist/powersync/runtime.js +19 -3
  96. package/dist/powersync/schema/content.d.ts +1 -78
  97. package/dist/powersync/schema/content.js +1 -78
  98. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  99. package/dist/powersync/schema/electronCanonical.js +1154 -0
  100. package/dist/powersync/schema/index.d.ts +573 -129
  101. package/dist/powersync/schema/index.js +3 -32
  102. package/dist/powersync/schema/local.d.ts +1 -56
  103. package/dist/powersync/schema/local.js +1 -60
  104. package/dist/powersync/schema/note.d.ts +1 -35
  105. package/dist/powersync/schema/note.js +1 -35
  106. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  107. package/dist/powersync/schema/notesExtras.js +1 -71
  108. package/dist/powersync/schema/projects.d.ts +1 -104
  109. package/dist/powersync/schema/projects.js +1 -104
  110. package/dist/powersync/schema/savedViews.d.ts +1 -0
  111. package/dist/powersync/schema/savedViews.js +1 -0
  112. package/dist/powersync/schema/storage.d.ts +1 -24
  113. package/dist/powersync/schema/storage.js +1 -24
  114. package/dist/powersync/schema/tags.d.ts +1 -37
  115. package/dist/powersync/schema/tags.js +1 -37
  116. package/dist/powersync/syncGate.d.ts +20 -2
  117. package/dist/powersync/uploader.d.ts +16 -2
  118. package/dist/powersync/uploader.js +152 -158
  119. package/dist/skills/check.d.ts +1 -0
  120. package/dist/skills/check.js +538 -70
  121. package/dist/skills/commandRefs.d.ts +21 -0
  122. package/dist/skills/commandRefs.js +132 -0
  123. package/dist/skills/generate.d.ts +13 -0
  124. package/dist/skills/generate.js +80 -0
  125. package/dist/skills/registry.d.ts +13 -0
  126. package/dist/skills/registry.js +58 -0
  127. package/dist/skills/retired.d.ts +60 -0
  128. package/dist/skills/retired.js +58 -0
  129. package/dist/utils/deadline.d.ts +8 -0
  130. package/dist/utils/deadline.js +31 -0
  131. package/dist/utils/dryRun.d.ts +20 -1
  132. package/dist/utils/dryRun.js +22 -3
  133. package/dist/utils/errorCodes.d.ts +311 -0
  134. package/dist/utils/errorCodes.js +92 -0
  135. package/dist/utils/errors.d.ts +15 -2
  136. package/dist/utils/errors.js +13 -0
  137. package/dist/utils/processExit.d.ts +1 -0
  138. package/dist/utils/processExit.js +33 -0
  139. package/dist/utils/updateNotice.d.ts +1 -0
  140. package/dist/utils/updateNotice.js +1 -1
  141. package/package.json +18 -8
@@ -12,6 +12,7 @@ import { loadConfig } from '../config/store.js';
12
12
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
13
13
  import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
14
14
  import { withAsyncRelease } from '../utils/asyncRelease.js';
15
+ import { remainingDeadlineMs } from '../utils/deadline.js';
15
16
  import { getPackageVersion } from '../utils/version.js';
16
17
  const DAEMON_STOP_POLL_INTERVAL_MS = 100;
17
18
  const DAEMON_STOP_TIMEOUT_MS = 5_000;
@@ -63,9 +64,11 @@ async function waitForDaemonExit(instanceId, timeoutMs) {
63
64
  const info = await getDaemonInfo();
64
65
  return !info || info.instanceId !== instanceId;
65
66
  }
66
- async function expectedDaemonIdentity(baseConfig) {
67
+ async function expectedDaemonIdentity(baseConfig, timeoutMs) {
67
68
  const resolved = baseConfig ?? resolveConfig(await loadConfig());
68
- const { config } = await hydrateResolvedAuthIdentity(resolved, { timeoutMs: resolved.sync.timeoutMs });
69
+ const { config } = await hydrateResolvedAuthIdentity(resolved, {
70
+ timeoutMs: timeoutMs ?? resolved.sync.timeoutMs,
71
+ });
69
72
  return {
70
73
  version: getPackageVersion(),
71
74
  userId: config.auth.userId ?? null,
@@ -82,9 +85,29 @@ function matchesRuntimeTokenBinding(info, version, config) {
82
85
  && (!config.auth.userId || info.userId === config.auth.userId));
83
86
  }
84
87
  export async function startDaemonLocked(port, detached, options = {}) {
88
+ const maximumTimeoutMs = options.timeoutMs === undefined
89
+ ? undefined
90
+ : Math.max(0, Math.floor(options.timeoutMs));
91
+ const deadlineAtMs = maximumTimeoutMs === undefined
92
+ ? undefined
93
+ : Date.now() + maximumTimeoutMs;
94
+ const remainingStartTimeoutMs = (phase) => {
95
+ if (deadlineAtMs === undefined || maximumTimeoutMs === undefined) {
96
+ return undefined;
97
+ }
98
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
99
+ if (remainingMs <= 0) {
100
+ throw new DinoxError('Failed to start daemon before the caller deadline expired.', {
101
+ code: 'DAEMON_START_TIMEOUT',
102
+ details: { phase },
103
+ recoverable: true,
104
+ });
105
+ }
106
+ return remainingMs;
107
+ };
85
108
  const baseConfig = resolveConfig(await loadConfig());
86
109
  const version = getPackageVersion();
87
- const runningInfo = await getDaemonInfo();
110
+ const runningInfo = await getDaemonInfo(remainingStartTimeoutMs('initial-ping'));
88
111
  if (runningInfo) {
89
112
  if (matchesRuntimeTokenBinding(runningInfo, version, baseConfig)) {
90
113
  return { running: true, port, socketPath: getDaemonSocketPath(), transport: 'socket', detached, alreadyRunning: true };
@@ -94,7 +117,7 @@ export async function startDaemonLocked(port, detached, options = {}) {
94
117
  recoverable: true,
95
118
  });
96
119
  }
97
- const expected = await expectedDaemonIdentity(baseConfig);
120
+ const expected = await expectedDaemonIdentity(baseConfig, remainingStartTimeoutMs('auth-hydration'));
98
121
  if (detached) {
99
122
  const args = ['daemon', 'start', '--port', String(port), '--no-detach'];
100
123
  const entrypoint = process.argv[1];
@@ -107,7 +130,7 @@ export async function startDaemonLocked(port, detached, options = {}) {
107
130
  env: { ...process.env, [DAEMON_CHILD_ENV]: '1' },
108
131
  });
109
132
  child.unref();
110
- const ready = await waitForFreshDaemon(expected.version, expected.userId, expected.tokenFingerprint, expected.runtimeConfigFingerprint);
133
+ const ready = await waitForFreshDaemon(expected.version, expected.userId, expected.tokenFingerprint, expected.runtimeConfigFingerprint, remainingStartTimeoutMs('ready-poll'));
111
134
  if (!ready) {
112
135
  child.kill('SIGTERM');
113
136
  throw new DinoxError('Failed to start daemon');
@@ -28,17 +28,22 @@ type DaemonLifecycleDependencies = {
28
28
  getPackageVersion: typeof getPackageVersion;
29
29
  printWarning: typeof printHumanReadableWarning;
30
30
  };
31
- export declare function captureDaemonSnapshot(overrides?: Partial<DaemonLifecycleDependencies>): Promise<DaemonSnapshot>;
31
+ export declare function captureDaemonSnapshot(overrides?: Partial<DaemonLifecycleDependencies>, timeoutMs?: number): Promise<DaemonSnapshot>;
32
32
  export declare function daemonSnapshotIsFresh(config: ResolvedDinoxConfig, snapshot: DaemonSnapshot, version?: string): boolean;
33
33
  /** Caller must hold the daemon start lock until restore completes. */
34
34
  export declare function suspendDaemonForExclusiveAccessLocked(config: ResolvedDinoxConfig, timeoutMs: number, options?: {
35
35
  snapshot?: DaemonSnapshot;
36
36
  forceStop?: boolean;
37
+ deadlineAtMs?: number;
37
38
  dependencies?: Partial<DaemonLifecycleDependencies>;
38
39
  }): Promise<SuspendedDaemon>;
39
40
  /** Caller must hold the daemon start lock. */
40
- export declare function restoreSuspendedDaemonLocked(suspended: SuspendedDaemon, overrides?: Partial<DaemonLifecycleDependencies>): Promise<boolean>;
41
+ export declare function restoreSuspendedDaemonLocked(suspended: SuspendedDaemon, overrides?: Partial<DaemonLifecycleDependencies>, options?: {
42
+ timeoutMs?: number;
43
+ }): Promise<boolean>;
41
44
  export declare function withDaemonExclusiveAccess<T>(config: ResolvedDinoxConfig, timeoutMs: number, run: (control: DaemonExclusiveAccessControl) => Promise<T>, overrides?: Partial<DaemonLifecycleDependencies>, options?: {
42
45
  deadlineAtMs?: number;
46
+ operationDeadlineAtMs?: number;
47
+ cleanupFailureMode?: 'warn' | 'throw';
43
48
  }): Promise<T>;
44
49
  export {};
@@ -1,6 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { getPowerSyncDbPath } from '../config/paths.js';
3
3
  import { fingerprintDaemonRuntimeConfig } from '../config/resolve.js';
4
+ import { remainingDeadlineMs, runWithinDeadline } from '../utils/deadline.js';
5
+ import { DinoxError } from '../utils/errors.js';
4
6
  import { printHumanReadableWarning } from '../utils/output.js';
5
7
  import { getPackageVersion } from '../utils/version.js';
6
8
  import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess, } from './client.js';
@@ -22,6 +24,21 @@ function dependencies(overrides) {
22
24
  function errorMessage(error) {
23
25
  return error instanceof Error ? error.message : String(error);
24
26
  }
27
+ function createLifecycleDeadlineError(phase, action) {
28
+ return new DinoxError(`Sync deadline exceeded while ${action}.`, {
29
+ code: 'SYNC_TIMEOUT',
30
+ details: { phase },
31
+ suggestion: 'Run `dino doctor --format json` to verify the local runtime before retrying sync.',
32
+ recoverable: true,
33
+ });
34
+ }
35
+ function requireRemainingDeadlineMs(deadlineAtMs, maximumTimeoutMs, phase, action) {
36
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
37
+ if (deadlineAtMs !== undefined && remainingMs <= 0) {
38
+ throw createLifecycleDeadlineError(phase, action);
39
+ }
40
+ return Math.max(1, remainingMs);
41
+ }
25
42
  function snapshotPort(snapshot) {
26
43
  const pidFileMatches = Boolean(snapshot.info
27
44
  && snapshot.pidInfo
@@ -29,10 +46,10 @@ function snapshotPort(snapshot) {
29
46
  && snapshot.pidInfo.instanceId === snapshot.info.instanceId);
30
47
  return pidFileMatches ? snapshot.pidInfo?.port ?? DEFAULT_DAEMON_PORT : DEFAULT_DAEMON_PORT;
31
48
  }
32
- export async function captureDaemonSnapshot(overrides = {}) {
49
+ export async function captureDaemonSnapshot(overrides = {}, timeoutMs) {
33
50
  const deps = dependencies(overrides);
34
51
  return {
35
- info: await deps.getDaemonInfo(),
52
+ info: await deps.getDaemonInfo(timeoutMs),
36
53
  pidInfo: deps.readPid(),
37
54
  socketExists: process.platform !== 'win32' && existsSync(getDaemonSocketPath()),
38
55
  };
@@ -44,12 +61,13 @@ export function daemonSnapshotIsFresh(config, snapshot, version = getPackageVers
44
61
  /** Caller must hold the daemon start lock until restore completes. */
45
62
  export async function suspendDaemonForExclusiveAccessLocked(config, timeoutMs, options = {}) {
46
63
  const deps = dependencies(options.dependencies ?? {});
47
- const snapshot = options.snapshot ?? await captureDaemonSnapshot(deps);
64
+ const remainingTimeoutMs = (phase, action) => requireRemainingDeadlineMs(options.deadlineAtMs, timeoutMs, phase, action);
65
+ const snapshot = options.snapshot ?? await captureDaemonSnapshot(deps, remainingTimeoutMs('daemon-snapshot', 'inspecting the background daemon'));
48
66
  const wasFresh = daemonSnapshotIsFresh(config, snapshot, deps.getPackageVersion());
49
67
  const daemonUsesTargetDatabase = !snapshot.info
50
68
  || getPowerSyncDbPath(snapshot.info.userId) === getPowerSyncDbPath(config.auth.userId);
51
69
  const wasStopped = options.forceStop || daemonUsesTargetDatabase
52
- ? await deps.shutdownDaemon(Math.min(timeoutMs, 10_000), snapshot.info?.instanceId)
70
+ ? await deps.shutdownDaemon(Math.min(remainingTimeoutMs('daemon-shutdown', 'stopping the background daemon'), 10_000), snapshot.info?.instanceId)
53
71
  : false;
54
72
  return {
55
73
  snapshot,
@@ -59,22 +77,26 @@ export async function suspendDaemonForExclusiveAccessLocked(config, timeoutMs, o
59
77
  };
60
78
  }
61
79
  /** Caller must hold the daemon start lock. */
62
- export async function restoreSuspendedDaemonLocked(suspended, overrides = {}) {
80
+ export async function restoreSuspendedDaemonLocked(suspended, overrides = {}, options = {}) {
63
81
  if (!suspended.wasStopped || !suspended.wasFresh) {
64
82
  return false;
65
83
  }
66
84
  const deps = dependencies(overrides);
67
- await deps.startDaemonLocked(suspended.port, true, { quiet: true });
85
+ await deps.startDaemonLocked(suspended.port, true, {
86
+ quiet: true,
87
+ timeoutMs: options.timeoutMs,
88
+ });
68
89
  return true;
69
90
  }
70
91
  export async function withDaemonExclusiveAccess(config, timeoutMs, run, overrides = {}, options = {}) {
71
92
  const deps = dependencies(overrides);
72
- const remainingTimeoutMs = () => options.deadlineAtMs === undefined
73
- ? timeoutMs
74
- : Math.max(1, Math.min(timeoutMs, options.deadlineAtMs - Date.now()));
75
- const releaseStartLock = await deps.acquireStartLock(remainingTimeoutMs());
93
+ const operationDeadlineAtMs = options.operationDeadlineAtMs ?? options.deadlineAtMs;
94
+ const remainingOperationTimeoutMs = (phase, action) => requireRemainingDeadlineMs(operationDeadlineAtMs, timeoutMs, phase, action);
95
+ const remainingCleanupTimeoutMs = () => Math.max(1, remainingDeadlineMs(options.deadlineAtMs, timeoutMs));
96
+ const releaseStartLock = await deps.acquireStartLock(remainingOperationTimeoutMs('daemon-lock-acquire', 'acquiring the daemon lifecycle lock'));
76
97
  let suspended = null;
77
98
  let restoreAllowed = true;
99
+ let cleanupError;
78
100
  let outcome;
79
101
  const control = {
80
102
  suppressRestore() {
@@ -82,7 +104,8 @@ export async function withDaemonExclusiveAccess(config, timeoutMs, run, override
82
104
  },
83
105
  };
84
106
  try {
85
- suspended = await suspendDaemonForExclusiveAccessLocked(config, remainingTimeoutMs(), {
107
+ suspended = await suspendDaemonForExclusiveAccessLocked(config, remainingOperationTimeoutMs('daemon-suspend', 'suspending the background daemon'), {
108
+ deadlineAtMs: operationDeadlineAtMs,
86
109
  dependencies: deps,
87
110
  });
88
111
  try {
@@ -97,22 +120,39 @@ export async function withDaemonExclusiveAccess(config, timeoutMs, run, override
97
120
  }
98
121
  finally {
99
122
  if (suspended && restoreAllowed) {
123
+ const suspendedDaemon = suspended;
100
124
  try {
101
- await restoreSuspendedDaemonLocked(suspended, deps);
125
+ await runWithinDeadline(() => restoreSuspendedDaemonLocked(suspendedDaemon, deps, {
126
+ timeoutMs: remainingCleanupTimeoutMs(),
127
+ }), {
128
+ deadlineAtMs: options.deadlineAtMs,
129
+ maximumTimeoutMs: timeoutMs,
130
+ createTimeoutError: () => createLifecycleDeadlineError('daemon-restore', 'restoring the background daemon'),
131
+ });
102
132
  }
103
133
  catch (error) {
134
+ cleanupError = error;
104
135
  deps.printWarning(`warning: failed to restore daemon after local command: ${errorMessage(error)}`);
105
136
  }
106
137
  }
107
138
  try {
108
- await releaseStartLock();
139
+ await runWithinDeadline(releaseStartLock, {
140
+ deadlineAtMs: options.deadlineAtMs,
141
+ maximumTimeoutMs: timeoutMs,
142
+ createTimeoutError: () => createLifecycleDeadlineError('daemon-lock-release', 'releasing the daemon lifecycle lock'),
143
+ runWhenExpired: true,
144
+ });
109
145
  }
110
146
  catch (error) {
147
+ cleanupError ??= error;
111
148
  deps.printWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(error)}`);
112
149
  }
113
150
  }
114
151
  if (!outcome.ok) {
115
152
  throw outcome.error;
116
153
  }
154
+ if (cleanupError && options.cleanupFailureMode === 'throw') {
155
+ throw cleanupError;
156
+ }
117
157
  return outcome.value;
118
158
  }
@@ -19,5 +19,5 @@ export declare function removePid(expectedInstanceId?: string, options?: {
19
19
  expectedPid?: number;
20
20
  }): boolean;
21
21
  export declare function isProcessRunning(pid: number): boolean;
22
- export declare function isLegacyDaemonProcess(pid: number, daemonUptimeMs: number): Promise<boolean>;
22
+ export declare function isLegacyDaemonProcess(pid: number, daemonUptimeMs: number, timeoutMs?: number): Promise<boolean>;
23
23
  export declare function acquireDaemonStartLock(timeoutMs?: number): Promise<() => Promise<void>>;
@@ -139,10 +139,13 @@ function commandLooksLikeLegacyDaemon(command) {
139
139
  return /(?:^|[\\/\s])dino(?:x)?(?:\.[cm]?[jt]s)?["']?\s+daemon\s+start(?:\s|$)/i.test(command)
140
140
  && /(?:^|\s)--no-detach(?:\s|$)/.test(command);
141
141
  }
142
- export async function isLegacyDaemonProcess(pid, daemonUptimeMs) {
142
+ export async function isLegacyDaemonProcess(pid, daemonUptimeMs, timeoutMs = 2_000) {
143
143
  if (!Number.isInteger(pid) || pid <= 0 || !Number.isFinite(daemonUptimeMs) || daemonUptimeMs < 0) {
144
144
  return false;
145
145
  }
146
+ const commandTimeoutMs = Number.isFinite(timeoutMs)
147
+ ? Math.max(1, Math.min(2_000, Math.floor(timeoutMs)))
148
+ : 2_000;
146
149
  try {
147
150
  if (process.platform === 'win32') {
148
151
  const command = [
@@ -152,7 +155,7 @@ export async function isLegacyDaemonProcess(pid, daemonUptimeMs) {
152
155
  ' [Console]::Write($p.CommandLine);',
153
156
  '}',
154
157
  ].join(' ');
155
- const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], { encoding: 'utf8', timeout: 2_000, maxBuffer: 64 * 1024 });
158
+ const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], { encoding: 'utf8', timeout: commandTimeoutMs, maxBuffer: 64 * 1024 });
156
159
  const newline = stdout.indexOf('\n');
157
160
  if (newline < 0) {
158
161
  return false;
@@ -163,7 +166,7 @@ export async function isLegacyDaemonProcess(pid, daemonUptimeMs) {
163
166
  && Math.abs(processUptimeMs - daemonUptimeMs) <= LEGACY_DAEMON_UPTIME_TOLERANCE_MS
164
167
  && commandLooksLikeLegacyDaemon(stdout.slice(newline + 1).trim());
165
168
  }
166
- const { stdout } = await execFileAsync('ps', ['-ww', '-p', String(pid), '-o', 'etime=', '-o', 'command='], { encoding: 'utf8', timeout: 2_000, maxBuffer: 64 * 1024 });
169
+ const { stdout } = await execFileAsync('ps', ['-ww', '-p', String(pid), '-o', 'etime=', '-o', 'command='], { encoding: 'utf8', timeout: commandTimeoutMs, maxBuffer: 64 * 1024 });
167
170
  const match = stdout.trim().match(/^(\S+)\s+([\s\S]+)$/);
168
171
  if (!match) {
169
172
  return false;
package/dist/dinox.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { createCli } from './cli.js';
3
3
  import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
4
4
  import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, printHumanReadableError, setRequestedStructuredOutputFormat, } from './utils/output.js';
5
+ import { exitProcessAfterFlushing } from './utils/processExit.js';
5
6
  function normalizeArgv(argv) {
6
7
  // pnpm/npm script forwarding can prepend a standalone "--".
7
8
  if (argv[0] === '--') {
@@ -25,7 +26,12 @@ function isCommanderError(error) {
25
26
  && error.code.startsWith('commander.')
26
27
  && typeof error.exitCode === 'number';
27
28
  }
28
- main().catch((error) => {
29
+ async function exitAfterFatalCleanup(error, exitCode) {
30
+ if (isDinoxError(error) && error.requiresProcessExit) {
31
+ await exitProcessAfterFlushing(exitCode);
32
+ }
33
+ }
34
+ main().catch(async (error) => {
29
35
  if (isCommanderError(error)) {
30
36
  process.exitCode = error.exitCode === 0 ? 0 : 2;
31
37
  return;
@@ -46,6 +52,7 @@ main().catch((error) => {
46
52
  setRequestedStructuredOutputFormat(format);
47
53
  printError(payload, format);
48
54
  clearRequestedStructuredOutputFormat();
55
+ await exitAfterFatalCleanup(error, exitCode);
49
56
  return;
50
57
  }
51
58
  printHumanReadableError(payload.message);
@@ -59,4 +66,5 @@ main().catch((error) => {
59
66
  if (verbose && payload.stack) {
60
67
  printHumanReadableError(payload.stack);
61
68
  }
69
+ await exitAfterFatalCleanup(error, exitCode);
62
70
  });
@@ -13,6 +13,7 @@ export declare function withExclusivePowerSync<T>(args: {
13
13
  offline: boolean;
14
14
  timeoutMs: number;
15
15
  deadlineAtMs?: number;
16
+ operationDeadlineAtMs?: number;
16
17
  waitForInitialSync?: boolean;
17
18
  expectedTokenFingerprint?: string | null;
18
19
  cleanupFailureMode?: 'warn' | 'throw';
@@ -1,4 +1,6 @@
1
1
  import { withDaemonExclusiveAccess } from '../daemon/lifecycle.js';
2
+ import { runWithinDeadline, remainingDeadlineMs } from '../utils/deadline.js';
3
+ import { DinoxError } from '../utils/errors.js';
2
4
  import { printHumanReadableWarning } from '../utils/output.js';
3
5
  import { closePowerSyncAndReleaseOwner, connectPowerSync, } from './runtime.js';
4
6
  const exclusivePowerSyncDependencies = {
@@ -10,16 +12,24 @@ const exclusivePowerSyncDependencies = {
10
12
  function errorMessage(error) {
11
13
  return error instanceof Error ? error.message : String(error);
12
14
  }
15
+ function createRuntimeCleanupDeadlineError() {
16
+ return new DinoxError('Sync deadline exceeded while closing the local PowerSync runtime.', {
17
+ code: 'SYNC_TIMEOUT',
18
+ details: { phase: 'powersync-cleanup' },
19
+ suggestion: 'Run `dino doctor --format json` to verify the local runtime before retrying sync.',
20
+ recoverable: true,
21
+ requiresProcessExit: true,
22
+ });
23
+ }
13
24
  export async function withExclusivePowerSync(args, run, overrides = {}) {
14
25
  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) => {
26
+ const { cleanupFailureMode, deadlineAtMs, operationDeadlineAtMs = deadlineAtMs, ...connectArgs } = args;
27
+ const remainingOperationTimeoutMs = () => Math.max(1, remainingDeadlineMs(operationDeadlineAtMs, args.timeoutMs));
28
+ const remainingTotalTimeoutMs = () => Math.max(1, remainingDeadlineMs(deadlineAtMs, args.timeoutMs));
29
+ return dependencies.withDaemonExclusiveAccess(args.config, remainingTotalTimeoutMs(), async (daemonLifecycle) => {
20
30
  const connected = await dependencies.connectPowerSync({
21
31
  ...connectArgs,
22
- timeoutMs: remainingTimeoutMs(),
32
+ timeoutMs: remainingOperationTimeoutMs(),
23
33
  });
24
34
  let outcome;
25
35
  try {
@@ -30,7 +40,11 @@ export async function withExclusivePowerSync(args, run, overrides = {}) {
30
40
  }
31
41
  let cleanupError;
32
42
  try {
33
- await dependencies.closePowerSyncAndReleaseOwner(connected.db, connected.releaseOwner);
43
+ await runWithinDeadline(() => dependencies.closePowerSyncAndReleaseOwner(connected.db, connected.releaseOwner), {
44
+ deadlineAtMs,
45
+ maximumTimeoutMs: args.timeoutMs,
46
+ createTimeoutError: createRuntimeCleanupDeadlineError,
47
+ });
34
48
  }
35
49
  catch (error) {
36
50
  cleanupError = error;
@@ -44,5 +58,9 @@ export async function withExclusivePowerSync(args, run, overrides = {}) {
44
58
  throw cleanupError;
45
59
  }
46
60
  return outcome.value;
47
- }, {}, { deadlineAtMs });
61
+ }, {}, {
62
+ deadlineAtMs,
63
+ operationDeadlineAtMs,
64
+ cleanupFailureMode,
65
+ });
48
66
  }
@@ -1,6 +1,7 @@
1
1
  import { PowerSyncDatabase } from '@powersync/node';
2
- import type { AbstractPowerSyncDatabase, SyncStatusOptions } from '@powersync/common';
2
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
3
3
  import type { ResolvedDinoxConfig } from '../config/types.js';
4
+ import type { SyncStatusRaw } from './syncGate.js';
4
5
  export type ConnectResult = {
5
6
  db: PowerSyncDatabase;
6
7
  dbPath: string;
@@ -29,7 +30,7 @@ export declare function getStatusSnapshot(db: AbstractPowerSyncDatabase): {
29
30
  message: string;
30
31
  };
31
32
  };
32
- raw: SyncStatusOptions;
33
+ raw: SyncStatusRaw;
33
34
  };
34
35
  export declare function connectPowerSync(args: {
35
36
  config: ResolvedDinoxConfig;
@@ -88,8 +88,25 @@ async function ensureDataDir() {
88
88
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
89
89
  return dir;
90
90
  }
91
+ function readSyncStatus(status) {
92
+ if (!status)
93
+ return {};
94
+ return {
95
+ connected: status.connected,
96
+ connecting: status.connecting,
97
+ hasSynced: status.hasSynced,
98
+ lastSyncedAt: status.lastSyncedAt,
99
+ dataFlow: {
100
+ downloading: status.downloading,
101
+ uploading: status.uploading,
102
+ downloadError: status.downloadError,
103
+ uploadError: status.uploadError,
104
+ },
105
+ priorityStatusEntries: status.priorityStatusEntries,
106
+ };
107
+ }
91
108
  export function getStatusSnapshot(db) {
92
- const raw = db.currentStatus?.toJSON?.() ?? {};
109
+ const raw = readSyncStatus(db.currentStatus);
93
110
  const lastSyncedAt = raw.lastSyncedAt instanceof Date ? raw.lastSyncedAt.toISOString() : raw.lastSyncedAt ?? null;
94
111
  const dataFlow = raw.dataFlow ?? {};
95
112
  return {
@@ -185,8 +202,7 @@ export async function connectPowerSync(args) {
185
202
  ? createPowerSyncUploadHandler({
186
203
  authorization: args.config.auth.authorization,
187
204
  uploadBaseUrl: args.config.powersync.uploadBaseUrl,
188
- uploadV4Path: args.config.powersync.uploadV4Path,
189
- uploadV2Path: args.config.powersync.uploadV2Path,
205
+ uploadPath: args.config.powersync.uploadPath,
190
206
  timeoutMs: args.timeoutMs,
191
207
  })
192
208
  : undefined;
@@ -1,78 +1 @@
1
- import { Table } from '@powersync/common';
2
- export declare const c_card_box: Table<{
3
- id: import("@powersync/common").BaseColumnType<string | null>;
4
- name: import("@powersync/common").BaseColumnType<string | null>;
5
- type: import("@powersync/common").BaseColumnType<string | null>;
6
- cover_url: import("@powersync/common").BaseColumnType<string | null>;
7
- prompt: import("@powersync/common").BaseColumnType<string | null>;
8
- headers: import("@powersync/common").BaseColumnType<string | null>;
9
- priority: import("@powersync/common").BaseColumnType<number | null>;
10
- created_at: import("@powersync/common").BaseColumnType<string | null>;
11
- updated_at: import("@powersync/common").BaseColumnType<string | null>;
12
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
13
- user_no: import("@powersync/common").BaseColumnType<string | null>;
14
- is_del: import("@powersync/common").BaseColumnType<number | null>;
15
- template_id: import("@powersync/common").BaseColumnType<string | null>;
16
- }>;
17
- export declare const c_card_note: Table<{
18
- id: import("@powersync/common").BaseColumnType<string | null>;
19
- box_id: import("@powersync/common").BaseColumnType<string | null>;
20
- content: import("@powersync/common").BaseColumnType<string | null>;
21
- created_at: import("@powersync/common").BaseColumnType<string | null>;
22
- updated_at: import("@powersync/common").BaseColumnType<string | null>;
23
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
24
- user_no: import("@powersync/common").BaseColumnType<string | null>;
25
- is_del: import("@powersync/common").BaseColumnType<number | null>;
26
- origin_note_id: import("@powersync/common").BaseColumnType<string | null>;
27
- origin_type: import("@powersync/common").BaseColumnType<string | null>;
28
- output_type: import("@powersync/common").BaseColumnType<string | null>;
29
- is_array: import("@powersync/common").BaseColumnType<number | null>;
30
- }>;
31
- export declare const c_cmd: Table<{
32
- id: import("@powersync/common").BaseColumnType<string | null>;
33
- zone: import("@powersync/common").BaseColumnType<string | null>;
34
- type: import("@powersync/common").BaseColumnType<string | null>;
35
- cmd: import("@powersync/common").BaseColumnType<string | null>;
36
- name: import("@powersync/common").BaseColumnType<string | null>;
37
- priority: import("@powersync/common").BaseColumnType<number | null>;
38
- is_show: import("@powersync/common").BaseColumnType<number | null>;
39
- description: import("@powersync/common").BaseColumnType<string | null>;
40
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
41
- user_id: import("@powersync/common").BaseColumnType<string | null>;
42
- insert_at: import("@powersync/common").BaseColumnType<string | null>;
43
- update_at: import("@powersync/common").BaseColumnType<string | null>;
44
- is_del: import("@powersync/common").BaseColumnType<number | null>;
45
- template_id: import("@powersync/common").BaseColumnType<string | null>;
46
- meta_data: import("@powersync/common").BaseColumnType<string | null>;
47
- output: import("@powersync/common").BaseColumnType<string | null>;
48
- output_param: import("@powersync/common").BaseColumnType<string | null>;
49
- category: import("@powersync/common").BaseColumnType<string | null>;
50
- }>;
51
- export declare const c_note_template: Table<{
52
- id: import("@powersync/common").BaseColumnType<string | null>;
53
- content: import("@powersync/common").BaseColumnType<string | null>;
54
- name: import("@powersync/common").BaseColumnType<string | null>;
55
- priority: import("@powersync/common").BaseColumnType<number | null>;
56
- is_show: import("@powersync/common").BaseColumnType<number | null>;
57
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
58
- user_id: import("@powersync/common").BaseColumnType<string | null>;
59
- insert_at: import("@powersync/common").BaseColumnType<string | null>;
60
- update_at: import("@powersync/common").BaseColumnType<string | null>;
61
- is_del: import("@powersync/common").BaseColumnType<number | null>;
62
- }>;
63
- export declare const c_zettel_box: Table<{
64
- id: import("@powersync/common").BaseColumnType<string | null>;
65
- name: import("@powersync/common").BaseColumnType<string | null>;
66
- path: import("@powersync/common").BaseColumnType<string | null>;
67
- parent_id: import("@powersync/common").BaseColumnType<string | null>;
68
- description: import("@powersync/common").BaseColumnType<string | null>;
69
- color: import("@powersync/common").BaseColumnType<string | null>;
70
- priority: import("@powersync/common").BaseColumnType<number | null>;
71
- is_show: import("@powersync/common").BaseColumnType<number | null>;
72
- cover_url: import("@powersync/common").BaseColumnType<string | null>;
73
- created_at: import("@powersync/common").BaseColumnType<string | null>;
74
- updated_at: import("@powersync/common").BaseColumnType<string | null>;
75
- extra_data: import("@powersync/common").BaseColumnType<string | null>;
76
- user_no: import("@powersync/common").BaseColumnType<string | null>;
77
- is_del: import("@powersync/common").BaseColumnType<number | null>;
78
- }>;
1
+ export { c_card_box, c_card_note, c_cmd, c_note_template, c_zettel_box, } from './electronCanonical.js';
@@ -1,78 +1 @@
1
- import { column, Table } from '@powersync/common';
2
- export const c_card_box = new Table({
3
- id: column.text,
4
- name: column.text,
5
- type: column.text,
6
- cover_url: column.text,
7
- prompt: column.text,
8
- headers: column.text,
9
- priority: column.integer,
10
- created_at: column.text,
11
- updated_at: column.text,
12
- extra_data: column.text,
13
- user_no: column.text,
14
- is_del: column.integer,
15
- template_id: column.text,
16
- });
17
- export const c_card_note = new Table({
18
- id: column.text,
19
- box_id: column.text,
20
- content: column.text,
21
- created_at: column.text,
22
- updated_at: column.text,
23
- extra_data: column.text,
24
- user_no: column.text,
25
- is_del: column.integer,
26
- origin_note_id: column.text,
27
- origin_type: column.text,
28
- output_type: column.text,
29
- is_array: column.integer,
30
- });
31
- export const c_cmd = new Table({
32
- id: column.text,
33
- zone: column.text,
34
- type: column.text,
35
- cmd: column.text,
36
- name: column.text,
37
- priority: column.integer,
38
- is_show: column.integer,
39
- description: column.text,
40
- extra_data: column.text,
41
- user_id: column.text,
42
- insert_at: column.text,
43
- update_at: column.text,
44
- is_del: column.integer,
45
- template_id: column.text,
46
- meta_data: column.text,
47
- output: column.text,
48
- output_param: column.text,
49
- category: column.text,
50
- });
51
- export const c_note_template = new Table({
52
- id: column.text,
53
- content: column.text,
54
- name: column.text,
55
- priority: column.integer,
56
- is_show: column.integer,
57
- extra_data: column.text,
58
- user_id: column.text,
59
- insert_at: column.text,
60
- update_at: column.text,
61
- is_del: column.integer,
62
- });
63
- export const c_zettel_box = new Table({
64
- id: column.text,
65
- name: column.text,
66
- path: column.text,
67
- parent_id: column.text,
68
- description: column.text,
69
- color: column.text,
70
- priority: column.integer,
71
- is_show: column.integer,
72
- cover_url: column.text,
73
- created_at: column.text,
74
- updated_at: column.text,
75
- extra_data: column.text,
76
- user_no: column.text,
77
- is_del: column.integer,
78
- });
1
+ export { c_card_box, c_card_note, c_cmd, c_note_template, c_zettel_box, } from './electronCanonical.js';