@adhdev/daemon-core 1.0.17 → 1.0.18-rc.10
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-adapters/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +7 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +63 -0
- package/dist/index.js +5818 -4496
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5853 -4525
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +204 -3
- package/src/cli-adapters/provider-cli-adapter.ts +39 -5
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/upgrade-helper.ts +146 -79
- package/src/commands/windows-atomic-upgrade.ts +631 -0
- package/src/config/mesh-json-config.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +256 -10
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-ledger.ts +72 -0
- package/src/mesh/mesh-queue-assignment.ts +126 -1
- package/src/mesh/mesh-reconcile-loop.ts +49 -0
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/providers/cli-provider-instance-types.ts +25 -0
- package/src/providers/cli-provider-instance.ts +116 -0
- package/src/session-host/managed-host.ts +34 -0
|
@@ -3,6 +3,19 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
+
import {
|
|
7
|
+
ADHDEV_OWNED_MARKERS,
|
|
8
|
+
createDefaultWindowsAtomicHooks,
|
|
9
|
+
findPortableNode22,
|
|
10
|
+
performWindowsAtomicUpgrade,
|
|
11
|
+
resolveWindowsInstallerLayout,
|
|
12
|
+
} from './windows-atomic-upgrade.js';
|
|
13
|
+
import {
|
|
14
|
+
getProcessCommandLine,
|
|
15
|
+
killProcess,
|
|
16
|
+
stopOwnedProcessesForPrefixes,
|
|
17
|
+
waitForPidExit,
|
|
18
|
+
} from './process-lifecycle.js';
|
|
6
19
|
|
|
7
20
|
const UPGRADE_HELPER_ENV = 'ADHDEV_DAEMON_UPGRADE_HELPER';
|
|
8
21
|
|
|
@@ -32,17 +45,16 @@ export interface PinnedGlobalInstallCommand {
|
|
|
32
45
|
|
|
33
46
|
export type NpmExecOptions = { shell: boolean; windowsHide?: boolean };
|
|
34
47
|
|
|
35
|
-
function getUpgradeLogPath(): string {
|
|
36
|
-
const home = os.homedir();
|
|
48
|
+
function getUpgradeLogPath(home: string = os.homedir()): string {
|
|
37
49
|
const dir = path.join(home, '.adhdev');
|
|
38
50
|
fs.mkdirSync(dir, { recursive: true });
|
|
39
51
|
return path.join(dir, 'daemon-upgrade.log');
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
function appendUpgradeLog(message: string): void {
|
|
54
|
+
function appendUpgradeLog(message: string, homeDir: string = os.homedir()): void {
|
|
43
55
|
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
44
56
|
try {
|
|
45
|
-
fs.appendFileSync(getUpgradeLogPath(), line, 'utf8');
|
|
57
|
+
fs.appendFileSync(getUpgradeLogPath(homeDir), line, 'utf8');
|
|
46
58
|
} catch {
|
|
47
59
|
// noop
|
|
48
60
|
}
|
|
@@ -132,19 +144,68 @@ function resolveInstallPrefixFromPackageRoot(packageRoot: string, packageName: s
|
|
|
132
144
|
return maybeLibDir;
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
// True when `prefix` is the bin dir of a portable Node 22 the installer manages
|
|
148
|
+
// under ~/.adhdev/tools/node22/<node-vX>/. A `npm i -g adhdev` run while that
|
|
149
|
+
// portable node is the active `node` installs adhdev into node's own default
|
|
150
|
+
// global prefix (= that dir) — a "legacy node22-prefix" install that lives at
|
|
151
|
+
// the FRONT of PATH (Enable-NodePath prepends node22) and shadows the canonical
|
|
152
|
+
// dispatcher shims in ~/.adhdev/npm-global. Because self-upgrade reuses the
|
|
153
|
+
// running prefix, that install then re-installs into the same node22 dir forever
|
|
154
|
+
// and never converts to the dispatcher. Detecting it lets us force convergence.
|
|
155
|
+
function isPortableNode22Prefix(prefix: string | null, homeDir: string): boolean {
|
|
156
|
+
if (!prefix) return false;
|
|
157
|
+
const portableRoot = path.join(homeDir, '.adhdev', 'tools', 'node22');
|
|
158
|
+
const normalizedPrefix = path.resolve(prefix).replace(/[\\/]+$/, '').toLowerCase();
|
|
159
|
+
const normalizedRoot = path.resolve(portableRoot).replace(/[\\/]+$/, '').toLowerCase();
|
|
160
|
+
return normalizedPrefix === normalizedRoot || normalizedPrefix.startsWith(`${normalizedRoot}${path.sep.toLowerCase()}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Redirect a legacy node22-prefix install to the canonical dispatcher install
|
|
164
|
+
// root so resolveWindowsInstallerLayout accepts it and the atomic-upgrade
|
|
165
|
+
// publishes the ~/.adhdev/npm-global pointer + shims. Prefer the version the
|
|
166
|
+
// dispatcher pointer already names (so we sit on the real active dispatcher
|
|
167
|
+
// prefix); otherwise synthesize a stable migration sentinel under npm-installs.
|
|
168
|
+
// resolveWindowsInstallerLayout only requires a `npm-installs/version-*` path —
|
|
169
|
+
// performWindowsAtomicUpgrade stages a fresh version- prefix of its own and uses
|
|
170
|
+
// this only as the "old prefix" to stop/clean, so a non-existent path is a no-op.
|
|
171
|
+
function canonicalDispatcherInstallPrefix(homeDir: string): string {
|
|
172
|
+
const installRoot = path.join(homeDir, '.adhdev', 'npm-installs');
|
|
173
|
+
const pointerPath = path.join(homeDir, '.adhdev', 'npm-global', '.adhdev-current');
|
|
174
|
+
try {
|
|
175
|
+
const activeVersion = fs.readFileSync(pointerPath, 'utf8').trim();
|
|
176
|
+
if (activeVersion.startsWith('version-')) return path.join(installRoot, activeVersion);
|
|
177
|
+
} catch {
|
|
178
|
+
// No dispatcher pointer yet (first migration off the legacy layout).
|
|
179
|
+
}
|
|
180
|
+
return path.join(installRoot, 'version-legacy-migrate');
|
|
181
|
+
}
|
|
182
|
+
|
|
135
183
|
export function resolveCurrentGlobalInstallSurface(options: {
|
|
136
184
|
packageName: string;
|
|
137
185
|
currentCliPath?: string;
|
|
138
186
|
nodeExecutable?: string;
|
|
139
187
|
platform?: NodeJS.Platform;
|
|
188
|
+
homeDir?: string;
|
|
140
189
|
}): CurrentGlobalInstallSurface {
|
|
141
190
|
const packageRoot = findCurrentPackageRoot(options.currentCliPath || process.argv[1], options.packageName);
|
|
142
191
|
const npmInvocation = resolveSiblingNpmInvocation(options.nodeExecutable || process.execPath, options.platform);
|
|
192
|
+
const platform = options.platform || process.platform;
|
|
193
|
+
const homeDir = options.homeDir || os.homedir();
|
|
194
|
+
let installPrefix = packageRoot ? resolveInstallPrefixFromPackageRoot(packageRoot, options.packageName) : null;
|
|
195
|
+
// FIX C: on Windows, never let a self-upgrade perpetuate the legacy
|
|
196
|
+
// node22-prefix install. If the running adhdev lives under ~/.adhdev/tools/
|
|
197
|
+
// node22, force the install onto the canonical dispatcher prefix so the update
|
|
198
|
+
// converges to the ~/.adhdev/npm-global pointer + shims. Scoped to win32 AND a
|
|
199
|
+
// tools/node22 prefix so npm-linked dev / standalone / real dispatcher installs
|
|
200
|
+
// are untouched.
|
|
201
|
+
if (platform === 'win32' && isPortableNode22Prefix(installPrefix, homeDir)) {
|
|
202
|
+
installPrefix = canonicalDispatcherInstallPrefix(homeDir);
|
|
203
|
+
}
|
|
143
204
|
return {
|
|
144
205
|
npmExecutable: npmInvocation.executable,
|
|
145
206
|
npmArgsPrefix: npmInvocation.argsPrefix,
|
|
146
207
|
packageRoot,
|
|
147
|
-
installPrefix
|
|
208
|
+
installPrefix,
|
|
148
209
|
execOptions: npmInvocation.execOptions,
|
|
149
210
|
};
|
|
150
211
|
}
|
|
@@ -230,77 +291,11 @@ export function execNpmCommandSync(
|
|
|
230
291
|
);
|
|
231
292
|
}
|
|
232
293
|
|
|
233
|
-
function killPid(pid: number): boolean {
|
|
234
|
-
try {
|
|
235
|
-
if (process.platform === 'win32') {
|
|
236
|
-
execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
237
|
-
} else {
|
|
238
|
-
process.kill(pid, 'SIGTERM');
|
|
239
|
-
}
|
|
240
|
-
return true;
|
|
241
|
-
} catch {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function getWindowsProcessCommandLine(pid: number): string | null {
|
|
247
|
-
const pidFilter = `ProcessId=${pid}`;
|
|
248
|
-
try {
|
|
249
|
-
const psOut = execFileSync('powershell.exe', [
|
|
250
|
-
'-NoProfile',
|
|
251
|
-
'-NonInteractive',
|
|
252
|
-
'-ExecutionPolicy', 'Bypass',
|
|
253
|
-
'-Command',
|
|
254
|
-
`(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
|
|
255
|
-
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
256
|
-
if (psOut) return psOut;
|
|
257
|
-
} catch {
|
|
258
|
-
// fall through to wmic fallback
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
const wmicOut = execFileSync('wmic', [
|
|
263
|
-
'process', 'where', pidFilter, 'get', 'CommandLine',
|
|
264
|
-
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
265
|
-
if (wmicOut) return wmicOut;
|
|
266
|
-
} catch {
|
|
267
|
-
// noop
|
|
268
|
-
}
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getProcessCommandLine(pid: number): string | null {
|
|
273
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
274
|
-
if (process.platform === 'win32') return getWindowsProcessCommandLine(pid);
|
|
275
|
-
try {
|
|
276
|
-
const text = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
277
|
-
encoding: 'utf8',
|
|
278
|
-
timeout: 3000,
|
|
279
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
280
|
-
}).trim();
|
|
281
|
-
return text || null;
|
|
282
|
-
} catch {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
294
|
function isManagedSessionHostPid(pid: number): boolean {
|
|
288
295
|
const commandLine = getProcessCommandLine(pid);
|
|
289
296
|
return !!commandLine && /session-host-daemon/i.test(commandLine);
|
|
290
297
|
}
|
|
291
298
|
|
|
292
|
-
async function waitForPidExit(pid: number, timeoutMs: number): Promise<void> {
|
|
293
|
-
const start = Date.now();
|
|
294
|
-
while (Date.now() - start < timeoutMs) {
|
|
295
|
-
try {
|
|
296
|
-
process.kill(pid, 0);
|
|
297
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
298
|
-
} catch {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
299
|
export async function stopSessionHostProcesses(appName: string): Promise<void> {
|
|
305
300
|
const pidFile = path.join(os.homedir(), '.adhdev', `${appName}-session-host.pid`);
|
|
306
301
|
let killedPid: number | null = null;
|
|
@@ -308,7 +303,7 @@ export async function stopSessionHostProcesses(appName: string): Promise<void> {
|
|
|
308
303
|
if (fs.existsSync(pidFile)) {
|
|
309
304
|
const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
310
305
|
if (Number.isFinite(pid) && pid !== process.pid && isManagedSessionHostPid(pid)) {
|
|
311
|
-
if (
|
|
306
|
+
if (killProcess(pid)) killedPid = pid;
|
|
312
307
|
}
|
|
313
308
|
}
|
|
314
309
|
} catch {
|
|
@@ -425,7 +420,7 @@ export async function stopForeignNativeAddonHolders(
|
|
|
425
420
|
appendUpgradeLog(
|
|
426
421
|
`Foreign native-addon holder found: pid ${holder.pid}${holder.commandLine ? ` — ${holder.commandLine}` : ''}`,
|
|
427
422
|
);
|
|
428
|
-
const killed =
|
|
423
|
+
const killed = killProcess(holder.pid);
|
|
429
424
|
if (killed) {
|
|
430
425
|
await waitForPidExit(holder.pid, 15000);
|
|
431
426
|
appendUpgradeLog(`Terminated foreign native-addon holder pid ${holder.pid}`);
|
|
@@ -437,8 +432,7 @@ export async function stopForeignNativeAddonHolders(
|
|
|
437
432
|
return results;
|
|
438
433
|
}
|
|
439
434
|
|
|
440
|
-
function getUpgradeFailureNoticePath(): string {
|
|
441
|
-
const home = os.homedir();
|
|
435
|
+
function getUpgradeFailureNoticePath(home: string = os.homedir()): string {
|
|
442
436
|
const dir = path.join(home, '.adhdev');
|
|
443
437
|
try {
|
|
444
438
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -459,11 +453,11 @@ function buildManualRecoveryCommand(installCommand: PinnedGlobalInstallCommand):
|
|
|
459
453
|
* log line: the pids/commandlines still holding the lock and a paste-ready
|
|
460
454
|
* recovery command. Written to a stable path the CLI can surface on next boot.
|
|
461
455
|
*/
|
|
462
|
-
function emitUpgradeFailureNotice(lines: string[]): void {
|
|
456
|
+
export function emitUpgradeFailureNotice(lines: string[], homeDir: string = os.homedir()): void {
|
|
463
457
|
const body = lines.join('\n');
|
|
464
|
-
appendUpgradeLog(`Upgrade blocked — user action required:\n${body}
|
|
458
|
+
appendUpgradeLog(`Upgrade blocked — user action required:\n${body}`, homeDir);
|
|
465
459
|
try {
|
|
466
|
-
fs.writeFileSync(getUpgradeFailureNoticePath(), `[${new Date().toISOString()}]\n${body}\n`, 'utf8');
|
|
460
|
+
fs.writeFileSync(getUpgradeFailureNoticePath(homeDir), `[${new Date().toISOString()}]\n${body}\n`, 'utf8');
|
|
467
461
|
} catch {
|
|
468
462
|
// noop
|
|
469
463
|
}
|
|
@@ -582,6 +576,73 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
582
576
|
|
|
583
577
|
await stopSessionHostProcesses(sessionHostAppName);
|
|
584
578
|
removeDaemonPidFile();
|
|
579
|
+
const windowsInstallerLayout = resolveWindowsInstallerLayout({
|
|
580
|
+
homeDir: os.homedir(),
|
|
581
|
+
installPrefix: installCommand.surface.installPrefix,
|
|
582
|
+
});
|
|
583
|
+
if (windowsInstallerLayout) {
|
|
584
|
+
const portableNode = findPortableNode22(os.homedir());
|
|
585
|
+
if (!portableNode) {
|
|
586
|
+
throw new Error('installer-managed Windows update requires the portable Node.js 22 runtime');
|
|
587
|
+
}
|
|
588
|
+
const npmCliPath = path.join(path.dirname(portableNode), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
589
|
+
if (!fs.existsSync(npmCliPath)) {
|
|
590
|
+
throw new Error(`portable Node.js 22 npm CLI is missing: ${npmCliPath}`);
|
|
591
|
+
}
|
|
592
|
+
appendUpgradeLog(`Installer-managed pointer layout detected; active prefix will remain untouched: ${windowsInstallerLayout.activePrefix}`);
|
|
593
|
+
|
|
594
|
+
// Terminate any ADHDev-owned process still executing from the current active
|
|
595
|
+
// prefix or the legacy stable shim tree before activation. The parent daemon
|
|
596
|
+
// and this helper itself are excluded: the parent is already exiting, and the
|
|
597
|
+
// helper must survive to complete the upgrade.
|
|
598
|
+
const upgradePids = [process.pid, payload.parentPid].filter((n): n is number => Number.isFinite(n) && n > 0);
|
|
599
|
+
const preStop = await stopOwnedProcessesForPrefixes({
|
|
600
|
+
prefixes: [windowsInstallerLayout.activePrefix, windowsInstallerLayout.stablePrefix],
|
|
601
|
+
excludePids: upgradePids,
|
|
602
|
+
markers: Array.from(ADHDEV_OWNED_MARKERS),
|
|
603
|
+
waitMs: 15_000,
|
|
604
|
+
log: appendUpgradeLog,
|
|
605
|
+
});
|
|
606
|
+
if (preStop.survivors.length > 0) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Cannot upgrade: owned processes still running under current prefix: ${preStop.survivors.map((s) => s.pid).join(', ')}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await performWindowsAtomicUpgrade({
|
|
614
|
+
layout: windowsInstallerLayout,
|
|
615
|
+
packageName: payload.packageName,
|
|
616
|
+
targetVersion: payload.targetVersion,
|
|
617
|
+
portableNode,
|
|
618
|
+
excludePids: upgradePids,
|
|
619
|
+
hooks: createDefaultWindowsAtomicHooks({
|
|
620
|
+
packageName: payload.packageName,
|
|
621
|
+
targetVersion: payload.targetVersion,
|
|
622
|
+
npmCliPath,
|
|
623
|
+
restartArgv,
|
|
624
|
+
cwd: payload.cwd || process.cwd(),
|
|
625
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== UPGRADE_HELPER_ENV)),
|
|
626
|
+
log: appendUpgradeLog,
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
} catch (error: any) {
|
|
630
|
+
// performWindowsAtomicUpgrade already rolled the pointer/shims back to the
|
|
631
|
+
// prior version before rethrowing. Without a durable notice that rollback
|
|
632
|
+
// was silent — the daemon simply kept running the old version (the rc.6
|
|
633
|
+
// stuck-upgrade defect). Leave an actionable last-error file naming the
|
|
634
|
+
// target that failed its health/version gate.
|
|
635
|
+
emitUpgradeFailureNotice([
|
|
636
|
+
`adhdev ${payload.packageName}@${payload.targetVersion} upgrade failed and was rolled back: ${error?.message || String(error)}`,
|
|
637
|
+
`Previous version preserved (active prefix: ${windowsInstallerLayout.activePrefix}).`,
|
|
638
|
+
'See daemon-upgrade.log for the full install/health trace. The next daemon start will retry.',
|
|
639
|
+
]);
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
643
|
+
appendUpgradeLog('Installer-managed Windows atomic upgrade completed');
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
585
646
|
// Kill any *foreign* process still holding this install's conpty.node mapped
|
|
586
647
|
// (the session-host stop above only covers the single managed pid). Do this
|
|
587
648
|
// BEFORE the staging GC so the just-released file can also be cleaned up now
|
|
@@ -671,6 +732,7 @@ async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Prom
|
|
|
671
732
|
} else {
|
|
672
733
|
appendUpgradeLog('No restart argv provided; upgrade completed without restart');
|
|
673
734
|
}
|
|
735
|
+
try { fs.unlinkSync(getUpgradeFailureNoticePath()); } catch { /* no previous failure notice */ }
|
|
674
736
|
}
|
|
675
737
|
|
|
676
738
|
export async function maybeRunDaemonUpgradeHelperFromEnv(): Promise<boolean> {
|
|
@@ -683,7 +745,12 @@ export async function maybeRunDaemonUpgradeHelperFromEnv(): Promise<boolean> {
|
|
|
683
745
|
await runDaemonUpgradeHelper(payload);
|
|
684
746
|
process.exit(0);
|
|
685
747
|
} catch (error: any) {
|
|
686
|
-
|
|
748
|
+
const detail = error?.stack || error?.message || String(error);
|
|
749
|
+
appendUpgradeLog(`Upgrade helper failed: ${detail}`);
|
|
750
|
+
emitUpgradeFailureNotice([
|
|
751
|
+
`adhdev upgrade failed: ${error?.message || String(error)}`,
|
|
752
|
+
`See ${getUpgradeLogPath()} for details. The previous installer-managed version was preserved or restored when available.`,
|
|
753
|
+
]);
|
|
687
754
|
process.exit(1);
|
|
688
755
|
}
|
|
689
756
|
}
|