@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,15 +1,39 @@
1
1
  import { spawn } from 'child_process';
2
+ import { existsSync } from 'node:fs';
2
3
  import { DaemonServer } from './server.js';
3
4
  import { DEFAULT_DAEMON_PORT } from './types.js';
4
- import { readPid, isProcessRunning, removePid } from './pid.js';
5
- import { isDaemonRunning } from './client.js';
6
- import { getPackageVersion } from '../utils/version.js';
5
+ import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, removePid, } from './pid.js';
6
+ import { getDaemonInfo, isLegacyDaemonInfo, requestDaemonShutdown, shutdownLegacyDaemon, waitForFreshDaemon, } from './client.js';
7
7
  import { DinoxError } from '../utils/errors.js';
8
8
  import { getRequestedStructuredOutputFormat, printHumanReadableError, printHumanReadableLine, printSuccess, } from '../utils/output.js';
9
9
  import { registerCommandGroup } from '../commands/descriptor/register.js';
10
10
  import { FORMAT_OPTIONS } from '../commands/schema/globalOptions.js';
11
+ import { loadConfig } from '../config/store.js';
12
+ import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
13
+ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
14
+ import { withAsyncRelease } from '../utils/asyncRelease.js';
15
+ import { getPackageVersion } from '../utils/version.js';
11
16
  const DAEMON_STOP_POLL_INTERVAL_MS = 100;
12
17
  const DAEMON_STOP_TIMEOUT_MS = 5_000;
18
+ const DAEMON_CHILD_ENV = 'DINOX_INTERNAL_DAEMON_CHILD';
19
+ async function withDaemonStartLock(run) {
20
+ return withAsyncRelease(() => acquireDaemonStartLock(), run);
21
+ }
22
+ function runningDaemonStatus(info, pidInfo) {
23
+ const pidFileMatches = Boolean(pidInfo
24
+ && pidInfo.pid === info.pid
25
+ && pidInfo.instanceId === info.instanceId);
26
+ return {
27
+ running: true,
28
+ unreachable: false,
29
+ pid: info.pid || pidInfo?.pid || null,
30
+ port: pidFileMatches ? pidInfo?.port ?? null : null,
31
+ socketPath: getDaemonSocketPath(),
32
+ transport: 'socket',
33
+ version: info.version,
34
+ stalePidFile: Boolean(pidInfo && !pidFileMatches),
35
+ };
36
+ }
13
37
  function parsePort(value) {
14
38
  if (value === undefined || value === null || value === '') {
15
39
  return DEFAULT_DAEMON_PORT;
@@ -27,44 +51,84 @@ function parsePort(value) {
27
51
  }
28
52
  return Math.trunc(parsed);
29
53
  }
30
- async function waitForProcessExit(pid, timeoutMs) {
54
+ async function waitForDaemonExit(instanceId, timeoutMs) {
31
55
  const deadline = Date.now() + timeoutMs;
32
56
  while (Date.now() < deadline) {
33
- if (!isProcessRunning(pid)) {
57
+ const info = await getDaemonInfo();
58
+ if (!info || info.instanceId !== instanceId) {
34
59
  return true;
35
60
  }
36
61
  await new Promise((resolve) => setTimeout(resolve, DAEMON_STOP_POLL_INTERVAL_MS));
37
62
  }
38
- return !isProcessRunning(pid);
63
+ const info = await getDaemonInfo();
64
+ return !info || info.instanceId !== instanceId;
39
65
  }
40
- async function startDaemon(port, detached = true, options = {}) {
41
- if (await isDaemonRunning()) {
42
- return { running: true, port, detached, alreadyRunning: true };
66
+ async function expectedDaemonIdentity(baseConfig) {
67
+ const resolved = baseConfig ?? resolveConfig(await loadConfig());
68
+ const { config } = await hydrateResolvedAuthIdentity(resolved, { timeoutMs: resolved.sync.timeoutMs });
69
+ return {
70
+ version: getPackageVersion(),
71
+ userId: config.auth.userId ?? null,
72
+ tokenFingerprint: config.auth.tokenFingerprint ?? null,
73
+ runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
74
+ };
75
+ }
76
+ function matchesRuntimeTokenBinding(info, version, config) {
77
+ return Boolean(config.auth.authorization
78
+ && info.authenticated
79
+ && info.version === version
80
+ && info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
81
+ && info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
82
+ && (!config.auth.userId || info.userId === config.auth.userId));
83
+ }
84
+ export async function startDaemonLocked(port, detached, options = {}) {
85
+ const baseConfig = resolveConfig(await loadConfig());
86
+ const version = getPackageVersion();
87
+ const runningInfo = await getDaemonInfo();
88
+ if (runningInfo) {
89
+ if (matchesRuntimeTokenBinding(runningInfo, version, baseConfig)) {
90
+ return { running: true, port, socketPath: getDaemonSocketPath(), transport: 'socket', detached, alreadyRunning: true };
91
+ }
92
+ throw new DinoxError('A stale daemon is already responding. Run `dino daemon restart` and retry.', {
93
+ code: 'DAEMON_STALE',
94
+ recoverable: true,
95
+ });
43
96
  }
97
+ const expected = await expectedDaemonIdentity(baseConfig);
44
98
  if (detached) {
45
99
  const args = ['daemon', 'start', '--port', String(port), '--no-detach'];
46
- const child = spawn(process.argv[0], [process.argv[1], ...args], {
100
+ const entrypoint = process.argv[1];
101
+ if (!entrypoint) {
102
+ throw new DinoxError('Cannot determine the CLI entrypoint for daemon startup');
103
+ }
104
+ const child = spawn(process.execPath, [...process.execArgv, entrypoint, ...args], {
47
105
  detached: true,
48
106
  stdio: 'ignore',
107
+ env: { ...process.env, [DAEMON_CHILD_ENV]: '1' },
49
108
  });
50
109
  child.unref();
51
- await new Promise((resolve) => setTimeout(resolve, 1000));
52
- if (await isDaemonRunning()) {
53
- return { running: true, port, detached, alreadyRunning: false };
54
- }
55
- else {
56
- return { running: false, port, detached, alreadyRunning: false };
110
+ const ready = await waitForFreshDaemon(expected.version, expected.userId, expected.tokenFingerprint, expected.runtimeConfigFingerprint);
111
+ if (!ready) {
112
+ child.kill('SIGTERM');
113
+ throw new DinoxError('Failed to start daemon');
57
114
  }
115
+ return { running: true, port, socketPath: getDaemonSocketPath(), transport: 'socket', detached, alreadyRunning: false };
58
116
  }
59
117
  else {
60
118
  if (!options.quiet) {
61
- printHumanReadableLine(`Starting daemon on port ${port}...`);
119
+ printHumanReadableLine(`Starting daemon on ${getDaemonSocketPath()}...`);
62
120
  }
63
121
  const server = new DaemonServer();
64
- await server.initialize();
65
- await server.start(port);
122
+ try {
123
+ await server.initialize();
124
+ await server.start(port);
125
+ }
126
+ catch (error) {
127
+ await server.stop().catch(() => undefined);
128
+ throw error;
129
+ }
66
130
  if (!options.quiet) {
67
- printHumanReadableLine(`Daemon running on port ${port} (PID: ${process.pid})`);
131
+ printHumanReadableLine(`Daemon running on ${getDaemonSocketPath()} (PID: ${process.pid})`);
68
132
  }
69
133
  process.on('SIGINT', async () => {
70
134
  if (!options.quiet) {
@@ -79,51 +143,131 @@ async function startDaemon(port, detached = true, options = {}) {
79
143
  process.exit(0);
80
144
  });
81
145
  }
82
- return { running: true, port, detached, alreadyRunning: false };
146
+ return { running: true, port, socketPath: getDaemonSocketPath(), transport: 'socket', detached, alreadyRunning: false };
83
147
  }
84
- async function stopDaemon() {
148
+ async function startDaemon(port, detached = true, options = {}) {
149
+ const internalChild = process.env[DAEMON_CHILD_ENV] === '1';
150
+ if (internalChild) {
151
+ return startDaemonLocked(port, detached, options);
152
+ }
153
+ return withDaemonStartLock(() => startDaemonLocked(port, detached, options));
154
+ }
155
+ /** Caller must hold the daemon start lock. */
156
+ export async function stopDaemonLocked() {
85
157
  const pidInfo = readPid();
86
- if (!pidInfo) {
158
+ const info = await getDaemonInfo();
159
+ if (!info) {
160
+ if (pidInfo && !isProcessRunning(pidInfo.pid)) {
161
+ const stalePidFile = pidInfo.instanceId
162
+ ? removePid(pidInfo.instanceId)
163
+ : removePid(undefined, { expectedPid: pidInfo.pid });
164
+ return { stopped: false, wasRunning: false, stalePidFile };
165
+ }
166
+ const socketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
167
+ if (pidInfo || socketExists) {
168
+ throw new DinoxError('Daemon artifacts exist, but the private socket is not responding; refusing to signal or delete unverified process state.', {
169
+ code: 'DAEMON_UNREACHABLE',
170
+ recoverable: true,
171
+ suggestion: 'Inspect the recorded process and socket ownership, then retry `dino daemon stop`.',
172
+ });
173
+ }
87
174
  return { stopped: false, wasRunning: false, stalePidFile: false };
88
175
  }
89
- if (!isProcessRunning(pidInfo.pid)) {
90
- removePid();
91
- return { stopped: false, wasRunning: false, stalePidFile: true };
176
+ if (isLegacyDaemonInfo(info)) {
177
+ await shutdownLegacyDaemon(info, DAEMON_STOP_TIMEOUT_MS);
178
+ return { stopped: true, wasRunning: true, stalePidFile: false };
92
179
  }
93
180
  try {
94
- process.kill(pidInfo.pid, 'SIGTERM');
181
+ await requestDaemonShutdown(info.instanceId, 2_000);
95
182
  }
96
183
  catch (error) {
97
- throw new DinoxError(`Failed to stop daemon: ${error.message}`);
184
+ throw new DinoxError(`Failed to stop daemon through its private socket: ${error.message}`, {
185
+ code: 'DAEMON_STOP_FAILED',
186
+ suggestion: 'The daemon PID was not signaled because its process identity could not be verified safely.',
187
+ });
98
188
  }
99
- const stopped = await waitForProcessExit(pidInfo.pid, DAEMON_STOP_TIMEOUT_MS);
189
+ const stopped = await waitForDaemonExit(info.instanceId, DAEMON_STOP_TIMEOUT_MS);
100
190
  if (!stopped) {
101
191
  throw new DinoxError(`Daemon did not stop within ${DAEMON_STOP_TIMEOUT_MS}ms. Try stopping it manually and rerun \`dino daemon status\`.`);
102
192
  }
103
- removePid();
193
+ const replacement = await getDaemonInfo();
194
+ if (replacement) {
195
+ throw new DinoxError('A replacement daemon started while the requested daemon was stopping.', {
196
+ code: 'DAEMON_INSTANCE_CHANGED',
197
+ details: { stoppedInstanceId: info.instanceId, replacementInstanceId: replacement.instanceId },
198
+ suggestion: 'Retry after checking `dino daemon status`.',
199
+ recoverable: true,
200
+ });
201
+ }
202
+ removePid(info.instanceId);
104
203
  return { stopped: true, wasRunning: true, stalePidFile: false };
105
204
  }
205
+ export async function stopDaemon() {
206
+ return withDaemonStartLock(() => stopDaemonLocked());
207
+ }
106
208
  async function daemonStatus() {
107
209
  const pidInfo = readPid();
108
- if (!pidInfo) {
109
- return { running: false, pid: null, port: null, version: null, stalePidFile: false };
110
- }
111
- const running = await isDaemonRunning();
112
- if (running) {
113
- return { running: true, pid: pidInfo.pid, port: pidInfo.port, version: getPackageVersion(), stalePidFile: false };
210
+ const info = await getDaemonInfo();
211
+ if (info) {
212
+ // Report the daemon's handshake version, which can differ after a CLI update.
213
+ return runningDaemonStatus(info, pidInfo);
114
214
  }
115
215
  else {
116
- removePid();
117
- return { running: false, pid: null, port: null, version: null, stalePidFile: true };
216
+ if (pidInfo && !isProcessRunning(pidInfo.pid)) {
217
+ const stalePidFile = pidInfo.instanceId
218
+ ? removePid(pidInfo.instanceId)
219
+ : removePid(undefined, { expectedPid: pidInfo.pid });
220
+ if (stalePidFile) {
221
+ return { running: false, unreachable: false, pid: null, port: null, socketPath: null, transport: 'socket', version: null, stalePidFile: true };
222
+ }
223
+ // A replacement rewrote the PID metadata after the initial snapshot.
224
+ // Re-read once and leave the replacement instance untouched.
225
+ const replacementPidInfo = readPid();
226
+ const replacementInfo = await getDaemonInfo();
227
+ if (replacementInfo) {
228
+ return runningDaemonStatus(replacementInfo, replacementPidInfo);
229
+ }
230
+ const replacementSocketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
231
+ return {
232
+ running: false,
233
+ unreachable: Boolean(replacementPidInfo || replacementSocketExists),
234
+ pid: replacementPidInfo?.pid ?? null,
235
+ port: replacementPidInfo?.port ?? null,
236
+ socketPath: replacementSocketExists ? getDaemonSocketPath() : null,
237
+ transport: 'socket',
238
+ version: null,
239
+ stalePidFile: false,
240
+ };
241
+ }
242
+ const socketExists = process.platform !== 'win32' && existsSync(getDaemonSocketPath());
243
+ return {
244
+ running: false,
245
+ unreachable: Boolean(pidInfo || socketExists),
246
+ pid: pidInfo?.pid ?? null,
247
+ port: pidInfo?.port ?? null,
248
+ socketPath: socketExists ? getDaemonSocketPath() : null,
249
+ transport: 'socket',
250
+ version: null,
251
+ stalePidFile: false,
252
+ };
118
253
  }
119
254
  }
255
+ export async function restartDaemonLocked(port = DEFAULT_DAEMON_PORT, options = {}) {
256
+ const stopResult = await stopDaemonLocked();
257
+ await new Promise((resolve) => setTimeout(resolve, 1000));
258
+ const startResult = await startDaemonLocked(port, true, options);
259
+ return { stopped: stopResult.stopped, ...startResult };
260
+ }
261
+ export async function restartDaemon(port = DEFAULT_DAEMON_PORT, options = {}) {
262
+ return withDaemonStartLock(() => restartDaemonLocked(port, options));
263
+ }
120
264
  function printDaemonStartResult(result) {
121
265
  if (result.alreadyRunning) {
122
266
  printHumanReadableLine('Daemon is already running');
123
267
  return;
124
268
  }
125
269
  if (result.running) {
126
- printHumanReadableLine(`Daemon started on port ${result.port}`);
270
+ printHumanReadableLine(`Daemon started on ${result.socketPath}`);
127
271
  return;
128
272
  }
129
273
  printHumanReadableError('Failed to start daemon');
@@ -141,18 +285,26 @@ function printDaemonStopResult(result) {
141
285
  }
142
286
  function printDaemonStatusResult(result) {
143
287
  if (!result.running) {
144
- printHumanReadableLine(result.stalePidFile ? 'Status: Not running (stale PID file)' : 'Status: Not running');
288
+ printHumanReadableLine(result.unreachable
289
+ ? 'Status: Unreachable (process/socket ownership left intact)'
290
+ : result.stalePidFile
291
+ ? 'Status: Not running (stale PID file removed)'
292
+ : 'Status: Not running');
145
293
  return;
146
294
  }
147
295
  printHumanReadableLine('Status: Running');
148
296
  printHumanReadableLine(`PID: ${result.pid}`);
149
- printHumanReadableLine(`Port: ${result.port}`);
297
+ printHumanReadableLine(`Transport: ${result.transport}`);
298
+ printHumanReadableLine(`Socket: ${result.socketPath}`);
150
299
  printHumanReadableLine(`Version: ${result.version}`);
151
300
  }
152
301
  async function runDaemonStart(context) {
153
302
  const port = parsePort(context.options.port);
154
303
  const structured = Boolean(getRequestedStructuredOutputFormat());
155
304
  const result = await startDaemon(port, context.options.detach !== false, { quiet: structured });
305
+ if (!result.running) {
306
+ throw new DinoxError('Failed to start daemon', { code: 'DAEMON_START_FAILED' });
307
+ }
156
308
  if (structured) {
157
309
  printSuccess(result);
158
310
  return;
@@ -177,15 +329,13 @@ async function runDaemonStatus() {
177
329
  }
178
330
  async function runDaemonRestart(context) {
179
331
  const structured = Boolean(getRequestedStructuredOutputFormat());
180
- const stopResult = await stopDaemon();
181
- await new Promise((resolve) => setTimeout(resolve, 1000));
182
- const startResult = await startDaemon(parsePort(context.options.port), true, { quiet: structured });
332
+ const result = await restartDaemon(parsePort(context.options.port), { quiet: structured });
183
333
  if (structured) {
184
- printSuccess({ stopped: stopResult.stopped, ...startResult });
334
+ printSuccess(result);
185
335
  return;
186
336
  }
187
- printDaemonStopResult(stopResult);
188
- printDaemonStartResult(startResult);
337
+ printDaemonStopResult({ stopped: result.stopped, wasRunning: result.stopped, stalePidFile: false });
338
+ printDaemonStartResult(result);
189
339
  }
190
340
  export const daemonCommandGroup = {
191
341
  name: 'daemon',
@@ -195,7 +345,7 @@ export const daemonCommandGroup = {
195
345
  name: 'start',
196
346
  description: 'Start daemon process',
197
347
  options: [
198
- { flags: '--port <number>', description: 'Daemon port', type: 'number', defaultValue: DEFAULT_DAEMON_PORT },
348
+ { flags: '--port <number>', description: 'Legacy compatibility field; daemon listens on a private local socket', type: 'number', defaultValue: DEFAULT_DAEMON_PORT },
199
349
  { flags: '--no-detach', description: 'Run in foreground for debugging', type: 'boolean' },
200
350
  ],
201
351
  schema: {
@@ -206,7 +356,7 @@ export const daemonCommandGroup = {
206
356
  output: {
207
357
  type: 'structured',
208
358
  summary: 'Daemon start acknowledgement',
209
- fields: ['running', 'port', 'detached', 'alreadyRunning'],
359
+ fields: ['running', 'transport', 'socketPath', 'port', 'detached', 'alreadyRunning'],
210
360
  },
211
361
  examples: ['dino daemon start', 'dino daemon start --port 17321', 'dino daemon start --no-detach'],
212
362
  },
@@ -240,7 +390,7 @@ export const daemonCommandGroup = {
240
390
  output: {
241
391
  type: 'structured',
242
392
  summary: 'Daemon process status',
243
- fields: ['running', 'pid', 'port', 'version', 'stalePidFile'],
393
+ fields: ['running', 'unreachable', 'pid', 'transport', 'socketPath', 'port', 'version', 'stalePidFile'],
244
394
  },
245
395
  examples: ['dino daemon status'],
246
396
  },
@@ -249,7 +399,7 @@ export const daemonCommandGroup = {
249
399
  {
250
400
  name: 'restart',
251
401
  description: 'Restart daemon process',
252
- options: [{ flags: '--port <number>', description: 'Daemon port', type: 'number', defaultValue: DEFAULT_DAEMON_PORT }],
402
+ options: [{ flags: '--port <number>', description: 'Legacy compatibility field; daemon listens on a private local socket', type: 'number', defaultValue: DEFAULT_DAEMON_PORT }],
253
403
  schema: {
254
404
  path: 'daemon.restart',
255
405
  risk: 'write',
@@ -258,7 +408,7 @@ export const daemonCommandGroup = {
258
408
  output: {
259
409
  type: 'structured',
260
410
  summary: 'Daemon restart acknowledgement',
261
- fields: ['stopped', 'running', 'port', 'detached', 'alreadyRunning'],
411
+ fields: ['stopped', 'running', 'transport', 'socketPath', 'port', 'detached', 'alreadyRunning'],
262
412
  },
263
413
  examples: ['dino daemon restart', 'dino daemon restart --port 17321'],
264
414
  },
@@ -0,0 +1,44 @@
1
+ import type { ResolvedDinoxConfig } from '../config/types.js';
2
+ import { printHumanReadableWarning } from '../utils/output.js';
3
+ import { getPackageVersion } from '../utils/version.js';
4
+ import { getDaemonInfo, shutdownDaemonForExclusiveAccess } from './client.js';
5
+ import { startDaemonLocked } from './index.js';
6
+ import { acquireDaemonStartLock, readPid, type DaemonPidInfo } from './pid.js';
7
+ import { type DaemonPingResult } from './types.js';
8
+ export type DaemonSnapshot = {
9
+ info: DaemonPingResult | null;
10
+ pidInfo: DaemonPidInfo | null;
11
+ socketExists: boolean;
12
+ };
13
+ export type SuspendedDaemon = {
14
+ snapshot: DaemonSnapshot;
15
+ wasStopped: boolean;
16
+ wasFresh: boolean;
17
+ port: number;
18
+ };
19
+ export type DaemonExclusiveAccessControl = {
20
+ suppressRestore(): void;
21
+ };
22
+ type DaemonLifecycleDependencies = {
23
+ acquireStartLock: typeof acquireDaemonStartLock;
24
+ getDaemonInfo: typeof getDaemonInfo;
25
+ readPid: typeof readPid;
26
+ shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
27
+ startDaemonLocked: typeof startDaemonLocked;
28
+ getPackageVersion: typeof getPackageVersion;
29
+ printWarning: typeof printHumanReadableWarning;
30
+ };
31
+ export declare function captureDaemonSnapshot(overrides?: Partial<DaemonLifecycleDependencies>): Promise<DaemonSnapshot>;
32
+ export declare function daemonSnapshotIsFresh(config: ResolvedDinoxConfig, snapshot: DaemonSnapshot, version?: string): boolean;
33
+ /** Caller must hold the daemon start lock until restore completes. */
34
+ export declare function suspendDaemonForExclusiveAccessLocked(config: ResolvedDinoxConfig, timeoutMs: number, options?: {
35
+ snapshot?: DaemonSnapshot;
36
+ forceStop?: boolean;
37
+ dependencies?: Partial<DaemonLifecycleDependencies>;
38
+ }): Promise<SuspendedDaemon>;
39
+ /** Caller must hold the daemon start lock. */
40
+ export declare function restoreSuspendedDaemonLocked(suspended: SuspendedDaemon, overrides?: Partial<DaemonLifecycleDependencies>): Promise<boolean>;
41
+ export declare function withDaemonExclusiveAccess<T>(config: ResolvedDinoxConfig, timeoutMs: number, run: (control: DaemonExclusiveAccessControl) => Promise<T>, overrides?: Partial<DaemonLifecycleDependencies>, options?: {
42
+ deadlineAtMs?: number;
43
+ }): Promise<T>;
44
+ export {};
@@ -0,0 +1,118 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { getPowerSyncDbPath } from '../config/paths.js';
3
+ import { fingerprintDaemonRuntimeConfig } from '../config/resolve.js';
4
+ import { printHumanReadableWarning } from '../utils/output.js';
5
+ import { getPackageVersion } from '../utils/version.js';
6
+ import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess, } from './client.js';
7
+ import { startDaemonLocked } from './index.js';
8
+ import { acquireDaemonStartLock, getDaemonSocketPath, readPid, } from './pid.js';
9
+ import { DEFAULT_DAEMON_PORT } from './types.js';
10
+ const daemonLifecycleDependencies = {
11
+ acquireStartLock: acquireDaemonStartLock,
12
+ getDaemonInfo,
13
+ readPid,
14
+ shutdownDaemon: shutdownDaemonForExclusiveAccess,
15
+ startDaemonLocked,
16
+ getPackageVersion,
17
+ printWarning: printHumanReadableWarning,
18
+ };
19
+ function dependencies(overrides) {
20
+ return { ...daemonLifecycleDependencies, ...overrides };
21
+ }
22
+ function errorMessage(error) {
23
+ return error instanceof Error ? error.message : String(error);
24
+ }
25
+ function snapshotPort(snapshot) {
26
+ const pidFileMatches = Boolean(snapshot.info
27
+ && snapshot.pidInfo
28
+ && snapshot.pidInfo.pid === snapshot.info.pid
29
+ && snapshot.pidInfo.instanceId === snapshot.info.instanceId);
30
+ return pidFileMatches ? snapshot.pidInfo?.port ?? DEFAULT_DAEMON_PORT : DEFAULT_DAEMON_PORT;
31
+ }
32
+ export async function captureDaemonSnapshot(overrides = {}) {
33
+ const deps = dependencies(overrides);
34
+ return {
35
+ info: await deps.getDaemonInfo(),
36
+ pidInfo: deps.readPid(),
37
+ socketExists: process.platform !== 'win32' && existsSync(getDaemonSocketPath()),
38
+ };
39
+ }
40
+ export function daemonSnapshotIsFresh(config, snapshot, version = getPackageVersion()) {
41
+ return Boolean(snapshot.info
42
+ && isDaemonFresh(snapshot.info, version, config.auth.userId ?? null, config.auth.tokenFingerprint ?? null, fingerprintDaemonRuntimeConfig(config)));
43
+ }
44
+ /** Caller must hold the daemon start lock until restore completes. */
45
+ export async function suspendDaemonForExclusiveAccessLocked(config, timeoutMs, options = {}) {
46
+ const deps = dependencies(options.dependencies ?? {});
47
+ const snapshot = options.snapshot ?? await captureDaemonSnapshot(deps);
48
+ const wasFresh = daemonSnapshotIsFresh(config, snapshot, deps.getPackageVersion());
49
+ const daemonUsesTargetDatabase = !snapshot.info
50
+ || getPowerSyncDbPath(snapshot.info.userId) === getPowerSyncDbPath(config.auth.userId);
51
+ const wasStopped = options.forceStop || daemonUsesTargetDatabase
52
+ ? await deps.shutdownDaemon(Math.min(timeoutMs, 10_000), snapshot.info?.instanceId)
53
+ : false;
54
+ return {
55
+ snapshot,
56
+ wasStopped,
57
+ wasFresh,
58
+ port: snapshotPort(snapshot),
59
+ };
60
+ }
61
+ /** Caller must hold the daemon start lock. */
62
+ export async function restoreSuspendedDaemonLocked(suspended, overrides = {}) {
63
+ if (!suspended.wasStopped || !suspended.wasFresh) {
64
+ return false;
65
+ }
66
+ const deps = dependencies(overrides);
67
+ await deps.startDaemonLocked(suspended.port, true, { quiet: true });
68
+ return true;
69
+ }
70
+ export async function withDaemonExclusiveAccess(config, timeoutMs, run, overrides = {}, options = {}) {
71
+ 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());
76
+ let suspended = null;
77
+ let restoreAllowed = true;
78
+ let outcome;
79
+ const control = {
80
+ suppressRestore() {
81
+ restoreAllowed = false;
82
+ },
83
+ };
84
+ try {
85
+ suspended = await suspendDaemonForExclusiveAccessLocked(config, remainingTimeoutMs(), {
86
+ dependencies: deps,
87
+ });
88
+ try {
89
+ outcome = { ok: true, value: await run(control) };
90
+ }
91
+ catch (error) {
92
+ outcome = { ok: false, error };
93
+ }
94
+ }
95
+ catch (error) {
96
+ outcome = { ok: false, error };
97
+ }
98
+ finally {
99
+ if (suspended && restoreAllowed) {
100
+ try {
101
+ await restoreSuspendedDaemonLocked(suspended, deps);
102
+ }
103
+ catch (error) {
104
+ deps.printWarning(`warning: failed to restore daemon after local command: ${errorMessage(error)}`);
105
+ }
106
+ }
107
+ try {
108
+ await releaseStartLock();
109
+ }
110
+ catch (error) {
111
+ deps.printWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(error)}`);
112
+ }
113
+ }
114
+ if (!outcome.ok) {
115
+ throw outcome.error;
116
+ }
117
+ return outcome.value;
118
+ }
@@ -1,10 +1,23 @@
1
+ export type DaemonPidInfo = {
2
+ pid: number;
3
+ port: number;
4
+ instanceId: string | null;
5
+ startedAt: string | null;
6
+ };
1
7
  export declare function getDaemonDir(): string;
2
8
  export declare function getPidFilePath(): string;
3
9
  export declare function getPortFilePath(): string;
4
- export declare function savePid(pid: number, port: number): void;
5
- export declare function readPid(): {
6
- pid: number;
7
- port: number;
8
- } | null;
9
- export declare function removePid(): void;
10
+ export declare function getSocketFilePath(): string;
11
+ export declare function getDaemonSocketPath(): string;
12
+ export declare function getTokenFilePath(): string;
13
+ export declare function getStartLockFilePath(): string;
14
+ export declare function ensureDaemonDir(): string;
15
+ export declare function savePid(pid: number, port: number, instanceId: string): void;
16
+ export declare function readPid(): DaemonPidInfo | null;
17
+ export declare function removePid(expectedInstanceId?: string, options?: {
18
+ allowMissingOwner?: boolean;
19
+ expectedPid?: number;
20
+ }): boolean;
10
21
  export declare function isProcessRunning(pid: number): boolean;
22
+ export declare function isLegacyDaemonProcess(pid: number, daemonUptimeMs: number): Promise<boolean>;
23
+ export declare function acquireDaemonStartLock(timeoutMs?: number): Promise<() => Promise<void>>;