@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
@@ -1,11 +1,10 @@
1
1
  import { request as httpRequest } from 'node:http';
2
- import { DEFAULT_DAEMON_PORT } from './types.js';
3
- import { readPid } from './pid.js';
4
- const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 2_000;
2
+ import { existsSync } from 'node:fs';
3
+ import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
+ import { DinoxError } from '../utils/errors.js';
5
+ const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
5
6
  const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
6
- export async function sendDaemonRequest(method, params) {
7
- const pidInfo = readPid();
8
- const port = pidInfo?.port || DEFAULT_DAEMON_PORT;
7
+ export async function sendDaemonRequest(method, params, requestOptions = {}) {
9
8
  const request = {
10
9
  id: Date.now().toString(),
11
10
  method,
@@ -25,15 +24,15 @@ export async function sendDaemonRequest(method, params) {
25
24
  };
26
25
  const data = JSON.stringify(request);
27
26
  const options = {
28
- hostname: '127.0.0.1',
29
- port,
27
+ socketPath: getDaemonSocketPath(),
30
28
  path: '/',
31
29
  method: 'POST',
32
30
  headers: {
31
+ Host: 'dinox-daemon',
33
32
  'Content-Type': 'application/json',
34
33
  'Content-Length': Buffer.byteLength(data),
35
34
  },
36
- timeout: DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
35
+ timeout: requestOptions.timeoutMs ?? DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
37
36
  };
38
37
  const req = httpRequest(options, (res) => {
39
38
  const statusCode = res.statusCode ?? 0;
@@ -59,7 +58,7 @@ export async function sendDaemonRequest(method, params) {
59
58
  finalize(undefined, response.result);
60
59
  }
61
60
  else {
62
- finalize(new Error(response.error || 'Unknown error'));
61
+ finalize(toDaemonError(response.error));
63
62
  }
64
63
  }
65
64
  catch (error) {
@@ -75,12 +74,237 @@ export async function sendDaemonRequest(method, params) {
75
74
  req.end();
76
75
  });
77
76
  }
77
+ function isStructuredDaemonError(value) {
78
+ return Boolean(value)
79
+ && typeof value === 'object'
80
+ && typeof value.message === 'string'
81
+ && typeof value.code === 'string';
82
+ }
83
+ function toDaemonError(error) {
84
+ if (isStructuredDaemonError(error)) {
85
+ if (error.name === 'DinoxError' || error.code !== 'INTERNAL_ERROR') {
86
+ return new DinoxError(error.message, {
87
+ code: error.code,
88
+ details: error.details,
89
+ suggestion: error.suggestion,
90
+ suggestedAction: error.suggested_action,
91
+ recoverable: error.recoverable,
92
+ });
93
+ }
94
+ const plain = new Error(error.message);
95
+ plain.name = error.name || 'Error';
96
+ return plain;
97
+ }
98
+ return new Error(typeof error === 'string' && error.trim() ? error : 'Unknown error');
99
+ }
78
100
  export async function isDaemonRunning() {
101
+ return (await getDaemonInfo()) !== null;
102
+ }
103
+ /**
104
+ * Ping the daemon and return its handshake payload (version + connected identity),
105
+ * or null when no daemon is reachable. Callers use this to detect a stale daemon
106
+ * (wrong version or wrong account) rather than only "running / not running".
107
+ */
108
+ export async function getDaemonInfo() {
79
109
  try {
80
- await sendDaemonRequest('ping');
81
- return true;
110
+ const result = await sendDaemonRequest('ping');
111
+ if (result && typeof result === 'object') {
112
+ const info = result;
113
+ return {
114
+ pid: typeof info.pid === 'number' && Number.isInteger(info.pid) && info.pid > 0 ? info.pid : 0,
115
+ instanceId: typeof info.instanceId === 'string' ? info.instanceId : '',
116
+ uptime: typeof info.uptime === 'number' ? info.uptime : 0,
117
+ version: typeof info.version === 'string' ? info.version : 'unknown',
118
+ transport: 'socket',
119
+ socketPath: typeof info.socketPath === 'string' ? info.socketPath : getDaemonSocketPath(),
120
+ userId: typeof info.userId === 'string' ? info.userId : null,
121
+ tokenFingerprint: typeof info.tokenFingerprint === 'string' ? info.tokenFingerprint : null,
122
+ runtimeConfigFingerprint: typeof info.runtimeConfigFingerprint === 'string'
123
+ ? info.runtimeConfigFingerprint
124
+ : null,
125
+ authenticated: info.authenticated === true,
126
+ };
127
+ }
128
+ return null;
82
129
  }
83
130
  catch {
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * Whether a reachable daemon matches the current CLI version and signed-in account.
136
+ * A mismatch means the daemon was started by an older build or a different user and
137
+ * must be restarted before its cached data can be trusted.
138
+ */
139
+ export function isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint) {
140
+ return info.authenticated
141
+ && info.version === expectedVersion
142
+ && info.userId === expectedUserId
143
+ && info.tokenFingerprint === expectedTokenFingerprint
144
+ && info.runtimeConfigFingerprint === expectedRuntimeConfigFingerprint;
145
+ }
146
+ export function isLegacyDaemonInfo(info) {
147
+ return info.pid === 0 && info.instanceId.length === 0;
148
+ }
149
+ export async function shutdownLegacyDaemon(initialInfo, timeoutMs = 5_000) {
150
+ const pidInfo = readPid();
151
+ if (!isLegacyDaemonInfo(initialInfo)
152
+ || !pidInfo
153
+ || pidInfo.instanceId !== null
154
+ || !isProcessRunning(pidInfo.pid)
155
+ || !await isLegacyDaemonProcess(pidInfo.pid, initialInfo.uptime)) {
156
+ throw new DinoxError('A legacy daemon is responding, but its process identity could not be verified safely.', {
157
+ code: 'DAEMON_LEGACY_UNVERIFIED',
158
+ suggestion: 'Inspect the daemon PID and command line, stop that process manually, then retry.',
159
+ recoverable: true,
160
+ });
161
+ }
162
+ const confirmedInfo = await getDaemonInfo();
163
+ 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)) {
169
+ throw new DinoxError('The legacy daemon changed while its process identity was being verified.', {
170
+ code: 'DAEMON_INSTANCE_CHANGED',
171
+ suggestion: 'Retry after checking `dino daemon status`.',
172
+ recoverable: true,
173
+ });
174
+ }
175
+ try {
176
+ process.kill(pidInfo.pid, 'SIGTERM');
177
+ }
178
+ catch (error) {
179
+ throw new DinoxError('Failed to stop the verified legacy daemon process.', {
180
+ code: 'DAEMON_STOP_FAILED',
181
+ cause: error,
182
+ suggestion: 'Inspect the reported daemon PID and stop it manually before retrying.',
183
+ recoverable: true,
184
+ });
185
+ }
186
+ const deadline = Date.now() + timeoutMs;
187
+ while (Date.now() < deadline) {
188
+ const current = await getDaemonInfo();
189
+ const processRunning = isProcessRunning(pidInfo.pid);
190
+ if (!current && !processRunning) {
191
+ removePid(undefined, { expectedPid: pidInfo.pid });
192
+ return true;
193
+ }
194
+ if (current && !isLegacyDaemonInfo(current)) {
195
+ throw new DinoxError('A replacement daemon started while the legacy daemon was stopping.', {
196
+ code: 'DAEMON_INSTANCE_CHANGED',
197
+ details: { replacementInstanceId: current.instanceId },
198
+ suggestion: 'Retry after checking `dino daemon status`.',
199
+ recoverable: true,
200
+ });
201
+ }
202
+ await new Promise((resolve) => setTimeout(resolve, 100));
203
+ }
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
+ });
209
+ }
210
+ /** Poll until the daemon answers a ping or the timeout elapses. */
211
+ export async function waitForDaemonReady(timeoutMs = 5_000, intervalMs = 250) {
212
+ const deadline = Date.now() + timeoutMs;
213
+ while (Date.now() < deadline) {
214
+ if (await isDaemonRunning()) {
215
+ return true;
216
+ }
217
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
218
+ }
219
+ return isDaemonRunning();
220
+ }
221
+ /** Poll until the daemon answers with the expected authenticated identity. */
222
+ 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();
226
+ if (info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint)) {
227
+ return true;
228
+ }
229
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
230
+ }
231
+ const info = await getDaemonInfo();
232
+ return Boolean(info && isDaemonFresh(info, expectedVersion, expectedUserId, expectedTokenFingerprint, expectedRuntimeConfigFingerprint));
233
+ }
234
+ /** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
235
+ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
236
+ if (!expectedInstanceId) {
237
+ throw new DinoxError('Cannot safely stop a daemon without an instance identity.', {
238
+ code: 'DAEMON_INSTANCE_UNKNOWN',
239
+ recoverable: true,
240
+ });
241
+ }
242
+ const result = await sendDaemonRequest('shutdown', { expectedInstanceId }, { timeoutMs });
243
+ if (!result
244
+ || typeof result !== 'object'
245
+ || result.stopping !== true
246
+ || result.instanceId !== expectedInstanceId) {
247
+ throw new DinoxError('Daemon instance changed before shutdown could be confirmed.', {
248
+ code: 'DAEMON_INSTANCE_CHANGED',
249
+ recoverable: true,
250
+ details: { expectedInstanceId },
251
+ suggestion: 'Retry after checking `dino daemon status`.',
252
+ });
253
+ }
254
+ return result;
255
+ }
256
+ /** Stop a reachable daemon before another process acquires the exclusive PowerSync owner lease. */
257
+ export async function shutdownDaemonForExclusiveAccess(timeoutMs = 5_000, expectedInstanceId) {
258
+ const info = await getDaemonInfo();
259
+ if (!info) {
260
+ const pidInfo = readPid();
261
+ const socketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
262
+ if ((pidInfo && isProcessRunning(pidInfo.pid)) || socketExists) {
263
+ throw new DinoxError('Daemon process artifacts exist, but its private socket is not responding.', {
264
+ code: 'DAEMON_UNREACHABLE',
265
+ suggestion: 'Run `dino daemon status`, inspect the recorded process, and stop it before retrying.',
266
+ recoverable: true,
267
+ });
268
+ }
84
269
  return false;
85
270
  }
271
+ if (isLegacyDaemonInfo(info)) {
272
+ return shutdownLegacyDaemon(info, timeoutMs);
273
+ }
274
+ if (expectedInstanceId && info.instanceId !== expectedInstanceId) {
275
+ throw new DinoxError('Daemon instance changed before exclusive access could be acquired.', {
276
+ code: 'DAEMON_INSTANCE_CHANGED',
277
+ details: { expectedInstanceId, actualInstanceId: info.instanceId },
278
+ suggestion: 'Retry the command after checking `dino daemon status`.',
279
+ recoverable: true,
280
+ });
281
+ }
282
+ try {
283
+ await requestDaemonShutdown(info.instanceId, Math.min(timeoutMs, 2_000));
284
+ }
285
+ catch (error) {
286
+ throw new DinoxError('The running daemon could not release the PowerSync runtime.', {
287
+ code: 'DAEMON_RELEASE_FAILED',
288
+ cause: error,
289
+ suggestion: 'Run `dino daemon stop`, then retry the command.',
290
+ recoverable: true,
291
+ });
292
+ }
293
+ const deadline = Date.now() + timeoutMs;
294
+ while (Date.now() < deadline) {
295
+ const current = await getDaemonInfo();
296
+ if (!current || current.instanceId !== info.instanceId) {
297
+ return true;
298
+ }
299
+ await new Promise((resolve) => setTimeout(resolve, 100));
300
+ }
301
+ const current = await getDaemonInfo();
302
+ if (!current || current.instanceId !== info.instanceId) {
303
+ return true;
304
+ }
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
+ });
86
310
  }
@@ -1,24 +1,86 @@
1
1
  import { spawn } from 'child_process';
2
- import { isDaemonRunning } from './client.js';
2
+ import { getDaemonInfo, isDaemonFresh, waitForFreshDaemon } from './client.js';
3
+ import { stopDaemonLocked } from './index.js';
3
4
  import { DEFAULT_DAEMON_PORT } from './types.js';
4
- import { printHumanReadableLine } from '../utils/output.js';
5
- export async function ensureDaemonRunning() {
6
- if (await isDaemonRunning()) {
7
- return;
8
- }
9
- printHumanReadableLine('Starting daemon...');
5
+ import { acquireDaemonStartLock, } from './pid.js';
6
+ import { loadConfig } from '../config/store.js';
7
+ import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
8
+ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
9
+ import { getPackageVersion } from '../utils/version.js';
10
+ import { printHumanReadableWarning } from '../utils/output.js';
11
+ import { withAsyncRelease } from '../utils/asyncRelease.js';
12
+ const DAEMON_READY_TIMEOUT_MS = 10_000;
13
+ const DAEMON_CHILD_ENV = 'DINOX_INTERNAL_DAEMON_CHILD';
14
+ async function hydrateDaemonIdentity(baseConfig) {
15
+ const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs: baseConfig.sync.timeoutMs });
16
+ return {
17
+ userId: config.auth.userId ?? null,
18
+ tokenFingerprint: config.auth.tokenFingerprint ?? null,
19
+ runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
20
+ };
21
+ }
22
+ function spawnDetachedDaemon() {
10
23
  const args = ['daemon', 'start', '--port', String(DEFAULT_DAEMON_PORT), '--no-detach'];
11
- const child = spawn(process.argv[0], [process.argv[1], ...args], {
24
+ const entrypoint = process.argv[1];
25
+ if (!entrypoint) {
26
+ throw new Error('Cannot determine the CLI entrypoint for daemon startup');
27
+ }
28
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, ...args], {
12
29
  detached: true,
13
30
  stdio: 'ignore',
31
+ env: { ...process.env, [DAEMON_CHILD_ENV]: '1' },
14
32
  });
15
33
  child.unref();
16
- for (let i = 0; i < 10; i++) {
17
- await new Promise((resolve) => setTimeout(resolve, 500));
18
- if (await isDaemonRunning()) {
19
- printHumanReadableLine('Daemon started');
34
+ return child;
35
+ }
36
+ function matchesRuntimeTokenBinding(info, version, config) {
37
+ return Boolean(config.auth.authorization
38
+ && info.authenticated
39
+ && info.version === version
40
+ && info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
41
+ && info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
42
+ && (!config.auth.userId || info.userId === config.auth.userId));
43
+ }
44
+ async function ensureDaemonRunningUnlocked() {
45
+ const expectedVersion = getPackageVersion();
46
+ const baseConfig = resolveConfig(await loadConfig());
47
+ const info = await getDaemonInfo();
48
+ if (info && matchesRuntimeTokenBinding(info, expectedVersion, baseConfig)) {
49
+ return;
50
+ }
51
+ const expectedIdentity = await hydrateDaemonIdentity(baseConfig);
52
+ if (info) {
53
+ // A daemon is reachable — make sure it is the *right* one. It holds a long-lived
54
+ // PowerSync connection bound to whatever account/version it started with, so after
55
+ // `auth login` (new userId) or `dino update` (new version) the running daemon would
56
+ // otherwise keep serving stale or wrong-account data. Restart it when it drifts.
57
+ if (isDaemonFresh(info, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
20
58
  return;
21
59
  }
60
+ printHumanReadableWarning('Restarting daemon (version or account changed)...');
61
+ await stopDaemonLocked().catch((error) => {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ printHumanReadableWarning(`warning: failed to stop stale daemon: ${message}`);
64
+ });
65
+ const afterStop = await getDaemonInfo();
66
+ if (afterStop) {
67
+ if (isDaemonFresh(afterStop, expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint)) {
68
+ return;
69
+ }
70
+ throw new Error('Stale daemon is still responding after restart attempt');
71
+ }
22
72
  }
73
+ else {
74
+ printHumanReadableWarning('Starting daemon...');
75
+ }
76
+ const child = spawnDetachedDaemon();
77
+ if (await waitForFreshDaemon(expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint, DAEMON_READY_TIMEOUT_MS)) {
78
+ printHumanReadableWarning('Daemon started');
79
+ return;
80
+ }
81
+ child.kill('SIGTERM');
23
82
  throw new Error('Failed to start daemon');
24
83
  }
84
+ export async function ensureDaemonRunning() {
85
+ await withAsyncRelease(() => acquireDaemonStartLock(), () => ensureDaemonRunningUnlocked());
86
+ }
@@ -1,4 +1,46 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandGroupDescriptor } from '../commands/descriptor/types.js';
3
+ type DaemonStartResult = {
4
+ running: boolean;
5
+ port: number;
6
+ socketPath: string;
7
+ transport: 'socket';
8
+ detached: boolean;
9
+ alreadyRunning: boolean;
10
+ };
11
+ type DaemonStopResult = {
12
+ stopped: boolean;
13
+ wasRunning: boolean;
14
+ stalePidFile: boolean;
15
+ };
16
+ export declare function startDaemonLocked(port: number, detached: boolean, options?: {
17
+ quiet?: boolean;
18
+ }): Promise<DaemonStartResult>;
19
+ /** Caller must hold the daemon start lock. */
20
+ export declare function stopDaemonLocked(): Promise<DaemonStopResult>;
21
+ export declare function stopDaemon(): Promise<DaemonStopResult>;
22
+ export declare function restartDaemonLocked(port?: number, options?: {
23
+ quiet?: boolean;
24
+ }): Promise<{
25
+ stopped: boolean;
26
+ running: boolean;
27
+ port: number;
28
+ socketPath: string;
29
+ transport: 'socket';
30
+ detached: boolean;
31
+ alreadyRunning: boolean;
32
+ }>;
33
+ export declare function restartDaemon(port?: number, options?: {
34
+ quiet?: boolean;
35
+ }): Promise<{
36
+ stopped: boolean;
37
+ running: boolean;
38
+ port: number;
39
+ socketPath: string;
40
+ transport: 'socket';
41
+ detached: boolean;
42
+ alreadyRunning: boolean;
43
+ }>;
3
44
  export declare const daemonCommandGroup: CommandGroupDescriptor;
4
45
  export declare function registerDaemonCommands(program: Command): void;
46
+ export {};