@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/index.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
|
54
|
+
async function waitForDaemonExit(instanceId, timeoutMs) {
|
|
31
55
|
const deadline = Date.now() + timeoutMs;
|
|
32
56
|
while (Date.now() < deadline) {
|
|
33
|
-
|
|
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
|
-
|
|
63
|
+
const info = await getDaemonInfo();
|
|
64
|
+
return !info || info.instanceId !== instanceId;
|
|
39
65
|
}
|
|
40
|
-
async function
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
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
|
|
119
|
+
printHumanReadableLine(`Starting daemon on ${getDaemonSocketPath()}...`);
|
|
62
120
|
}
|
|
63
121
|
const server = new DaemonServer();
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
90
|
-
|
|
91
|
-
return { stopped:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
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.
|
|
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(`
|
|
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
|
|
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(
|
|
334
|
+
printSuccess(result);
|
|
185
335
|
return;
|
|
186
336
|
}
|
|
187
|
-
printDaemonStopResult(
|
|
188
|
-
printDaemonStartResult(
|
|
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: '
|
|
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: '
|
|
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,42 @@
|
|
|
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>): Promise<T>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
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 = {}) {
|
|
71
|
+
const deps = dependencies(overrides);
|
|
72
|
+
const releaseStartLock = await deps.acquireStartLock(timeoutMs);
|
|
73
|
+
let suspended = null;
|
|
74
|
+
let restoreAllowed = true;
|
|
75
|
+
let outcome;
|
|
76
|
+
const control = {
|
|
77
|
+
suppressRestore() {
|
|
78
|
+
restoreAllowed = false;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
suspended = await suspendDaemonForExclusiveAccessLocked(config, timeoutMs, {
|
|
83
|
+
dependencies: deps,
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
outcome = { ok: true, value: await run(control) };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
outcome = { ok: false, error };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
outcome = { ok: false, error };
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (suspended && restoreAllowed) {
|
|
97
|
+
try {
|
|
98
|
+
await restoreSuspendedDaemonLocked(suspended, deps);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
deps.printWarning(`warning: failed to restore daemon after local command: ${errorMessage(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await releaseStartLock();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
deps.printWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(error)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!outcome.ok) {
|
|
112
|
+
throw outcome.error;
|
|
113
|
+
}
|
|
114
|
+
return outcome.value;
|
|
115
|
+
}
|
package/dist/daemon/pid.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
export declare function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export declare function
|
|
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>>;
|