@dinoxx/dinox-cli 1.0.22 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +55 -14
- package/dist/cli.js +36 -2
- package/dist/commands/auth/index.d.ts +35 -0
- package/dist/commands/auth/index.js +382 -83
- package/dist/commands/boxes/index.js +2 -6
- package/dist/commands/boxes/model.d.ts +3 -0
- package/dist/commands/boxes/model.js +16 -1
- package/dist/commands/boxes/operations.js +18 -8
- package/dist/commands/boxes/repo.js +13 -11
- package/dist/commands/boxes/store.js +1 -1
- package/dist/commands/config/index.js +7 -5
- package/dist/commands/descriptor/register.js +9 -0
- package/dist/commands/doctor.d.ts +123 -0
- package/dist/commands/doctor.js +539 -0
- package/dist/commands/graph/index.d.ts +1 -1
- package/dist/commands/graph/index.js +35 -21
- package/dist/commands/graph/repo.js +92 -17
- package/dist/commands/meta/index.js +4 -4
- package/dist/commands/meta/repo.js +4 -3
- package/dist/commands/notes/actions.d.ts +1 -1
- package/dist/commands/notes/actions.js +121 -78
- package/dist/commands/notes/bulk.d.ts +1 -0
- package/dist/commands/notes/bulk.js +23 -17
- package/dist/commands/notes/contentEngine.js +69 -11
- package/dist/commands/notes/contentPatch.d.ts +4 -1
- package/dist/commands/notes/contentPatch.js +105 -41
- package/dist/commands/notes/contentPatchCommand.js +28 -11
- package/dist/commands/notes/descriptors.js +50 -20
- package/dist/commands/notes/dryRun.d.ts +1 -0
- package/dist/commands/notes/dryRun.js +4 -7
- package/dist/commands/notes/export.d.ts +6 -0
- package/dist/commands/notes/export.js +26 -6
- package/dist/commands/notes/metadataMutation.d.ts +1 -0
- package/dist/commands/notes/metadataMutation.js +14 -3
- package/dist/commands/notes/repo.d.ts +46 -4
- package/dist/commands/notes/repo.js +113 -28
- package/dist/commands/notes/runtime.d.ts +2 -1
- package/dist/commands/notes/runtime.js +17 -1
- package/dist/commands/notes/searchSql.js +13 -6
- package/dist/commands/notes/writeReceipt.d.ts +20 -0
- package/dist/commands/notes/writeReceipt.js +82 -0
- package/dist/commands/prompt/index.js +3 -3
- package/dist/commands/prompt/repo.d.ts +4 -1
- package/dist/commands/prompt/repo.js +17 -7
- package/dist/commands/runtime.d.ts +14 -2
- package/dist/commands/runtime.js +74 -15
- package/dist/commands/schema/renderSkills.js +5 -2
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/storage/index.js +8 -2
- package/dist/commands/storage/repo.d.ts +4 -0
- package/dist/commands/storage/repo.js +356 -92
- package/dist/commands/sync.js +16 -12
- package/dist/commands/tags/content.js +9 -0
- package/dist/commands/tags/index.js +2 -6
- package/dist/commands/tags/operations.js +15 -10
- package/dist/commands/tags/repo.d.ts +2 -0
- package/dist/commands/tags/repo.js +18 -5
- package/dist/commands/tags/store.js +1 -1
- package/dist/commands/todo/contentModel.d.ts +4 -3
- package/dist/commands/todo/contentModel.js +78 -28
- package/dist/commands/todo/dryRun.d.ts +1 -0
- package/dist/commands/todo/dryRun.js +17 -13
- package/dist/commands/todo/index.d.ts +6 -0
- package/dist/commands/todo/index.js +89 -123
- package/dist/commands/todo/repo.d.ts +36 -1
- package/dist/commands/todo/repo.js +146 -37
- package/dist/commands/todo/taskIdentity.d.ts +2 -0
- package/dist/commands/todo/taskIdentity.js +64 -0
- package/dist/commands/todo/taskModel.js +27 -17
- package/dist/commands/update.d.ts +13 -1
- package/dist/commands/update.js +135 -23
- package/dist/commands/writeDryRun.d.ts +3 -2
- package/dist/commands/writeDryRun.js +4 -3
- package/dist/commands/writeDurability.d.ts +5 -0
- package/dist/commands/writeDurability.js +22 -0
- package/dist/config/paths.js +6 -1
- package/dist/config/resolve.d.ts +2 -0
- package/dist/config/resolve.js +23 -9
- package/dist/config/store.d.ts +3 -0
- package/dist/config/store.js +61 -14
- package/dist/config/types.d.ts +4 -0
- package/dist/daemon/client.d.ts +26 -1
- package/dist/daemon/client.js +236 -12
- package/dist/daemon/ensure.js +74 -12
- package/dist/daemon/index.d.ts +42 -0
- package/dist/daemon/index.js +202 -52
- package/dist/daemon/lifecycle.d.ts +42 -0
- package/dist/daemon/lifecycle.js +115 -0
- package/dist/daemon/pid.d.ts +19 -6
- package/dist/daemon/pid.js +147 -13
- package/dist/daemon/route.d.ts +14 -0
- package/dist/daemon/route.js +40 -0
- package/dist/daemon/server.d.ts +11 -0
- package/dist/daemon/server.js +201 -50
- package/dist/daemon/types.d.ts +29 -1
- package/dist/daemon/types.js +1 -0
- package/dist/dinox.js +25 -1
- package/dist/powersync/connector.js +31 -16
- package/dist/powersync/exclusiveRuntime.d.ts +19 -0
- package/dist/powersync/exclusiveRuntime.js +42 -0
- package/dist/powersync/runtime.d.ts +24 -4
- package/dist/powersync/runtime.js +234 -36
- package/dist/powersync/syncGate.d.ts +3 -1
- package/dist/powersync/syncGate.js +7 -1
- package/dist/powersync/tokenIndex.d.ts +11 -0
- package/dist/powersync/tokenIndex.js +159 -22
- package/dist/powersync/uploader.js +107 -52
- package/dist/utils/abort.js +0 -4
- package/dist/utils/asyncRelease.d.ts +2 -0
- package/dist/utils/asyncRelease.js +25 -0
- package/dist/utils/bidlinkCanonicalization.js +3 -0
- package/dist/utils/dryRun.d.ts +1 -1
- package/dist/utils/errors.d.ts +15 -1
- package/dist/utils/errors.js +223 -5
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +51 -15
- package/dist/utils/redact.js +1 -1
- package/dist/utils/sqliteLease.d.ts +8 -0
- package/dist/utils/sqliteLease.js +59 -0
- package/dist/utils/syncTimeout.d.ts +7 -1
- package/dist/utils/syncTimeout.js +8 -7
- package/dist/utils/terminalSanitization.js +6 -2
- package/dist/utils/tiptapMarkdown.js +49 -3
- package/dist/utils/tiptapMarkdownSyntax.d.ts +9 -0
- package/dist/utils/tiptapMarkdownSyntax.js +249 -0
- package/dist/utils/tokenize.js +24 -2
- package/dist/utils/updateNotice.js +60 -12
- package/package.json +12 -10
- package/dist/commands/mcp/auth.d.ts +0 -4
- package/dist/commands/mcp/auth.js +0 -75
- package/dist/commands/mcp/index.d.ts +0 -2
- package/dist/commands/mcp/index.js +0 -86
- package/dist/commands/mcp/noteContentTools.d.ts +0 -222
- package/dist/commands/mcp/noteContentTools.js +0 -390
- package/dist/commands/mcp/server.d.ts +0 -34
- package/dist/commands/mcp/server.js +0 -424
- package/dist/commands/mcp/tools.d.ts +0 -2
- package/dist/commands/mcp/tools.js +0 -32
- package/dist/commands/mcp/types.d.ts +0 -39
- package/dist/commands/mcp/types.js +0 -1
- package/dist/utils/noteContentEngine.d.ts +0 -333
- package/dist/utils/noteContentEngine.js +0 -740
package/dist/daemon/client.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { request as httpRequest } from 'node:http';
|
|
2
|
-
import {
|
|
3
|
-
import { readPid } from './pid.js';
|
|
4
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/dist/daemon/ensure.js
CHANGED
|
@@ -1,24 +1,86 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import {
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
+
}
|
package/dist/daemon/index.d.ts
CHANGED
|
@@ -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 {};
|