@adhdev/daemon-core 0.7.43 → 0.7.45
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/commands/upgrade-helper.d.ts +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +441 -252
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +429 -241
- package/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +2 -2
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +2 -2
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +4 -4
- package/src/commands/router.ts +29 -23
- package/src/commands/upgrade-helper.ts +214 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
const UPGRADE_HELPER_ENV = 'ADHDEV_DAEMON_UPGRADE_HELPER';
|
|
8
|
+
|
|
9
|
+
export interface DaemonUpgradeHelperPayload {
|
|
10
|
+
packageName: string;
|
|
11
|
+
targetVersion: string;
|
|
12
|
+
parentPid: number;
|
|
13
|
+
restartArgv: string[];
|
|
14
|
+
cwd?: string;
|
|
15
|
+
sessionHostAppName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getUpgradeLogPath(): string {
|
|
19
|
+
const home = os.homedir();
|
|
20
|
+
const dir = path.join(home, '.adhdev');
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
return path.join(dir, 'daemon-upgrade.log');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function appendUpgradeLog(message: string): void {
|
|
26
|
+
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
27
|
+
try {
|
|
28
|
+
fs.appendFileSync(getUpgradeLogPath(), line, 'utf8');
|
|
29
|
+
} catch {
|
|
30
|
+
// noop
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getNpmExecutable(): string {
|
|
35
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function killPid(pid: number): boolean {
|
|
39
|
+
try {
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
42
|
+
} else {
|
|
43
|
+
process.kill(pid, 'SIGTERM');
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function waitForPidExit(pid: number, timeoutMs: number): Promise<void> {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
while (Date.now() - start < timeoutMs) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stopSessionHostProcesses(appName: string): void {
|
|
64
|
+
const pidFile = path.join(os.homedir(), '.adhdev', `${appName}-session-host.pid`);
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(pidFile)) {
|
|
67
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
68
|
+
if (Number.isFinite(pid)) {
|
|
69
|
+
killPid(pid);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// noop
|
|
74
|
+
} finally {
|
|
75
|
+
try {
|
|
76
|
+
fs.unlinkSync(pidFile);
|
|
77
|
+
} catch {
|
|
78
|
+
// noop
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.platform !== 'win32') {
|
|
83
|
+
try {
|
|
84
|
+
const raw = execFileSync('pgrep', ['-f', 'session-host-daemon'], { encoding: 'utf8' }).trim();
|
|
85
|
+
for (const line of raw.split('\n')) {
|
|
86
|
+
const pid = Number.parseInt(line.trim(), 10);
|
|
87
|
+
if (Number.isFinite(pid)) {
|
|
88
|
+
killPid(pid);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// noop
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function removeDaemonPidFile(): void {
|
|
98
|
+
const pidFile = path.join(os.homedir(), '.adhdev', 'daemon.pid');
|
|
99
|
+
try {
|
|
100
|
+
fs.unlinkSync(pidFile);
|
|
101
|
+
} catch {
|
|
102
|
+
// noop
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cleanupStaleGlobalInstallDirs(pkgName: string): void {
|
|
107
|
+
const npmRoot = execFileSync(getNpmExecutable(), ['root', '-g'], { encoding: 'utf8' }).trim();
|
|
108
|
+
if (!npmRoot) return;
|
|
109
|
+
const npmPrefix = execFileSync(getNpmExecutable(), ['prefix', '-g'], { encoding: 'utf8' }).trim();
|
|
110
|
+
const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
|
111
|
+
const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
|
|
112
|
+
const binNames = new Set<string>([packageBaseName]);
|
|
113
|
+
if (pkgName === '@adhdev/daemon-standalone') {
|
|
114
|
+
binNames.add('adhdev-standalone');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (pkgName.startsWith('@')) {
|
|
118
|
+
const [scope, name] = pkgName.split('/');
|
|
119
|
+
const scopeDir = path.join(npmRoot, scope);
|
|
120
|
+
if (!fs.existsSync(scopeDir)) return;
|
|
121
|
+
for (const entry of fs.readdirSync(scopeDir)) {
|
|
122
|
+
if (!entry.startsWith(`.${name}-`)) continue;
|
|
123
|
+
fs.rmSync(path.join(scopeDir, entry), { recursive: true, force: true });
|
|
124
|
+
appendUpgradeLog(`Removed stale scoped staging dir: ${path.join(scopeDir, entry)}`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
for (const entry of fs.readdirSync(npmRoot)) {
|
|
128
|
+
if (!entry.startsWith(`.${pkgName}-`)) continue;
|
|
129
|
+
fs.rmSync(path.join(npmRoot, entry), { recursive: true, force: true });
|
|
130
|
+
appendUpgradeLog(`Removed stale staging dir: ${path.join(npmRoot, entry)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (fs.existsSync(binDir)) {
|
|
135
|
+
for (const entry of fs.readdirSync(binDir)) {
|
|
136
|
+
if (![...binNames].some((name) => entry.startsWith(`.${name}-`))) continue;
|
|
137
|
+
fs.rmSync(path.join(binDir, entry), { recursive: true, force: true });
|
|
138
|
+
appendUpgradeLog(`Removed stale bin staging entry: ${path.join(binDir, entry)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function spawnDetachedDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): void {
|
|
144
|
+
const env = { ...process.env, [UPGRADE_HELPER_ENV]: JSON.stringify(payload) };
|
|
145
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: 'ignore',
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
cwd: payload.cwd || process.cwd(),
|
|
150
|
+
env,
|
|
151
|
+
});
|
|
152
|
+
child.unref();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function runDaemonUpgradeHelper(payload: DaemonUpgradeHelperPayload): Promise<void> {
|
|
156
|
+
const restartArgv = Array.isArray(payload.restartArgv) ? payload.restartArgv : [];
|
|
157
|
+
const sessionHostAppName = payload.sessionHostAppName || process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev';
|
|
158
|
+
appendUpgradeLog(`Upgrade helper started for ${payload.packageName}@${payload.targetVersion}`);
|
|
159
|
+
|
|
160
|
+
if (Number.isFinite(payload.parentPid) && payload.parentPid > 0) {
|
|
161
|
+
appendUpgradeLog(`Waiting for parent pid ${payload.parentPid} to exit`);
|
|
162
|
+
await waitForPidExit(payload.parentPid, 15000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
stopSessionHostProcesses(sessionHostAppName);
|
|
166
|
+
removeDaemonPidFile();
|
|
167
|
+
cleanupStaleGlobalInstallDirs(payload.packageName);
|
|
168
|
+
|
|
169
|
+
const spec = `${payload.packageName}@${payload.targetVersion || 'latest'}`;
|
|
170
|
+
appendUpgradeLog(`Installing ${spec}`);
|
|
171
|
+
const installOutput = execFileSync(
|
|
172
|
+
getNpmExecutable(),
|
|
173
|
+
['install', '-g', spec, '--force'],
|
|
174
|
+
{
|
|
175
|
+
encoding: 'utf8',
|
|
176
|
+
stdio: 'pipe',
|
|
177
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
if (installOutput.trim()) {
|
|
181
|
+
appendUpgradeLog(installOutput.trim());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (restartArgv.length > 0) {
|
|
185
|
+
const env = { ...process.env };
|
|
186
|
+
delete env[UPGRADE_HELPER_ENV];
|
|
187
|
+
appendUpgradeLog(`Restarting daemon with args: ${restartArgv.join(' ')}`);
|
|
188
|
+
const child = spawn(process.execPath, restartArgv, {
|
|
189
|
+
detached: true,
|
|
190
|
+
stdio: 'ignore',
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
cwd: payload.cwd || process.cwd(),
|
|
193
|
+
env,
|
|
194
|
+
});
|
|
195
|
+
child.unref();
|
|
196
|
+
} else {
|
|
197
|
+
appendUpgradeLog('No restart argv provided; upgrade completed without restart');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function maybeRunDaemonUpgradeHelperFromEnv(): Promise<boolean> {
|
|
202
|
+
const raw = process.env[UPGRADE_HELPER_ENV];
|
|
203
|
+
if (!raw) return false;
|
|
204
|
+
delete process.env[UPGRADE_HELPER_ENV];
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const payload = JSON.parse(raw) as DaemonUpgradeHelperPayload;
|
|
208
|
+
await runDaemonUpgradeHelper(payload);
|
|
209
|
+
process.exit(0);
|
|
210
|
+
} catch (error: any) {
|
|
211
|
+
appendUpgradeLog(`Upgrade helper failed: ${error?.stack || error?.message || String(error)}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -89,6 +89,7 @@ export { DaemonCommandHandler } from './commands/handler.js';
|
|
|
89
89
|
export type { CommandResult, CommandContext } from './commands/handler.js';
|
|
90
90
|
export { DaemonCommandRouter } from './commands/router.js';
|
|
91
91
|
export type { CommandRouterDeps, CommandRouterResult } from './commands/router.js';
|
|
92
|
+
export { maybeRunDaemonUpgradeHelperFromEnv } from './commands/upgrade-helper.js';
|
|
92
93
|
|
|
93
94
|
// ── Status ──
|
|
94
95
|
export { DaemonStatusReporter } from './status/reporter.js';
|