@dinoxx/dinox-cli 1.1.1 → 2.0.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 (104) 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.d.ts +9 -0
  19. package/dist/commands/notes/repo.js +65 -15
  20. package/dist/commands/notes/searchOutput.d.ts +2 -1
  21. package/dist/commands/notes/searchOutput.js +2 -1
  22. package/dist/commands/notes/searchSql.js +9 -3
  23. package/dist/commands/prompt/index.d.ts +1 -0
  24. package/dist/commands/prompt/index.js +26 -11
  25. package/dist/commands/savedViews/index.d.ts +7 -0
  26. package/dist/commands/savedViews/index.js +458 -0
  27. package/dist/commands/savedViews/model.d.ts +87 -0
  28. package/dist/commands/savedViews/model.js +306 -0
  29. package/dist/commands/savedViews/repo.d.ts +123 -0
  30. package/dist/commands/savedViews/repo.js +512 -0
  31. package/dist/commands/savedViews/semantics.d.ts +46 -0
  32. package/dist/commands/savedViews/semantics.js +738 -0
  33. package/dist/commands/schema/renderSkills.d.ts +2 -0
  34. package/dist/commands/schema/renderSkills.js +69 -10
  35. package/dist/commands/schema/spec.js +4 -0
  36. package/dist/commands/skills/index.d.ts +54 -0
  37. package/dist/commands/skills/index.js +468 -0
  38. package/dist/commands/storage/index.js +2 -2
  39. package/dist/commands/sync.js +2 -1
  40. package/dist/commands/tags/index.js +30 -11
  41. package/dist/commands/tags/operations.d.ts +3 -3
  42. package/dist/commands/tags/operations.js +45 -20
  43. package/dist/commands/templates/model.d.ts +50 -0
  44. package/dist/commands/templates/model.js +239 -0
  45. package/dist/commands/todo/index.js +3 -3
  46. package/dist/commands/update.d.ts +1 -0
  47. package/dist/commands/update.js +46 -9
  48. package/dist/commands/writeConfirmation.d.ts +13 -0
  49. package/dist/commands/writeConfirmation.js +54 -0
  50. package/dist/config/resolve.js +5 -10
  51. package/dist/config/types.d.ts +3 -6
  52. package/dist/constants/links.d.ts +18 -1
  53. package/dist/constants/links.js +26 -1
  54. package/dist/daemon/client.d.ts +2 -2
  55. package/dist/daemon/client.js +106 -52
  56. package/dist/daemon/index.d.ts +5 -3
  57. package/dist/daemon/index.js +28 -5
  58. package/dist/daemon/lifecycle.d.ts +7 -2
  59. package/dist/daemon/lifecycle.js +53 -13
  60. package/dist/daemon/pid.d.ts +1 -1
  61. package/dist/daemon/pid.js +6 -3
  62. package/dist/dinox.js +9 -1
  63. package/dist/powersync/exclusiveRuntime.d.ts +1 -0
  64. package/dist/powersync/exclusiveRuntime.js +26 -8
  65. package/dist/powersync/runtime.d.ts +3 -2
  66. package/dist/powersync/runtime.js +19 -3
  67. package/dist/powersync/schema/content.d.ts +1 -78
  68. package/dist/powersync/schema/content.js +1 -78
  69. package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
  70. package/dist/powersync/schema/electronCanonical.js +1154 -0
  71. package/dist/powersync/schema/index.d.ts +573 -129
  72. package/dist/powersync/schema/index.js +3 -32
  73. package/dist/powersync/schema/local.d.ts +1 -56
  74. package/dist/powersync/schema/local.js +1 -60
  75. package/dist/powersync/schema/note.d.ts +1 -35
  76. package/dist/powersync/schema/note.js +1 -35
  77. package/dist/powersync/schema/notesExtras.d.ts +1 -62
  78. package/dist/powersync/schema/notesExtras.js +1 -71
  79. package/dist/powersync/schema/projects.d.ts +1 -104
  80. package/dist/powersync/schema/projects.js +1 -104
  81. package/dist/powersync/schema/savedViews.d.ts +1 -0
  82. package/dist/powersync/schema/savedViews.js +1 -0
  83. package/dist/powersync/schema/storage.d.ts +1 -24
  84. package/dist/powersync/schema/storage.js +1 -24
  85. package/dist/powersync/schema/tags.d.ts +1 -37
  86. package/dist/powersync/schema/tags.js +1 -37
  87. package/dist/powersync/syncGate.d.ts +20 -2
  88. package/dist/powersync/uploader.d.ts +16 -2
  89. package/dist/powersync/uploader.js +152 -158
  90. package/dist/skills/check.d.ts +1 -0
  91. package/dist/skills/check.js +538 -70
  92. package/dist/skills/retired.d.ts +60 -0
  93. package/dist/skills/retired.js +58 -0
  94. package/dist/utils/deadline.d.ts +8 -0
  95. package/dist/utils/deadline.js +31 -0
  96. package/dist/utils/dryRun.d.ts +20 -1
  97. package/dist/utils/dryRun.js +22 -3
  98. package/dist/utils/errors.d.ts +2 -0
  99. package/dist/utils/errors.js +2 -0
  100. package/dist/utils/processExit.d.ts +1 -0
  101. package/dist/utils/processExit.js +33 -0
  102. package/dist/utils/updateNotice.d.ts +1 -0
  103. package/dist/utils/updateNotice.js +1 -1
  104. package/package.json +8 -5
@@ -1 +1,18 @@
1
- export declare const SKILLS_REPO_URL = "https://github.com/ryzencool/dinox-cli/tree/main/skills";
1
+ export declare const SKILLS_REPO_SLUG = "ryzencool/dinox-cli-skills";
2
+ export declare const SKILLS_REPO_URL = "https://github.com/ryzencool/dinox-cli-skills";
3
+ export declare const SKILLS_INSTALLER_NAME = "skills";
4
+ export declare const SKILLS_INSTALLER_VERSION = "1.5.16";
5
+ export declare const SKILLS_INSTALL_AGENTS: readonly ["codex", "claude-code", "hermes-agent", "openclaw"];
6
+ export type SkillsInstallerMetadata = {
7
+ name: typeof SKILLS_INSTALLER_NAME;
8
+ version: typeof SKILLS_INSTALLER_VERSION;
9
+ };
10
+ export type SkillsReleaseMetadata = {
11
+ repository: string;
12
+ version: string;
13
+ tag: string;
14
+ release: string;
15
+ installer: SkillsInstallerMetadata;
16
+ installCommand: string;
17
+ };
18
+ export declare function getSkillsReleaseMetadata(version: string): SkillsReleaseMetadata;
@@ -1 +1,26 @@
1
- export const SKILLS_REPO_URL = 'https://github.com/ryzencool/dinox-cli/tree/main/skills';
1
+ export const SKILLS_REPO_SLUG = 'ryzencool/dinox-cli-skills';
2
+ export const SKILLS_REPO_URL = `https://github.com/${SKILLS_REPO_SLUG}`;
3
+ export const SKILLS_INSTALLER_NAME = 'skills';
4
+ export const SKILLS_INSTALLER_VERSION = '1.5.16';
5
+ export const SKILLS_INSTALL_AGENTS = Object.freeze([
6
+ 'codex',
7
+ 'claude-code',
8
+ 'hermes-agent',
9
+ 'openclaw',
10
+ ]);
11
+ export function getSkillsReleaseMetadata(version) {
12
+ const normalizedVersion = version.trim();
13
+ const tag = `v${normalizedVersion}`;
14
+ const agentFlags = SKILLS_INSTALL_AGENTS.map((agent) => `-a ${agent}`).join(' ');
15
+ return {
16
+ repository: SKILLS_REPO_URL,
17
+ version: normalizedVersion,
18
+ tag,
19
+ release: `${SKILLS_REPO_URL}/tree/${encodeURIComponent(tag)}`,
20
+ installer: {
21
+ name: SKILLS_INSTALLER_NAME,
22
+ version: SKILLS_INSTALLER_VERSION,
23
+ },
24
+ installCommand: `npx --yes ${SKILLS_INSTALLER_NAME}@${SKILLS_INSTALLER_VERSION} add ${SKILLS_REPO_SLUG}#${tag} -g ${agentFlags} --skill '*' -y`,
25
+ };
26
+ }
@@ -2,13 +2,13 @@ import type { DaemonPingResult, DaemonShutdownResult } from './types.js';
2
2
  export declare function sendDaemonRequest(method: string, params?: unknown, requestOptions?: {
3
3
  timeoutMs?: number;
4
4
  }): Promise<unknown>;
5
- export declare function isDaemonRunning(): Promise<boolean>;
5
+ export declare function isDaemonRunning(timeoutMs?: number): Promise<boolean>;
6
6
  /**
7
7
  * Ping the daemon and return its handshake payload (version + connected identity),
8
8
  * or null when no daemon is reachable. Callers use this to detect a stale daemon
9
9
  * (wrong version or wrong account) rather than only "running / not running".
10
10
  */
11
- export declare function getDaemonInfo(): Promise<DaemonPingResult | null>;
11
+ export declare function getDaemonInfo(timeoutMs?: number): Promise<DaemonPingResult | null>;
12
12
  /**
13
13
  * Whether a reachable daemon matches the current CLI version and signed-in account.
14
14
  * A mismatch means the daemon was started by an older build or a different user and
@@ -1,10 +1,32 @@
1
1
  import { request as httpRequest } from 'node:http';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
+ import { remainingDeadlineMs } from '../utils/deadline.js';
4
5
  import { DinoxError } from '../utils/errors.js';
5
6
  const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
6
7
  const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
8
+ function normalizedTimeoutMs(timeoutMs, fallbackMs) {
9
+ return Number.isFinite(timeoutMs)
10
+ ? Math.max(0, Math.floor(timeoutMs))
11
+ : fallbackMs;
12
+ }
13
+ function createDaemonReleaseTimeoutError(message) {
14
+ return new DinoxError(message, {
15
+ code: 'DAEMON_RELEASE_TIMEOUT',
16
+ suggestion: 'Run `dino daemon stop`, then retry the command.',
17
+ recoverable: true,
18
+ });
19
+ }
20
+ async function waitForNextPoll(deadlineAtMs, intervalMs) {
21
+ const waitMs = remainingDeadlineMs(deadlineAtMs, Math.max(0, intervalMs));
22
+ if (waitMs <= 0) {
23
+ return false;
24
+ }
25
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1, waitMs)));
26
+ return true;
27
+ }
7
28
  export async function sendDaemonRequest(method, params, requestOptions = {}) {
29
+ const timeoutMs = Math.max(1, normalizedTimeoutMs(requestOptions.timeoutMs ?? DEFAULT_DAEMON_REQUEST_TIMEOUT_MS, DEFAULT_DAEMON_REQUEST_TIMEOUT_MS));
8
30
  const request = {
9
31
  id: Date.now().toString(),
10
32
  method,
@@ -12,10 +34,14 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
12
34
  };
13
35
  return new Promise((resolve, reject) => {
14
36
  let settled = false;
37
+ let deadlineTimer;
15
38
  const finalize = (error, value) => {
16
39
  if (settled)
17
40
  return;
18
41
  settled = true;
42
+ if (deadlineTimer) {
43
+ clearTimeout(deadlineTimer);
44
+ }
19
45
  if (error) {
20
46
  reject(error);
21
47
  return;
@@ -32,7 +58,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
32
58
  'Content-Type': 'application/json',
33
59
  'Content-Length': Buffer.byteLength(data),
34
60
  },
35
- timeout: requestOptions.timeoutMs ?? DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
61
+ timeout: timeoutMs,
36
62
  };
37
63
  const req = httpRequest(options, (res) => {
38
64
  const statusCode = res.statusCode ?? 0;
@@ -70,6 +96,10 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
70
96
  req.destroy(new Error('Daemon request timed out'));
71
97
  });
72
98
  req.on('error', (error) => finalize(error));
99
+ deadlineTimer = setTimeout(() => {
100
+ req.destroy(new Error('Daemon request timed out'));
101
+ }, timeoutMs);
102
+ deadlineTimer.unref?.();
73
103
  req.write(data);
74
104
  req.end();
75
105
  });
@@ -97,17 +127,17 @@ function toDaemonError(error) {
97
127
  }
98
128
  return new Error(typeof error === 'string' && error.trim() ? error : 'Unknown error');
99
129
  }
100
- export async function isDaemonRunning() {
101
- return (await getDaemonInfo()) !== null;
130
+ export async function isDaemonRunning(timeoutMs) {
131
+ return (await getDaemonInfo(timeoutMs)) !== null;
102
132
  }
103
133
  /**
104
134
  * Ping the daemon and return its handshake payload (version + connected identity),
105
135
  * or null when no daemon is reachable. Callers use this to detect a stale daemon
106
136
  * (wrong version or wrong account) rather than only "running / not running".
107
137
  */
108
- export async function getDaemonInfo() {
138
+ export async function getDaemonInfo(timeoutMs) {
109
139
  try {
110
- const result = await sendDaemonRequest('ping');
140
+ const result = await sendDaemonRequest('ping', undefined, { timeoutMs });
111
141
  if (result && typeof result === 'object') {
112
142
  const info = result;
113
143
  return {
@@ -147,25 +177,38 @@ export function isLegacyDaemonInfo(info) {
147
177
  return info.pid === 0 && info.instanceId.length === 0;
148
178
  }
149
179
  export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
180
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
181
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
182
+ const remainingTimeoutMs = () => remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
183
+ const requireRemainingTimeoutMs = () => {
184
+ const remainingMs = remainingTimeoutMs();
185
+ if (remainingMs <= 0) {
186
+ throw createDaemonReleaseTimeoutError('Timed out while verifying the legacy daemon process.');
187
+ }
188
+ return remainingMs;
189
+ };
150
190
  const pidInfo = readPid();
151
- if (!isLegacyDaemonInfo(initialInfo)
152
- || !pidInfo
153
- || pidInfo.instanceId !== null
154
- || !isProcessRunning(pidInfo.pid)
155
- || !await isLegacyDaemonProcess(pidInfo.pid, initialInfo.uptime)) {
191
+ const legacyProcessVerified = Boolean(isLegacyDaemonInfo(initialInfo)
192
+ && pidInfo
193
+ && pidInfo.instanceId === null
194
+ && isProcessRunning(pidInfo.pid)
195
+ && await isLegacyDaemonProcess(pidInfo.pid, initialInfo.uptime, requireRemainingTimeoutMs()));
196
+ if (!legacyProcessVerified
197
+ || !pidInfo) {
156
198
  throw new DinoxError('A legacy daemon is responding, but its process identity could not be verified safely.', {
157
199
  code: 'DAEMON_LEGACY_UNVERIFIED',
158
200
  suggestion: 'Inspect the daemon PID and command line, stop that process manually, then retry.',
159
201
  recoverable: true,
160
202
  });
161
203
  }
162
- const confirmedInfo = await getDaemonInfo();
204
+ const confirmedInfo = await getDaemonInfo(requireRemainingTimeoutMs());
163
205
  const confirmedPidInfo = readPid();
164
- if (!confirmedInfo
165
- || !isLegacyDaemonInfo(confirmedInfo)
166
- || confirmedPidInfo?.pid !== pidInfo.pid
167
- || confirmedPidInfo.instanceId !== null
168
- || !await isLegacyDaemonProcess(pidInfo.pid, confirmedInfo.uptime)) {
206
+ const confirmedLegacyProcess = Boolean(confirmedInfo
207
+ && isLegacyDaemonInfo(confirmedInfo)
208
+ && confirmedPidInfo?.pid === pidInfo.pid
209
+ && confirmedPidInfo.instanceId === null
210
+ && await isLegacyDaemonProcess(pidInfo.pid, confirmedInfo.uptime, requireRemainingTimeoutMs()));
211
+ if (!confirmedLegacyProcess) {
169
212
  throw new DinoxError('The legacy daemon changed while its process identity was being verified.', {
170
213
  code: 'DAEMON_INSTANCE_CHANGED',
171
214
  suggestion: 'Retry after checking `dino daemon status`.',
@@ -183,9 +226,8 @@ export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
183
226
  recoverable: true,
184
227
  });
185
228
  }
186
- const deadline = Date.now() + timeoutMs;
187
- while (Date.now() < deadline) {
188
- const current = await getDaemonInfo();
229
+ while (remainingTimeoutMs() > 0) {
230
+ const current = await getDaemonInfo(requireRemainingTimeoutMs());
189
231
  const processRunning = isProcessRunning(pidInfo.pid);
190
232
  if (!current && !processRunning) {
191
233
  removePid(undefined, { expectedPid: pidInfo.pid });
@@ -199,37 +241,46 @@ export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
199
241
  recoverable: true,
200
242
  });
201
243
  }
202
- await new Promise((resolve) => setTimeout(resolve, 100));
244
+ if (!await waitForNextPoll(deadlineAtMs, 100)) {
245
+ break;
246
+ }
203
247
  }
204
- throw new DinoxError('Timed out waiting for the verified legacy daemon to stop.', {
205
- code: 'DAEMON_RELEASE_TIMEOUT',
206
- suggestion: 'Inspect the daemon PID and stop that process manually before retrying.',
207
- recoverable: true,
208
- });
248
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the verified legacy daemon to stop.');
209
249
  }
210
250
  /** Poll until the daemon answers a ping or the timeout elapses. */
211
251
  export async function waitForDaemonReady(timeoutMs = 5_000, intervalMs = 250) {
212
- const deadline = Date.now() + timeoutMs;
213
- while (Date.now() < deadline) {
214
- if (await isDaemonRunning()) {
252
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
253
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
254
+ while (true) {
255
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
256
+ if (remainingMs <= 0) {
257
+ return false;
258
+ }
259
+ if (await isDaemonRunning(remainingMs)) {
215
260
  return true;
216
261
  }
217
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
262
+ if (!await waitForNextPoll(deadlineAtMs, intervalMs)) {
263
+ return false;
264
+ }
218
265
  }
219
- return isDaemonRunning();
220
266
  }
221
267
  /** Poll until the daemon answers with the expected authenticated identity. */
222
268
  export async function waitForFreshDaemon(expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint, timeoutMs = 5_000, intervalMs = 250) {
223
- const deadline = Date.now() + timeoutMs;
224
- while (Date.now() < deadline) {
225
- const info = await getDaemonInfo();
269
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
270
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
271
+ while (true) {
272
+ const remainingMs = remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
273
+ if (remainingMs <= 0) {
274
+ return false;
275
+ }
276
+ const info = await getDaemonInfo(remainingMs);
226
277
  if (info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint)) {
227
278
  return true;
228
279
  }
229
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
280
+ if (!await waitForNextPoll(deadlineAtMs, intervalMs)) {
281
+ return false;
282
+ }
230
283
  }
231
- const info = await getDaemonInfo();
232
- return Boolean(info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint));
233
284
  }
234
285
  /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
235
286
  export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
@@ -255,7 +306,17 @@ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_00
255
306
  }
256
307
  /** Stop a reachable daemon before another process acquires the exclusive PowerSync owner lease. */
257
308
  export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expectedInstanceId) {
258
- const info = await getDaemonInfo();
309
+ const maximumTimeoutMs = normalizedTimeoutMs(timeoutMs, 5_000);
310
+ const deadlineAtMs = Date.now() + maximumTimeoutMs;
311
+ const remainingTimeoutMs = () => remainingDeadlineMs(deadlineAtMs, maximumTimeoutMs);
312
+ const requireRemainingTimeoutMs = () => {
313
+ const remainingMs = remainingTimeoutMs();
314
+ if (remainingMs <= 0) {
315
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the daemon to release the PowerSync runtime.');
316
+ }
317
+ return remainingMs;
318
+ };
319
+ const info = await getDaemonInfo(requireRemainingTimeoutMs());
259
320
  if (!info) {
260
321
  const pidInfo = readPid();
261
322
  const socketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
@@ -269,7 +330,7 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
269
330
  return false;
270
331
  }
271
332
  if (isLegacyDaemonInfo(info)) {
272
- return shutdownLegacyDaemon(info, timeoutMs);
333
+ return shutdownLegacyDaemon(info, requireRemainingTimeoutMs());
273
334
  }
274
335
  if (expectedInstanceId && info.instanceId !== expectedInstanceId) {
275
336
  throw new DinoxError('Daemon instance changed before exclusive access could be acquired.', {
@@ -280,7 +341,7 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
280
341
  });
281
342
  }
282
343
  try {
283
- await requestDaemonShutdown(info.instanceId, Math.min(timeoutMs, 2_000));
344
+ await requestDaemonShutdown(info.instanceId, Math.min(requireRemainingTimeoutMs(), 2_000));
284
345
  }
285
346
  catch (error) {
286
347
  throw new DinoxError('The running daemon could not release the PowerSync runtime.', {
@@ -290,21 +351,14 @@ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expect
290
351
  recoverable: true,
291
352
  });
292
353
  }
293
- const deadline = Date.now() + timeoutMs;
294
- while (Date.now() < deadline) {
295
- const current = await getDaemonInfo();
354
+ while (remainingTimeoutMs() > 0) {
355
+ const current = await getDaemonInfo(requireRemainingTimeoutMs());
296
356
  if (!current || current.instanceId !== info.instanceId) {
297
357
  return true;
298
358
  }
299
- await new Promise((resolve) => setTimeout(resolve, 100));
300
- }
301
- const current = await getDaemonInfo();
302
- if (!current || current.instanceId !== info.instanceId) {
303
- return true;
359
+ if (!await waitForNextPoll(deadlineAtMs, 100)) {
360
+ break;
361
+ }
304
362
  }
305
- throw new DinoxError('Timed out waiting for the daemon to release the PowerSync runtime.', {
306
- code: 'DAEMON_RELEASE_TIMEOUT',
307
- suggestion: 'Run `dino daemon stop`, then retry the command.',
308
- recoverable: true,
309
- });
363
+ throw createDaemonReleaseTimeoutError('Timed out waiting for the daemon to release the PowerSync runtime.');
310
364
  }
@@ -8,14 +8,16 @@ type DaemonStartResult = {
8
8
  detached: boolean;
9
9
  alreadyRunning: boolean;
10
10
  };
11
+ type DaemonStartOptions = {
12
+ quiet?: boolean;
13
+ timeoutMs?: number;
14
+ };
11
15
  type DaemonStopResult = {
12
16
  stopped: boolean;
13
17
  wasRunning: boolean;
14
18
  stalePidFile: boolean;
15
19
  };
16
- export declare function startDaemonLocked(port: number, detached: boolean, options?: {
17
- quiet?: boolean;
18
- }): Promise<DaemonStartResult>;
20
+ export declare function startDaemonLocked(port: number, detached: boolean, options?: DaemonStartOptions): Promise<DaemonStartResult>;
19
21
  /** Caller must hold the daemon start lock. */
20
22
  export declare function stopDaemonLocked(): Promise<DaemonStopResult>;
21
23
  export declare function stopDaemon(): Promise<DaemonStopResult>;
@@ -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;