@dinoxx/dinox-cli 2.13.1 → 2.14.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/dist/cli.js +5 -2
- package/dist/commands/auth/index.d.ts +2 -1
- package/dist/commands/auth/index.js +18 -1
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- package/dist/commands/notes/contentPatch.js +2 -1
- package/dist/commands/notes/export.js +3 -2
- package/dist/commands/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +1 -0
- package/dist/commands/schema/spec.js +9 -1
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +24 -5
- package/dist/daemon/client.js +32 -2
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +34 -0
- package/dist/daemon/forward.js +146 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +44 -0
- package/dist/daemon/server.js +175 -3
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +16 -0
- package/dist/dinox.js +41 -64
- package/dist/gateway/mcpCommandTools.d.ts +9 -0
- package/dist/gateway/mcpCommandTools.js +40 -10
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- package/dist/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/utils/argValue.js +2 -1
- package/dist/utils/errorCodes.d.ts +20 -0
- package/dist/utils/errorCodes.js +5 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +31 -7
- package/dist/utils/requestContext.d.ts +24 -0
- package/dist/utils/requestContext.js +13 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -21,8 +21,9 @@ import { registerTagsCommand } from './commands/tags/index.js';
|
|
|
21
21
|
import { registerTemplateCommands } from './commands/templates/index.js';
|
|
22
22
|
import { registerTodoCommands } from './commands/todo/index.js';
|
|
23
23
|
import { getPackageVersion } from './utils/version.js';
|
|
24
|
+
import { validateRequestId } from './requests/store.js';
|
|
24
25
|
import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
|
|
25
|
-
import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
26
|
+
import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
|
|
26
27
|
export function createCli() {
|
|
27
28
|
clearRequestedStructuredOutputFormat();
|
|
28
29
|
const program = new Command();
|
|
@@ -33,9 +34,10 @@ export function createCli() {
|
|
|
33
34
|
.version(getPackageVersion())
|
|
34
35
|
.showHelpAfterError()
|
|
35
36
|
.configureOutput({
|
|
37
|
+
writeOut: writeToStdout,
|
|
36
38
|
writeErr: (text) => {
|
|
37
39
|
if (!structuredCommanderErrorWritten) {
|
|
38
|
-
|
|
40
|
+
writeToStderr(text);
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
43
|
outputError: (text, write) => {
|
|
@@ -68,6 +70,7 @@ export function createCli() {
|
|
|
68
70
|
.option('--offline', 'skip connect/sync and only use local cache')
|
|
69
71
|
.option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
|
|
70
72
|
.option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
|
|
73
|
+
.option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
|
|
71
74
|
.option('--verbose', 'enable verbose logging');
|
|
72
75
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
73
76
|
const options = actionCommand.optsWithGlobals?.() ?? {};
|
|
@@ -3,13 +3,14 @@ import { fetchUserInfoWithAuthorization } from '../../auth/userInfo.js';
|
|
|
3
3
|
import { loadConfig, updateConfig } from '../../config/store.js';
|
|
4
4
|
import { resolveConfig } from '../../config/resolve.js';
|
|
5
5
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked } from '../../daemon/lifecycle.js';
|
|
6
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
6
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
7
7
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
8
8
|
import type { CommandGroupDescriptor } from '../descriptor/types.js';
|
|
9
9
|
import { printHumanReadableWarning } from '../../utils/output.js';
|
|
10
10
|
export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
|
|
11
11
|
type AuthLoginDependencies = {
|
|
12
12
|
acquireDaemonStartLock: typeof acquireDaemonStartLock;
|
|
13
|
+
acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
13
14
|
loadConfig: typeof loadConfig;
|
|
14
15
|
resolveConfig: typeof resolveConfig;
|
|
15
16
|
fetchCredentials: typeof fetchCredentialsWithAuthorization;
|
|
@@ -10,7 +10,7 @@ import { getDaemonInfo } from '../../daemon/client.js';
|
|
|
10
10
|
import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
|
|
11
11
|
import { stopDaemonLocked } from '../../daemon/index.js';
|
|
12
12
|
import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
|
|
13
|
-
import { acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
13
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
|
|
14
14
|
import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
|
|
15
15
|
import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
|
|
16
16
|
import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
|
|
@@ -22,6 +22,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
|
|
|
22
22
|
import { redactAuthorization } from '../../utils/redact.js';
|
|
23
23
|
import { withAsyncRelease } from '../../utils/asyncRelease.js';
|
|
24
24
|
import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
|
|
25
|
+
import { assertStdinAvailable } from '../../utils/requestContext.js';
|
|
25
26
|
const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
26
27
|
const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
27
28
|
const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
|
|
@@ -80,6 +81,7 @@ export async function releaseOwnerLeases(releases) {
|
|
|
80
81
|
return errors;
|
|
81
82
|
}
|
|
82
83
|
async function readAuthTokenFromStdin() {
|
|
84
|
+
assertStdinAvailable();
|
|
83
85
|
if (process.stdin.isTTY) {
|
|
84
86
|
throw new DinoxError('--token-stdin requires piped stdin', {
|
|
85
87
|
code: 'USAGE_ERROR',
|
|
@@ -100,6 +102,7 @@ async function readAuthTokenFromStdin() {
|
|
|
100
102
|
}
|
|
101
103
|
const authLoginDependencies = {
|
|
102
104
|
acquireDaemonStartLock,
|
|
105
|
+
acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
|
|
103
106
|
loadConfig,
|
|
104
107
|
resolveConfig,
|
|
105
108
|
fetchCredentials: fetchCredentialsWithAuthorization,
|
|
@@ -117,6 +120,15 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
117
120
|
const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
|
|
118
121
|
let previousConfig = null;
|
|
119
122
|
let suspended = null;
|
|
123
|
+
// Held from stopping the daemon until the new login is saved, so a daemon
|
|
124
|
+
// the service manager respawns meanwhile starts with the new identity.
|
|
125
|
+
// Released before any restore, which waits for the daemon to come up.
|
|
126
|
+
let releaseIntentLock = null;
|
|
127
|
+
const releaseIntent = async () => {
|
|
128
|
+
const release = releaseIntentLock;
|
|
129
|
+
releaseIntentLock = null;
|
|
130
|
+
await release?.();
|
|
131
|
+
};
|
|
120
132
|
let lifecycleLockReleaseError;
|
|
121
133
|
let outcome;
|
|
122
134
|
try {
|
|
@@ -138,6 +150,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
138
150
|
});
|
|
139
151
|
const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
|
|
140
152
|
const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
|
|
153
|
+
releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
|
|
141
154
|
suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
|
|
142
155
|
await dependencies.updateConfig((current) => {
|
|
143
156
|
if (!persistedAuthStateMatches(current, previousConfig)) {
|
|
@@ -149,6 +162,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
149
162
|
}
|
|
150
163
|
return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
|
|
151
164
|
}, { timeoutMs });
|
|
165
|
+
await releaseIntent();
|
|
152
166
|
outcome = {
|
|
153
167
|
ok: true,
|
|
154
168
|
value: {
|
|
@@ -164,6 +178,9 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
|
|
|
164
178
|
}
|
|
165
179
|
catch (error) {
|
|
166
180
|
outcome = { ok: false, error };
|
|
181
|
+
await releaseIntent().catch((releaseError) => {
|
|
182
|
+
dependencies.printWarning(`warning: failed to release daemon intent lock: ${authLoginErrorMessage(releaseError)}`);
|
|
183
|
+
});
|
|
167
184
|
if (previousConfig && suspended) {
|
|
168
185
|
try {
|
|
169
186
|
const current = await dependencies.loadConfig();
|
|
@@ -5,7 +5,7 @@ import { resolveConfig } from '../config/resolve.js';
|
|
|
5
5
|
import type { ResolvedDinoxConfig } from '../config/types.js';
|
|
6
6
|
import { shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
|
|
7
7
|
import { restartDaemonLocked } from '../daemon/index.js';
|
|
8
|
-
import { acquireDaemonStartLock } from '../daemon/pid.js';
|
|
8
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../daemon/pid.js';
|
|
9
9
|
import type { DaemonPidInfo } from '../daemon/pid.js';
|
|
10
10
|
import { type DaemonPingResult } from '../daemon/types.js';
|
|
11
11
|
import { connectPowerSync } from '../powersync/runtime.js';
|
|
@@ -89,6 +89,8 @@ type DoctorRuntime = {
|
|
|
89
89
|
daemonBefore: DoctorDaemonSnapshot;
|
|
90
90
|
daemonWasStopped: boolean;
|
|
91
91
|
releaseDaemonStartLock: () => Promise<void>;
|
|
92
|
+
/** Held while doctor owns the database; see acquireDaemonExclusiveIntentLock. */
|
|
93
|
+
releaseIntentLock?: () => Promise<void>;
|
|
92
94
|
daemonStartLockReleased: boolean;
|
|
93
95
|
released: boolean;
|
|
94
96
|
};
|
|
@@ -102,6 +104,7 @@ type DoctorPreparationDependencies = {
|
|
|
102
104
|
resolveConfig: typeof resolveConfig;
|
|
103
105
|
hydrateAuthIdentity: typeof hydrateResolvedAuthIdentity;
|
|
104
106
|
acquireDaemonStartLock: typeof acquireDaemonStartLock;
|
|
107
|
+
acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
105
108
|
captureDaemonSnapshot: typeof captureDaemonSnapshot;
|
|
106
109
|
shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
|
|
107
110
|
connectPowerSync: typeof connectPowerSync;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig } from '../config/store.js';
|
|
|
5
5
|
import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
|
|
6
6
|
import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess } from '../daemon/client.js';
|
|
7
7
|
import { restartDaemonLocked } from '../daemon/index.js';
|
|
8
|
-
import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
|
|
8
|
+
import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, } from '../daemon/pid.js';
|
|
9
9
|
import { DEFAULT_DAEMON_PORT } from '../daemon/types.js';
|
|
10
10
|
import { closePowerSyncAndReleaseOwner, connectPowerSync, getStatusSnapshot, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
|
|
11
11
|
import { inspectNoteTokenIndex, rebuildNoteTokenIndex, } from '../powersync/tokenIndex.js';
|
|
@@ -20,6 +20,7 @@ const doctorPreparationDependencies = {
|
|
|
20
20
|
resolveConfig,
|
|
21
21
|
hydrateAuthIdentity: hydrateResolvedAuthIdentity,
|
|
22
22
|
acquireDaemonStartLock,
|
|
23
|
+
acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
|
|
23
24
|
captureDaemonSnapshot,
|
|
24
25
|
shutdownDaemon: shutdownDaemonForExclusiveAccess,
|
|
25
26
|
connectPowerSync,
|
|
@@ -39,6 +40,14 @@ function doctorTimeoutMs(globals, config) {
|
|
|
39
40
|
export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
40
41
|
const dependencies = { ...doctorPreparationDependencies, ...overrides };
|
|
41
42
|
const releaseDaemonStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(context.globals.syncTimeout) ?? DEFAULT_DOCTOR_TIMEOUT_MS);
|
|
43
|
+
let releaseIntentLock;
|
|
44
|
+
const releaseIntent = async () => {
|
|
45
|
+
const release = releaseIntentLock;
|
|
46
|
+
releaseIntentLock = undefined;
|
|
47
|
+
await release?.().catch((error) => {
|
|
48
|
+
printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
42
51
|
try {
|
|
43
52
|
const rawConfig = await dependencies.loadConfig();
|
|
44
53
|
const baseConfig = dependencies.resolveConfig(rawConfig);
|
|
@@ -56,6 +65,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
56
65
|
authError = errorMessage(error);
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
|
|
59
69
|
const daemonBefore = await dependencies.captureDaemonSnapshot();
|
|
60
70
|
const daemonDbPath = daemonBefore.info ? getPowerSyncDbPath(daemonBefore.info.userId) : null;
|
|
61
71
|
const targetDbPath = getPowerSyncDbPath(config.auth.userId);
|
|
@@ -83,6 +93,7 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
83
93
|
daemonBefore,
|
|
84
94
|
daemonWasStopped,
|
|
85
95
|
releaseDaemonStartLock,
|
|
96
|
+
releaseIntentLock,
|
|
86
97
|
daemonStartLockReleased: false,
|
|
87
98
|
released: false,
|
|
88
99
|
};
|
|
@@ -109,16 +120,19 @@ export async function prepareDoctorRuntime(context, overrides = {}) {
|
|
|
109
120
|
daemonBefore,
|
|
110
121
|
daemonWasStopped,
|
|
111
122
|
releaseDaemonStartLock,
|
|
123
|
+
releaseIntentLock,
|
|
112
124
|
daemonStartLockReleased: false,
|
|
113
125
|
released: false,
|
|
114
126
|
};
|
|
115
127
|
}
|
|
116
128
|
catch (error) {
|
|
129
|
+
await releaseIntent();
|
|
117
130
|
await restoreStoppedDoctorDaemon(config, daemonBefore, daemonWasStopped, dependencies.restartDaemon);
|
|
118
131
|
throw error;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
catch (error) {
|
|
135
|
+
await releaseIntent();
|
|
122
136
|
await releaseDaemonStartLock().catch((releaseError) => {
|
|
123
137
|
printHumanReadableWarning(`warning: failed to release daemon lifecycle lock: ${errorMessage(releaseError)}`);
|
|
124
138
|
});
|
|
@@ -357,6 +371,11 @@ export async function releaseDoctorRuntime(runtime) {
|
|
|
357
371
|
}
|
|
358
372
|
await closePowerSyncAndReleaseOwner(runtime.db, runtime.releaseOwner);
|
|
359
373
|
runtime.released = true;
|
|
374
|
+
const releaseIntentLock = runtime.releaseIntentLock;
|
|
375
|
+
runtime.releaseIntentLock = undefined;
|
|
376
|
+
await releaseIntentLock?.().catch((error) => {
|
|
377
|
+
printHumanReadableWarning(`warning: failed to release daemon intent lock: ${errorMessage(error)}`);
|
|
378
|
+
});
|
|
360
379
|
}
|
|
361
380
|
export async function releaseDoctorDaemonStartLock(runtime) {
|
|
362
381
|
if (runtime.daemonStartLockReleased) {
|
|
@@ -4,6 +4,7 @@ import { registerCommandGroup } from '../descriptor/register.js';
|
|
|
4
4
|
import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from '../schema/globalOptions.js';
|
|
5
5
|
import { DinoxError } from '../../utils/errors.js';
|
|
6
6
|
import { printSuccess } from '../../utils/output.js';
|
|
7
|
+
import { localCommandExecution, requestContext } from '../../utils/requestContext.js';
|
|
7
8
|
import { commandStale, syncBeforeCommand, withCommandRuntime } from '../runtime.js';
|
|
8
9
|
import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from './repo.js';
|
|
9
10
|
const GRAPH_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -26,7 +27,7 @@ function parseDepth(value) {
|
|
|
26
27
|
}
|
|
27
28
|
async function runGraphCommand(context, onlineAction, offlineAction) {
|
|
28
29
|
const globals = context.globals;
|
|
29
|
-
if (Boolean(globals.offline) || Boolean(globals.requireSync)) {
|
|
30
|
+
if (requestContext.getStore()?.daemon || localCommandExecution.getStore() || Boolean(globals.offline) || Boolean(globals.requireSync)) {
|
|
30
31
|
await withCommandRuntime(context, async (runtime) => {
|
|
31
32
|
await syncBeforeCommand(runtime);
|
|
32
33
|
printSuccess({
|
|
@@ -11,6 +11,7 @@ import { nowIsoUtc } from '../../utils/time.js';
|
|
|
11
11
|
import { deriveImageDetailFromContentJson } from '../todo/contentModel.js';
|
|
12
12
|
import { applyJsonPatchToDocument, buildNoteContentBlocks, buildNoteContentOutline, getNoteContentHash, parseStoredNoteDocument, projectNoteContent, } from './contentEngine.js';
|
|
13
13
|
import { parseStoredTags } from './repo.js';
|
|
14
|
+
import { resolveUserPath } from '../../utils/requestContext.js';
|
|
14
15
|
const READ_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
15
16
|
const MAX_STORED_READ_TOKENS = 500;
|
|
16
17
|
const TOKEN_STORE_FILE = 'note-content-read-tokens.json';
|
|
@@ -111,7 +112,7 @@ async function loadTokenStore(nowMs = Date.now()) {
|
|
|
111
112
|
const filePath = tokenStorePath();
|
|
112
113
|
let tokens = [];
|
|
113
114
|
try {
|
|
114
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
115
|
+
const raw = await fs.readFile(resolveUserPath(filePath), 'utf8');
|
|
115
116
|
const parsed = JSON.parse(raw);
|
|
116
117
|
if (Array.isArray(parsed.tokens)) {
|
|
117
118
|
tokens = parsed.tokens.filter((token) => typeof token?.token === 'string' &&
|
|
@@ -6,6 +6,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
|
|
|
6
6
|
import { commandStale, } from '../runtime.js';
|
|
7
7
|
import { getNoteExportItems, } from './repo.js';
|
|
8
8
|
import { parseTargetNoteIds, syncBeforeNoteCommand, withNoteDatabase, } from './runtime.js';
|
|
9
|
+
import { resolveUserPath } from '../../utils/requestContext.js';
|
|
9
10
|
export function parseExportFormat(value) {
|
|
10
11
|
if (value === undefined || value === null || value === '') {
|
|
11
12
|
return 'markdown';
|
|
@@ -107,7 +108,7 @@ async function pathExists(filePath) {
|
|
|
107
108
|
}
|
|
108
109
|
async function resolveOutputTargets(args) {
|
|
109
110
|
const outputLooksLikeDirectory = /[\\/]$/.test(args.outputPath);
|
|
110
|
-
const outputPath =
|
|
111
|
+
const outputPath = resolveUserPath(args.outputPath);
|
|
111
112
|
if (args.notes.length === 1) {
|
|
112
113
|
if (outputLooksLikeDirectory) {
|
|
113
114
|
return [{
|
|
@@ -160,7 +161,7 @@ export async function writeExportFiles(args) {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
if (args.notes.length > 1) {
|
|
163
|
-
await fs.mkdir(
|
|
164
|
+
await fs.mkdir(resolveUserPath(args.outputPath), { recursive: true, mode: 0o700 });
|
|
164
165
|
}
|
|
165
166
|
else {
|
|
166
167
|
await fs.mkdir(path.dirname(targets[0].path), { recursive: true, mode: 0o700 });
|
|
@@ -43,4 +43,12 @@ export declare function waitForMutationUpload(runtime: CommandRuntime, options?:
|
|
|
43
43
|
durability?: unknown;
|
|
44
44
|
}): Promise<MutationUploadReceipt>;
|
|
45
45
|
export declare function printStaleCacheWarning(runtime: CommandRuntime, message?: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Freshness on the daemon's long-lived stream. The server pushes every change,
|
|
48
|
+
* so a connected, error-free stream is current without a new checkpoint; an
|
|
49
|
+
* idle stream may not produce one before the timeout. After a reconnect (wake,
|
|
50
|
+
* network change) it may have been a dead connection, so then a checkpoint
|
|
51
|
+
* completed after the reconnect is required.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForDaemonCurrent(db: AbstractPowerSyncDatabase, _startedAt: string, timeoutMs: number): Promise<boolean>;
|
|
46
54
|
export {};
|
package/dist/commands/runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { syncNoteTokenIndex } from '../powersync/tokenIndex.js';
|
|
|
8
8
|
import { createTimeoutSignal } from '../utils/abort.js';
|
|
9
9
|
import { DinoxError } from '../utils/errors.js';
|
|
10
10
|
import { printHumanReadableWarning } from '../utils/output.js';
|
|
11
|
+
import { requestContext } from '../utils/requestContext.js';
|
|
11
12
|
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
12
13
|
import { parseWriteDurability } from './writeDurability.js';
|
|
13
14
|
const PRE_SYNC_TIMEOUT_MS = 10_000;
|
|
@@ -44,6 +45,22 @@ export function resolveWriteDurabilityForRuntime(runtime, value) {
|
|
|
44
45
|
export async function withCommandRuntime(source, run) {
|
|
45
46
|
const startedAtIso = new Date().toISOString();
|
|
46
47
|
const globals = resolveGlobals(source);
|
|
48
|
+
const daemon = requestContext.getStore()?.daemon;
|
|
49
|
+
if (daemon) {
|
|
50
|
+
const offline = Boolean(globals.offline);
|
|
51
|
+
const status = getStatusSnapshot(daemon.db);
|
|
52
|
+
const stale = offline || !status.connected || !status.hasSynced || Boolean(daemon.syncUnconfirmedSince?.());
|
|
53
|
+
return run({
|
|
54
|
+
db: daemon.db,
|
|
55
|
+
config: daemon.config,
|
|
56
|
+
rawConfig: {},
|
|
57
|
+
offline,
|
|
58
|
+
timeoutMs: parseSyncTimeoutMs(globals.syncTimeout) ?? daemon.config.sync.timeoutMs,
|
|
59
|
+
stale,
|
|
60
|
+
requireSync: Boolean(globals.requireSync),
|
|
61
|
+
sync: { firstSyncStale: stale, preSyncIdle: null, recentSync: null, gate: null, startedAtIso },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
47
64
|
const rawConfig = await loadConfig();
|
|
48
65
|
const baseConfig = resolveConfig(rawConfig);
|
|
49
66
|
const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
@@ -99,7 +116,7 @@ export async function syncBeforeCommand(runtime, options = {}) {
|
|
|
99
116
|
const deadlineAtMs = Date.now() + timeoutMs;
|
|
100
117
|
const remainingTimeoutMs = () => Math.max(0, deadlineAtMs - Date.now());
|
|
101
118
|
const recentSync = runtime.requireSync
|
|
102
|
-
? await waitForLastSyncedAfter(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
|
|
119
|
+
? await (requestContext.getStore()?.daemon ? waitForDaemonCurrent : waitForLastSyncedAfter)(runtime.db, runtime.sync.startedAtIso, Math.max(1, remainingTimeoutMs()))
|
|
103
120
|
: true;
|
|
104
121
|
runtime.sync.recentSync = recentSync;
|
|
105
122
|
const downloadIdle = await waitForDownloadIdle(runtime.db, Math.max(1, remainingTimeoutMs()));
|
|
@@ -178,3 +195,34 @@ export function printStaleCacheWarning(runtime, message = 'warning: sync timed o
|
|
|
178
195
|
printHumanReadableWarning(message);
|
|
179
196
|
}
|
|
180
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Freshness on the daemon's long-lived stream. The server pushes every change,
|
|
200
|
+
* so a connected, error-free stream is current without a new checkpoint; an
|
|
201
|
+
* idle stream may not produce one before the timeout. After a reconnect (wake,
|
|
202
|
+
* network change) it may have been a dead connection, so then a checkpoint
|
|
203
|
+
* completed after the reconnect is required.
|
|
204
|
+
*/
|
|
205
|
+
export async function waitForDaemonCurrent(db, _startedAt, timeoutMs) {
|
|
206
|
+
const unconfirmedSince = requestContext.getStore()?.daemon?.syncUnconfirmedSince?.();
|
|
207
|
+
if (unconfirmedSince) {
|
|
208
|
+
return await waitForLastSyncedAfter(db, unconfirmedSince, timeoutMs)
|
|
209
|
+
&& !getStatusSnapshot(db).dataFlowStatus.downloadError;
|
|
210
|
+
}
|
|
211
|
+
const current = () => {
|
|
212
|
+
const status = getStatusSnapshot(db);
|
|
213
|
+
return status.connected && status.hasSynced && !status.dataFlowStatus.downloadError;
|
|
214
|
+
};
|
|
215
|
+
if (current())
|
|
216
|
+
return true;
|
|
217
|
+
const timeout = createTimeoutSignal(timeoutMs);
|
|
218
|
+
try {
|
|
219
|
+
await db.waitForStatus(() => current(), timeout.signal);
|
|
220
|
+
return !timeout.signal.aborted && current();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
timeout.cancel();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -2,3 +2,4 @@ export const FORMAT_OPTIONS = ['--format <yaml|json>', '--json'];
|
|
|
2
2
|
export const OFFLINE_OPTIONS = ['--offline'];
|
|
3
3
|
export const REQUIRE_SYNC_OPTIONS = ['--require-sync'];
|
|
4
4
|
export const SYNC_TIMEOUT_OPTIONS = ['--sync-timeout <ms>'];
|
|
5
|
+
export const REQUEST_ID_OPTIONS = ['--request-id <id>'];
|
|
@@ -33,6 +33,7 @@ const GLOBAL_OPTIONS = [
|
|
|
33
33
|
['`--offline`', 'Skip sync, use local cache only'],
|
|
34
34
|
['`--require-sync`', 'Fail if the local PowerSync cache cannot be confirmed fresh before reading'],
|
|
35
35
|
['`--sync-timeout <ms>`', 'Override default 300000 ms (5 min) sync/connect timeout'],
|
|
36
|
+
['`--request-id <id>`', 'Idempotency key for data commands: rerunning with the same id within 24h replays the stored result'],
|
|
36
37
|
['`--verbose`', 'Enable verbose logging'],
|
|
37
38
|
];
|
|
38
39
|
function buildCommandSignature(schema) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { authCommandGroup } from '../auth/index.js';
|
|
2
2
|
import { boxCommandGroup } from '../boxes/index.js';
|
|
3
|
+
import { commandExecutionTarget } from '../../daemon/forward.js';
|
|
3
4
|
import { commandSchemasFromGroupDescriptor } from '../descriptor/register.js';
|
|
4
5
|
import { standaloneCommandSchemaFromDescriptor } from '../descriptor/register.js';
|
|
5
6
|
import { configCommandGroup } from '../config/index.js';
|
|
@@ -14,6 +15,7 @@ import { promptCommandGroup } from '../prompt/index.js';
|
|
|
14
15
|
import { propertyCommandGroup } from '../properties/index.js';
|
|
15
16
|
import { savedViewCommandGroup } from '../savedViews/index.js';
|
|
16
17
|
import { schemaCommandMetadata } from './commandMetadata.js';
|
|
18
|
+
import { REQUEST_ID_OPTIONS } from './globalOptions.js';
|
|
17
19
|
import { skillsCommandGroup } from '../skills/index.js';
|
|
18
20
|
import { syncCommandDescriptor } from '../sync.js';
|
|
19
21
|
import { storageCommandGroup } from '../storage/index.js';
|
|
@@ -21,6 +23,12 @@ import { tagCommandGroup } from '../tags/index.js';
|
|
|
21
23
|
import { templateCommandGroup } from '../templates/index.js';
|
|
22
24
|
import { todoCommandGroup } from '../todo/index.js';
|
|
23
25
|
import { updateCommandDescriptor } from '../update.js';
|
|
26
|
+
/** Every command that runs inside the daemon accepts an idempotency key. */
|
|
27
|
+
function withRequestId(schema) {
|
|
28
|
+
if (commandExecutionTarget(schema.path.split('.')) !== 'daemon')
|
|
29
|
+
return schema;
|
|
30
|
+
return { ...schema, globalOptions: [...schema.globalOptions, ...REQUEST_ID_OPTIONS] };
|
|
31
|
+
}
|
|
24
32
|
export const COMMAND_SCHEMAS = [
|
|
25
33
|
...commandSchemasFromGroupDescriptor(authCommandGroup),
|
|
26
34
|
...commandSchemasFromGroupDescriptor(configCommandGroup),
|
|
@@ -43,7 +51,7 @@ export const COMMAND_SCHEMAS = [
|
|
|
43
51
|
...commandSchemasFromGroupDescriptor(noteCommandGroup),
|
|
44
52
|
...commandSchemasFromGroupDescriptor(graphCommandGroup),
|
|
45
53
|
...commandSchemasFromGroupDescriptor(todoCommandGroup),
|
|
46
|
-
];
|
|
54
|
+
].map(withRequestId);
|
|
47
55
|
const COMMAND_SCHEMA_MAP = new Map(COMMAND_SCHEMAS.map((schema) => [schema.path, schema]));
|
|
48
56
|
export function listCommandSchemas() {
|
|
49
57
|
return [...COMMAND_SCHEMAS];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Storage commands: inspect/test/upload orchestration with cleanup on partial failure.
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import { DinoxError } from '../../../utils/errors.js';
|
|
4
|
+
import { resolveUserPath } from '../../../utils/requestContext.js';
|
|
4
5
|
import { buildStorageTestPlan, buildStorageUploadPlan, getActiveCustomStorageConfigId, listStoredCustomStorageConfigs, resolveStorageSelection, } from './configs.js';
|
|
5
6
|
import { createStorageCollisionError, createStorageUploadOutcomeUnknownError, defaultDeleteObject, defaultUploadBytes, defaultUploadObject, generateThumbnailBuffer, isObjectCollisionError, loadSharp, } from './remote.js';
|
|
6
7
|
import { normalizeText, } from './shared.js';
|
|
@@ -8,7 +9,8 @@ import { persistUploadedResource } from './resources.js';
|
|
|
8
9
|
import { buildStorageTarget, generateThumbnailKey } from './targets.js';
|
|
9
10
|
export async function inspectStorageUpload(db, input, deps) {
|
|
10
11
|
const statFile = deps?.statFile ?? fsp.stat;
|
|
11
|
-
const
|
|
12
|
+
const suppliedPath = input.filePath.trim();
|
|
13
|
+
const filePath = suppliedPath ? resolveUserPath(suppliedPath) : suppliedPath;
|
|
12
14
|
if (!filePath) {
|
|
13
15
|
throw new DinoxError('File path is required.');
|
|
14
16
|
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
|
5
5
|
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
6
6
|
import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../utils/output.js';
|
|
8
|
-
import { getStatusSnapshot, waitForLastSyncedAfter } from '../powersync/runtime.js';
|
|
8
|
+
import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty } from '../powersync/runtime.js';
|
|
9
9
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
10
10
|
type SyncCommandDependencies = {
|
|
11
11
|
loadConfig: typeof loadConfig;
|
|
@@ -15,6 +15,7 @@ type SyncCommandDependencies = {
|
|
|
15
15
|
getRequestedStructuredOutputFormat: typeof getRequestedStructuredOutputFormat;
|
|
16
16
|
getStatusSnapshot: typeof getStatusSnapshot;
|
|
17
17
|
waitForLastSyncedAfter: typeof waitForLastSyncedAfter;
|
|
18
|
+
waitForUploadQueueEmpty: typeof waitForUploadQueueEmpty;
|
|
18
19
|
runSingleSyncPass: typeof runSingleSyncPass;
|
|
19
20
|
printHumanReadableLine: typeof printHumanReadableLine;
|
|
20
21
|
printSuccess: typeof printSuccess;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { getPowerSyncDbPath } from '../config/paths.js';
|
|
1
2
|
import { loadConfig } from '../config/store.js';
|
|
2
3
|
import { resolveConfig } from '../config/resolve.js';
|
|
3
4
|
import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
4
5
|
import { withExclusivePowerSync } from '../powersync/exclusiveRuntime.js';
|
|
6
|
+
import { requestContext } from '../utils/requestContext.js';
|
|
5
7
|
import { registerStandaloneCommand } from './descriptor/register.js';
|
|
8
|
+
import { waitForDaemonCurrent } from './runtime.js';
|
|
6
9
|
import { FORMAT_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
|
|
7
10
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
|
|
8
11
|
import { parseSyncTimeoutMs } from '../utils/syncTimeout.js';
|
|
9
|
-
import { getStatusSnapshot, waitForLastSyncedAfter, } from '../powersync/runtime.js';
|
|
12
|
+
import { getStatusSnapshot, waitForLastSyncedAfter, waitForUploadQueueEmpty, } from '../powersync/runtime.js';
|
|
10
13
|
import { runSingleSyncPass } from '../powersync/syncPass.js';
|
|
11
14
|
import { createSyncRequiredError, evaluateSyncGate } from '../powersync/syncGate.js';
|
|
12
15
|
const MAX_SYNC_CLEANUP_RESERVE_MS = 5_000;
|
|
@@ -18,6 +21,7 @@ const syncCommandDependencies = {
|
|
|
18
21
|
getRequestedStructuredOutputFormat,
|
|
19
22
|
getStatusSnapshot,
|
|
20
23
|
waitForLastSyncedAfter,
|
|
24
|
+
waitForUploadQueueEmpty,
|
|
21
25
|
runSingleSyncPass,
|
|
22
26
|
printHumanReadableLine,
|
|
23
27
|
printSuccess,
|
|
@@ -27,18 +31,26 @@ export async function executeSyncCommand(context, overrides = {}) {
|
|
|
27
31
|
const dependencies = { ...syncCommandDependencies, ...overrides };
|
|
28
32
|
const startedAtMs = dependencies.now();
|
|
29
33
|
const startedAtIso = new Date(startedAtMs).toISOString();
|
|
30
|
-
const
|
|
34
|
+
const daemon = requestContext.getStore()?.daemon;
|
|
35
|
+
const baseConfig = daemon?.config ?? dependencies.resolveConfig(await dependencies.loadConfig());
|
|
31
36
|
const structuredOutput = dependencies.getRequestedStructuredOutputFormat() !== null;
|
|
32
37
|
const timeoutMs = parseSyncTimeoutMs(context.globals.syncTimeout) ?? baseConfig.sync.timeoutMs;
|
|
33
38
|
const deadlineAtMs = startedAtMs + timeoutMs;
|
|
34
39
|
const cleanupReserveMs = Math.min(MAX_SYNC_CLEANUP_RESERVE_MS, Math.max(1, Math.floor(timeoutMs * 0.2)));
|
|
35
40
|
const operationDeadlineAtMs = deadlineAtMs - cleanupReserveMs;
|
|
36
41
|
const remainingTimeoutMs = (reserveMs = 0) => Math.max(1, deadlineAtMs - dependencies.now() - reserveMs);
|
|
37
|
-
const { config } = await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
42
|
+
const { config } = daemon ? { config: daemon.config } : await dependencies.hydrateResolvedAuthIdentity(baseConfig, {
|
|
38
43
|
timeoutMs: remainingTimeoutMs(),
|
|
39
44
|
});
|
|
40
45
|
const strict = Boolean(context.options.strict);
|
|
41
|
-
const
|
|
46
|
+
const withRuntime = daemon
|
|
47
|
+
? async (_args, run) => {
|
|
48
|
+
const status = dependencies.getStatusSnapshot(daemon.db);
|
|
49
|
+
return run({ db: daemon.db, dbPath: getPowerSyncDbPath(config.auth.userId),
|
|
50
|
+
stale: !status.connected || !status.hasSynced, releaseOwner: async () => { } });
|
|
51
|
+
}
|
|
52
|
+
: dependencies.withExclusivePowerSync;
|
|
53
|
+
const result = await withRuntime({
|
|
42
54
|
config,
|
|
43
55
|
offline: false,
|
|
44
56
|
timeoutMs: remainingTimeoutMs(),
|
|
@@ -49,9 +61,16 @@ export async function executeSyncCommand(context, overrides = {}) {
|
|
|
49
61
|
cleanupFailureMode: 'throw',
|
|
50
62
|
}, async ({ db, dbPath, stale }) => {
|
|
51
63
|
const recentSync = strict
|
|
52
|
-
? await dependencies.waitForLastSyncedAfter(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
64
|
+
? await (daemon ? waitForDaemonCurrent : dependencies.waitForLastSyncedAfter)(db, startedAtIso, remainingTimeoutMs(cleanupReserveMs))
|
|
53
65
|
: true;
|
|
54
66
|
const syncPass = await dependencies.runSingleSyncPass(db, remainingTimeoutMs(cleanupReserveMs));
|
|
67
|
+
const uploadQueue = daemon
|
|
68
|
+
? await dependencies.waitForUploadQueueEmpty(db, remainingTimeoutMs(cleanupReserveMs))
|
|
69
|
+
: null;
|
|
70
|
+
if (uploadQueue) {
|
|
71
|
+
syncPass.uploadIdle = uploadQueue.ok;
|
|
72
|
+
syncPass.idle = syncPass.downloadIdle && uploadQueue.ok;
|
|
73
|
+
}
|
|
55
74
|
const status = dependencies.getStatusSnapshot(db);
|
|
56
75
|
const gate = evaluateSyncGate({
|
|
57
76
|
offline: false,
|
package/dist/daemon/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { request as httpRequest } from 'node:http';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
|
|
4
|
+
import { getPackageVersion } from '../utils/version.js';
|
|
4
5
|
import { remainingDeadlineMs } from '../utils/deadline.js';
|
|
5
6
|
import { DinoxError, wireErrorCode } from '../utils/errors.js';
|
|
6
7
|
const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
|
|
@@ -34,6 +35,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
|
|
|
34
35
|
};
|
|
35
36
|
return new Promise((resolve, reject) => {
|
|
36
37
|
let settled = false;
|
|
38
|
+
let connected = false;
|
|
37
39
|
let deadlineTimer;
|
|
38
40
|
const finalize = (error, value) => {
|
|
39
41
|
if (settled)
|
|
@@ -43,6 +45,8 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
|
|
|
43
45
|
clearTimeout(deadlineTimer);
|
|
44
46
|
}
|
|
45
47
|
if (error) {
|
|
48
|
+
if (error instanceof Error)
|
|
49
|
+
Object.assign(error, { requestWritten: connected });
|
|
46
50
|
reject(error);
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
@@ -66,7 +70,7 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
|
|
|
66
70
|
let totalBytes = 0;
|
|
67
71
|
res.on('data', (chunk) => {
|
|
68
72
|
totalBytes += chunk.length;
|
|
69
|
-
if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
|
|
73
|
+
if (method !== 'command.execute' && totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
|
|
70
74
|
req.destroy(new Error('Daemon response too large'));
|
|
71
75
|
return;
|
|
72
76
|
}
|
|
@@ -92,10 +96,19 @@ export async function sendDaemonRequest(method, params, requestOptions = {}) {
|
|
|
92
96
|
}
|
|
93
97
|
});
|
|
94
98
|
});
|
|
99
|
+
req.on('socket', socket => {
|
|
100
|
+
if (!socket.connecting)
|
|
101
|
+
connected = true;
|
|
102
|
+
socket.once('connect', () => { connected = true; });
|
|
103
|
+
});
|
|
95
104
|
req.on('timeout', () => {
|
|
96
105
|
req.destroy(new Error('Daemon request timed out'));
|
|
97
106
|
});
|
|
98
107
|
req.on('error', (error) => finalize(error));
|
|
108
|
+
req.on('response', res => {
|
|
109
|
+
res.on('aborted', () => finalize(new Error('Daemon response interrupted')));
|
|
110
|
+
res.on('error', error => finalize(error));
|
|
111
|
+
});
|
|
99
112
|
deadlineTimer = setTimeout(() => {
|
|
100
113
|
req.destroy(new Error('Daemon request timed out'));
|
|
101
114
|
}, timeoutMs);
|
|
@@ -282,6 +295,16 @@ export async function waitForFreshDaemon(expectedVersion, expectedUserId, expect
|
|
|
282
295
|
}
|
|
283
296
|
}
|
|
284
297
|
}
|
|
298
|
+
/** Daemon log context for a stop request; never carries credentials. */
|
|
299
|
+
function redactRequesterCommand(argv) {
|
|
300
|
+
return argv.join(' ')
|
|
301
|
+
.replace(/(auth\s+login)\b[\s\S]*/i, '$1 [REDACTED]')
|
|
302
|
+
.replace(/((?:--)?[\w-]*(?:token|authorization|secret|password|api[-_]?key)[\w-]*[=\s]+)(?:Bearer\s+)?[^\s]+/gi, '$1[REDACTED]')
|
|
303
|
+
.replace(/\bBearer\s+\S+/gi, 'Bearer [REDACTED]')
|
|
304
|
+
.replace(/\beyJ[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+){1,2}\b/g, '[REDACTED]')
|
|
305
|
+
.replace(/\b[A-Za-z0-9_-]{32,}\b/g, '[REDACTED]')
|
|
306
|
+
.slice(0, 200);
|
|
307
|
+
}
|
|
285
308
|
/** Request shutdown of one exact daemon instance and reject if the socket changed owners. */
|
|
286
309
|
export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_000) {
|
|
287
310
|
if (!expectedInstanceId) {
|
|
@@ -290,7 +313,14 @@ export async function requestDaemonShutdown(expectedInstanceId, timeoutMs = 2_00
|
|
|
290
313
|
recoverable: true,
|
|
291
314
|
});
|
|
292
315
|
}
|
|
293
|
-
const result = await sendDaemonRequest('shutdown', {
|
|
316
|
+
const result = await sendDaemonRequest('shutdown', {
|
|
317
|
+
expectedInstanceId,
|
|
318
|
+
requester: {
|
|
319
|
+
pid: process.pid,
|
|
320
|
+
version: getPackageVersion(),
|
|
321
|
+
command: redactRequesterCommand(process.argv.slice(2)),
|
|
322
|
+
},
|
|
323
|
+
}, { timeoutMs });
|
|
294
324
|
if (!result
|
|
295
325
|
|| typeof result !== 'object'
|
|
296
326
|
|| result.stopping !== true
|