@adhdev/daemon-core 0.7.42 → 0.7.44
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/provider-cli-adapter.d.ts +2 -4
- package/dist/cli-adapters/pty-transport.d.ts +1 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +4 -0
- package/dist/cli-adapters/terminal-backends/types.d.ts +4 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +4 -0
- package/dist/cli-adapters/terminal-screen.d.ts +4 -0
- package/dist/commands/upgrade-helper.d.ts +10 -0
- package/dist/config/chat-history.d.ts +0 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +509 -364
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +497 -353
- package/dist/index.mjs.map +1 -1
- package/dist/providers/provider-instance.d.ts +0 -1
- package/dist/status/normalize.js +0 -7
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +0 -7
- package/dist/status/normalize.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 +79 -70
- package/src/cli-adapters/pty-transport.ts +2 -0
- package/src/cli-adapters/session-host-transport.ts +1 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +5 -0
- package/src/cli-adapters/terminal-backends/types.ts +1 -0
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +10 -0
- package/src/cli-adapters/terminal-screen.ts +4 -0
- package/src/commands/router.ts +29 -23
- package/src/commands/upgrade-helper.ts +214 -0
- package/src/config/chat-history.ts +3 -55
- package/src/index.ts +1 -0
- package/src/providers/cli-provider-instance.ts +1 -11
- package/src/providers/provider-instance.d.ts +0 -1
- package/src/providers/provider-instance.ts +0 -1
- package/src/status/normalize.ts +0 -4
|
@@ -64,7 +64,6 @@ export interface CliSessionStatus {
|
|
|
64
64
|
messages: CliChatMessage[];
|
|
65
65
|
workingDir: string;
|
|
66
66
|
activeModal: { message: string; buttons: string[] } | null;
|
|
67
|
-
terminalHistory?: string;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/**
|
|
@@ -90,7 +89,6 @@ export interface CliScriptInput {
|
|
|
90
89
|
rawBuffer: string; // Raw PTY output (with ANSI)
|
|
91
90
|
recentBuffer: string; // Recent 1000 chars (ANSI-stripped)
|
|
92
91
|
screenText: string; // Current visible screen snapshot
|
|
93
|
-
terminalHistory?: string; // Rolling append-only terminal transcript
|
|
94
92
|
messages: CliChatMessage[]; // Previously parsed messages
|
|
95
93
|
partialResponse: string; // Current partial response being generated
|
|
96
94
|
}
|
|
@@ -100,7 +98,6 @@ interface TurnParseScope {
|
|
|
100
98
|
startedAt: number;
|
|
101
99
|
bufferStart: number;
|
|
102
100
|
rawBufferStart: number;
|
|
103
|
-
terminalHistoryStart: number;
|
|
104
101
|
}
|
|
105
102
|
|
|
106
103
|
export interface CliProviderModule {
|
|
@@ -173,6 +170,51 @@ function sanitizeTerminalText(str: string): string {
|
|
|
173
170
|
return stripTerminalNoise(stripAnsi(str));
|
|
174
171
|
}
|
|
175
172
|
|
|
173
|
+
function buildCliSpawnEnv(baseEnv: NodeJS.ProcessEnv, overrides?: Record<string, string>): Record<string, string> {
|
|
174
|
+
const env: Record<string, string> = {};
|
|
175
|
+
const source = { ...baseEnv, ...(overrides || {}) } as NodeJS.ProcessEnv;
|
|
176
|
+
|
|
177
|
+
for (const [key, value] of Object.entries(source)) {
|
|
178
|
+
if (typeof value !== 'string') continue;
|
|
179
|
+
env[key] = value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const key of Object.keys(env)) {
|
|
183
|
+
if (
|
|
184
|
+
key === 'INIT_CWD'
|
|
185
|
+
|| key === 'NO_COLOR'
|
|
186
|
+
|| key === 'FORCE_COLOR'
|
|
187
|
+
|| key === 'npm_command'
|
|
188
|
+
|| key === 'npm_execpath'
|
|
189
|
+
|| key === 'npm_node_execpath'
|
|
190
|
+
|| key.startsWith('npm_')
|
|
191
|
+
|| key.startsWith('npm_config_')
|
|
192
|
+
|| key.startsWith('npm_package_')
|
|
193
|
+
|| key.startsWith('npm_lifecycle_')
|
|
194
|
+
|| key.startsWith('PNPM_')
|
|
195
|
+
|| key.startsWith('YARN_')
|
|
196
|
+
|| key.startsWith('BUN_')
|
|
197
|
+
) {
|
|
198
|
+
delete env[key];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return env;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function computeTerminalQueryTail(buffer: string): string {
|
|
206
|
+
const prefixes = ['\x1b[6n', '\x1b[?6n'];
|
|
207
|
+
const maxLength = prefixes.reduce((n, value) => Math.max(n, value.length), 0) - 1;
|
|
208
|
+
const start = Math.max(0, buffer.length - maxLength);
|
|
209
|
+
for (let i = start; i < buffer.length; i++) {
|
|
210
|
+
const suffix = buffer.slice(i);
|
|
211
|
+
if (prefixes.some((pattern) => suffix.length < pattern.length && pattern.startsWith(suffix))) {
|
|
212
|
+
return suffix;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
|
|
176
218
|
function findBinary(name: string): string {
|
|
177
219
|
const isWin = os.platform() === 'win32';
|
|
178
220
|
try {
|
|
@@ -284,44 +326,6 @@ function promptLikelyVisible(screenText: string, promptSnippet: string): boolean
|
|
|
284
326
|
return matched >= required;
|
|
285
327
|
}
|
|
286
328
|
|
|
287
|
-
function splitHistoryLines(text: string): string[] {
|
|
288
|
-
return String(text || '')
|
|
289
|
-
.split('\n')
|
|
290
|
-
.map(line => line.replace(/\s+$/, ''));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function normalizeHistoryLine(line: string): string {
|
|
294
|
-
return String(line || '').replace(/\s+/g, ' ').trim();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function mergeTerminalHistory(existing: string, snapshot: string): string {
|
|
298
|
-
const next = String(snapshot || '').trim();
|
|
299
|
-
if (!next) return existing;
|
|
300
|
-
const prev = String(existing || '').trim();
|
|
301
|
-
if (!prev) return next;
|
|
302
|
-
if (prev === next || prev.endsWith(next)) return prev;
|
|
303
|
-
|
|
304
|
-
const prevLines = splitHistoryLines(prev);
|
|
305
|
-
const nextLines = splitHistoryLines(next);
|
|
306
|
-
const prevNorm = prevLines.map(normalizeHistoryLine);
|
|
307
|
-
const nextNorm = nextLines.map(normalizeHistoryLine);
|
|
308
|
-
|
|
309
|
-
const maxOverlap = Math.min(prevLines.length, nextLines.length);
|
|
310
|
-
for (let overlap = maxOverlap; overlap >= 1; overlap -= 1) {
|
|
311
|
-
const prevTail = prevNorm.slice(prevNorm.length - overlap);
|
|
312
|
-
const nextHead = nextNorm.slice(0, overlap);
|
|
313
|
-
if (prevTail.every((line, index) => line === nextHead[index])) {
|
|
314
|
-
return [...prevLines, ...nextLines.slice(overlap)].join('\n').trim();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const compactPrev = prevNorm.join('\n');
|
|
319
|
-
const compactNext = nextNorm.join('\n');
|
|
320
|
-
if (compactPrev.includes(compactNext)) return prev;
|
|
321
|
-
|
|
322
|
-
return `${prev}\n${next}`.trim();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
329
|
/**
|
|
326
330
|
* Normalize provider.json for auto-implement approval detection.
|
|
327
331
|
* Kept for backward compat with dev-server auto-impl pipeline only.
|
|
@@ -390,6 +394,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
390
394
|
private pendingOutputParseTimer: NodeJS.Timeout | null = null;
|
|
391
395
|
private ptyOutputBuffer = '';
|
|
392
396
|
private ptyOutputFlushTimer: NodeJS.Timeout | null = null;
|
|
397
|
+
private pendingTerminalQueryTail = '';
|
|
393
398
|
|
|
394
399
|
// Server log forwarding
|
|
395
400
|
private serverConn: any = null;
|
|
@@ -428,9 +433,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
428
433
|
/** Full accumulated raw PTY output (with ANSI) */
|
|
429
434
|
private accumulatedRawBuffer: string = '';
|
|
430
435
|
/** Current visible terminal screen snapshot */
|
|
431
|
-
private terminalScreen = new TerminalScreen(
|
|
432
|
-
/** Rolling append-only terminal transcript built from screen snapshots */
|
|
433
|
-
private terminalHistory: string = '';
|
|
436
|
+
private terminalScreen = new TerminalScreen(30, 100);
|
|
434
437
|
/** Max accumulated buffer size (last 50KB) */
|
|
435
438
|
private static readonly MAX_ACCUMULATED_BUFFER = 50000;
|
|
436
439
|
private currentTurnScope: TurnParseScope | null = null;
|
|
@@ -461,23 +464,17 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
461
464
|
|
|
462
465
|
private buildParseInput(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): CliScriptInput {
|
|
463
466
|
const buffer = scope
|
|
464
|
-
? (this.sliceFromOffset(this.
|
|
465
|
-
|| this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart)
|
|
466
|
-
|| this.accumulatedBuffer)
|
|
467
|
+
? (this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart) || this.accumulatedBuffer)
|
|
467
468
|
: this.accumulatedBuffer;
|
|
468
469
|
const rawBuffer = scope
|
|
469
470
|
? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
|
|
470
471
|
: this.accumulatedRawBuffer;
|
|
471
|
-
const terminalHistory = scope
|
|
472
|
-
? (this.sliceFromOffset(this.terminalHistory, scope.terminalHistoryStart) || this.terminalHistory)
|
|
473
|
-
: this.terminalHistory;
|
|
474
472
|
|
|
475
473
|
return {
|
|
476
474
|
buffer,
|
|
477
475
|
rawBuffer,
|
|
478
476
|
recentBuffer: buffer.slice(-1000) || this.recentOutputBuffer,
|
|
479
477
|
screenText: this.terminalScreen.getText(),
|
|
480
|
-
terminalHistory,
|
|
481
478
|
messages: [...baseMessages],
|
|
482
479
|
partialResponse,
|
|
483
480
|
};
|
|
@@ -623,13 +620,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
623
620
|
}
|
|
624
621
|
|
|
625
622
|
const ptyOpts = {
|
|
626
|
-
cols:
|
|
627
|
-
rows:
|
|
623
|
+
cols: 100,
|
|
624
|
+
rows: 30,
|
|
628
625
|
cwd: this.workingDir,
|
|
629
|
-
env:
|
|
630
|
-
...process.env,
|
|
631
|
-
...spawnConfig.env,
|
|
632
|
-
} as Record<string, string>,
|
|
626
|
+
env: buildCliSpawnEnv(process.env, spawnConfig.env),
|
|
633
627
|
};
|
|
634
628
|
|
|
635
629
|
try {
|
|
@@ -650,9 +644,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
650
644
|
this.ptyProcess.onData((data: string) => {
|
|
651
645
|
if (Date.now() < this.resizeSuppressUntil) return;
|
|
652
646
|
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
this.ptyProcess?.write('\x1b[1;1R');
|
|
647
|
+
if (!this.ptyProcess?.terminalQueriesHandled) {
|
|
648
|
+
this.respondToTerminalQueries(data);
|
|
656
649
|
}
|
|
657
650
|
|
|
658
651
|
this.pendingOutputParseBuffer += data;
|
|
@@ -691,8 +684,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
691
684
|
this.spawnAt = Date.now();
|
|
692
685
|
this.startupParseGate = true;
|
|
693
686
|
this.startupBuffer = '';
|
|
694
|
-
this.terminalScreen.reset(
|
|
695
|
-
this.
|
|
687
|
+
this.terminalScreen.reset(30, 100);
|
|
688
|
+
this.pendingTerminalQueryTail = '';
|
|
696
689
|
this.currentTurnScope = null;
|
|
697
690
|
this.ready = false;
|
|
698
691
|
await this.ptyProcess.ready;
|
|
@@ -704,7 +697,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
704
697
|
|
|
705
698
|
private handleOutput(rawData: string): void {
|
|
706
699
|
this.terminalScreen.write(rawData);
|
|
707
|
-
this.terminalHistory = mergeTerminalHistory(this.terminalHistory, this.terminalScreen.getText());
|
|
708
700
|
const cleanData = sanitizeTerminalText(rawData);
|
|
709
701
|
|
|
710
702
|
if (this.isWaitingForResponse && cleanData) {
|
|
@@ -1013,7 +1005,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1013
1005
|
messages: [...this.committedMessages],
|
|
1014
1006
|
workingDir: this.workingDir,
|
|
1015
1007
|
activeModal: this.activeModal,
|
|
1016
|
-
terminalHistory: this.terminalHistory,
|
|
1017
1008
|
};
|
|
1018
1009
|
}
|
|
1019
1010
|
|
|
@@ -1032,7 +1023,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1032
1023
|
id: parsed.id || 'cli_session',
|
|
1033
1024
|
status: parsed.status || this.currentStatus,
|
|
1034
1025
|
title: parsed.title || this.cliName,
|
|
1035
|
-
terminalHistory: this.terminalHistory,
|
|
1036
1026
|
messages: parsed.messages,
|
|
1037
1027
|
activeModal: parsed.activeModal ?? this.activeModal,
|
|
1038
1028
|
};
|
|
@@ -1043,7 +1033,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1043
1033
|
id: 'cli_session',
|
|
1044
1034
|
status: this.currentStatus,
|
|
1045
1035
|
title: this.cliName,
|
|
1046
|
-
terminalHistory: this.terminalHistory,
|
|
1047
1036
|
messages: messages.slice(-50).map((message, index) => ({
|
|
1048
1037
|
id: `msg_${index}`,
|
|
1049
1038
|
role: message.role,
|
|
@@ -1114,9 +1103,8 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1114
1103
|
startedAt: Date.now(),
|
|
1115
1104
|
bufferStart: this.accumulatedBuffer.length,
|
|
1116
1105
|
rawBufferStart: this.accumulatedRawBuffer.length,
|
|
1117
|
-
terminalHistoryStart: this.terminalHistory.length,
|
|
1118
1106
|
};
|
|
1119
|
-
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart}
|
|
1107
|
+
LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
|
|
1120
1108
|
this.submitRetryUsed = false;
|
|
1121
1109
|
this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
|
|
1122
1110
|
const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
|
|
@@ -1304,6 +1292,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1304
1292
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1305
1293
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
1306
1294
|
this.pendingOutputParseBuffer = '';
|
|
1295
|
+
this.pendingTerminalQueryTail = '';
|
|
1307
1296
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1308
1297
|
this.ptyOutputBuffer = '';
|
|
1309
1298
|
if (this.ptyProcess) {
|
|
@@ -1326,6 +1315,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1326
1315
|
if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
|
|
1327
1316
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
1328
1317
|
this.pendingOutputParseBuffer = '';
|
|
1318
|
+
this.pendingTerminalQueryTail = '';
|
|
1329
1319
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1330
1320
|
this.ptyOutputBuffer = '';
|
|
1331
1321
|
if (this.ptyProcess) {
|
|
@@ -1349,12 +1339,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1349
1339
|
this.syncMessageViews();
|
|
1350
1340
|
this.accumulatedBuffer = '';
|
|
1351
1341
|
this.accumulatedRawBuffer = '';
|
|
1352
|
-
this.terminalHistory = '';
|
|
1353
1342
|
this.currentTurnScope = null;
|
|
1354
1343
|
this.submitRetryUsed = false;
|
|
1355
1344
|
this.submitRetryPromptSnippet = '';
|
|
1356
1345
|
if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
|
|
1357
1346
|
this.pendingOutputParseBuffer = '';
|
|
1347
|
+
this.pendingTerminalQueryTail = '';
|
|
1358
1348
|
if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
|
|
1359
1349
|
this.ptyOutputBuffer = '';
|
|
1360
1350
|
this.terminalScreen.reset();
|
|
@@ -1413,7 +1403,6 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1413
1403
|
structuredMessages: this.structuredMessages.slice(-20),
|
|
1414
1404
|
messageCount: this.committedMessages.length,
|
|
1415
1405
|
screenText: sanitizeTerminalText(this.terminalScreen.getText()).slice(-4000),
|
|
1416
|
-
terminalHistory: this.terminalHistory.slice(-8000),
|
|
1417
1406
|
currentTurnScope: this.currentTurnScope,
|
|
1418
1407
|
startupBuffer: this.startupBuffer.slice(-4000),
|
|
1419
1408
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
@@ -1441,4 +1430,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
1441
1430
|
ptyAlive: !!this.ptyProcess,
|
|
1442
1431
|
};
|
|
1443
1432
|
}
|
|
1433
|
+
|
|
1434
|
+
private respondToTerminalQueries(data: string): void {
|
|
1435
|
+
if (!this.ptyProcess || !data) return;
|
|
1436
|
+
|
|
1437
|
+
const combined = this.pendingTerminalQueryTail + data;
|
|
1438
|
+
const regex = /\x1b\[(\?)?6n/g;
|
|
1439
|
+
let match: RegExpExecArray | null;
|
|
1440
|
+
|
|
1441
|
+
while ((match = regex.exec(combined)) !== null) {
|
|
1442
|
+
const cursor = this.terminalScreen.getCursorPosition();
|
|
1443
|
+
const row = Math.max(1, (cursor.row | 0) + 1);
|
|
1444
|
+
const col = Math.max(1, (cursor.col | 0) + 1);
|
|
1445
|
+
const response = match[1]
|
|
1446
|
+
? `\x1b[?${row};${col}R`
|
|
1447
|
+
: `\x1b[${row};${col}R`;
|
|
1448
|
+
this.ptyProcess.write(response);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
this.pendingTerminalQueryTail = computeTerminalQueryTail(combined);
|
|
1452
|
+
}
|
|
1444
1453
|
}
|
|
@@ -37,6 +37,7 @@ export interface PtyRuntimeMetadata {
|
|
|
37
37
|
export interface PtyRuntimeTransport {
|
|
38
38
|
readonly pid: number;
|
|
39
39
|
readonly ready: Promise<void>;
|
|
40
|
+
readonly terminalQueriesHandled?: boolean;
|
|
40
41
|
write(data: string): void;
|
|
41
42
|
resize(cols: number, rows: number): void;
|
|
42
43
|
kill(): void;
|
|
@@ -53,6 +54,7 @@ export interface PtyTransportFactory {
|
|
|
53
54
|
|
|
54
55
|
class NodePtyRuntimeTransport implements PtyRuntimeTransport {
|
|
55
56
|
readonly ready = Promise.resolve();
|
|
57
|
+
readonly terminalQueriesHandled = false;
|
|
56
58
|
|
|
57
59
|
constructor(private readonly handle: any) {}
|
|
58
60
|
|
|
@@ -28,6 +28,7 @@ interface SessionHostRuntimeOptions extends SessionHostPtyTransportFactoryOption
|
|
|
28
28
|
|
|
29
29
|
class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
30
30
|
readonly ready: Promise<void>;
|
|
31
|
+
readonly terminalQueriesHandled = true;
|
|
31
32
|
|
|
32
33
|
private readonly client: SessionHostClient;
|
|
33
34
|
private readonly dataCallbacks = new Set<(data: string) => void>();
|
|
@@ -8,6 +8,7 @@ type GhosttyVtTerminal = {
|
|
|
8
8
|
write(data: string | Uint8Array): void;
|
|
9
9
|
resize(cols: number, rows: number): void;
|
|
10
10
|
formatPlainText(options?: { trim?: boolean }): string;
|
|
11
|
+
getCursorPosition(): { col: number; row: number };
|
|
11
12
|
dispose(): void;
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -123,6 +124,10 @@ export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
|
|
|
123
124
|
return this.terminal.formatPlainText({ trim: true }) || '';
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
getCursorPosition(): { col: number; row: number } {
|
|
128
|
+
return this.terminal.getCursorPosition();
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
dispose(): void {
|
|
127
132
|
this.terminal.dispose();
|
|
128
133
|
}
|
|
@@ -7,6 +7,8 @@ type XtermBufferLine = {
|
|
|
7
7
|
type XtermBuffer = {
|
|
8
8
|
length: number;
|
|
9
9
|
viewportY: number;
|
|
10
|
+
cursorX?: number;
|
|
11
|
+
cursorY?: number;
|
|
10
12
|
getLine(index: number): XtermBufferLine | undefined;
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -72,6 +74,14 @@ export class XtermTerminalBackend implements TerminalViewportBackend {
|
|
|
72
74
|
return lines.slice(first, last).join('\n');
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
getCursorPosition(): { col: number; row: number } {
|
|
78
|
+
const buffer = this.terminal.buffer.active;
|
|
79
|
+
return {
|
|
80
|
+
col: Math.max(0, buffer.cursorX || 0),
|
|
81
|
+
row: Math.max(0, buffer.cursorY || 0),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
dispose(): void {
|
|
76
86
|
this.terminal.dispose();
|
|
77
87
|
}
|
package/src/commands/router.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { logCommand } from '../logging/command-log.js';
|
|
|
26
26
|
import { getRecentLogs, LOG_PATH } from '../logging/logger.js';
|
|
27
27
|
import { buildSessionEntries } from '../status/builders.js';
|
|
28
28
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
29
|
+
import { spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
29
30
|
import * as fs from 'fs';
|
|
30
31
|
|
|
31
32
|
// ─── Types ───
|
|
@@ -315,36 +316,41 @@ export class DaemonCommandRouter {
|
|
|
315
316
|
// Check latest version
|
|
316
317
|
const latest = execSync(`npm view ${pkgName} version`, { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
317
318
|
LOG.info('Upgrade', `Latest ${pkgName}: v${latest}`);
|
|
319
|
+
let currentInstalled: string | null = null;
|
|
320
|
+
try {
|
|
321
|
+
const currentJson = execSync(`npm ls -g ${pkgName} --depth=0 --json`, {
|
|
322
|
+
encoding: 'utf-8',
|
|
323
|
+
timeout: 10000,
|
|
324
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
325
|
+
}).trim();
|
|
326
|
+
const parsed = JSON.parse(currentJson);
|
|
327
|
+
currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
|
|
328
|
+
} catch {
|
|
329
|
+
// ignore ls failures; upgrade can still proceed
|
|
330
|
+
}
|
|
318
331
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
if (currentInstalled === latest) {
|
|
333
|
+
LOG.info('Upgrade', `Already on latest version v${latest}; skipping install`);
|
|
334
|
+
return { success: true, upgraded: false, alreadyLatest: true, version: latest };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
spawnDetachedDaemonUpgradeHelper({
|
|
338
|
+
packageName: pkgName,
|
|
339
|
+
targetVersion: latest,
|
|
340
|
+
parentPid: process.pid,
|
|
341
|
+
restartArgv: process.argv.slice(1),
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
|
|
324
344
|
});
|
|
325
|
-
LOG.info('Upgrade',
|
|
345
|
+
LOG.info('Upgrade', `Scheduled detached upgrade to v${latest}`);
|
|
326
346
|
|
|
327
|
-
//
|
|
347
|
+
// Exit after the command response has been sent so the helper can replace the package cleanly.
|
|
328
348
|
setTimeout(() => {
|
|
329
|
-
LOG.info('Upgrade', '
|
|
330
|
-
// Remove PID file so the new process doesn't see 'already running'
|
|
331
|
-
try {
|
|
332
|
-
const path = require('path');
|
|
333
|
-
const fs = require('fs');
|
|
334
|
-
const pidFile = path.join(process.env.HOME || process.env.USERPROFILE || '', '.adhdev', 'daemon.pid');
|
|
335
|
-
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
|
336
|
-
} catch { /* ignore */ }
|
|
337
|
-
const { spawn } = require('child_process');
|
|
338
|
-
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
339
|
-
detached: true,
|
|
340
|
-
stdio: 'ignore',
|
|
341
|
-
env: { ...process.env },
|
|
342
|
-
});
|
|
343
|
-
child.unref();
|
|
349
|
+
LOG.info('Upgrade', 'Exiting daemon so detached upgrader can continue...');
|
|
344
350
|
process.exit(0);
|
|
345
351
|
}, 3000);
|
|
346
352
|
|
|
347
|
-
return { success: true, upgraded: true, version: latest };
|
|
353
|
+
return { success: true, upgraded: true, version: latest, restarting: true };
|
|
348
354
|
} catch (e: any) {
|
|
349
355
|
LOG.error('Upgrade', `Failed: ${e.message}`);
|
|
350
356
|
return { success: false, error: e.message };
|
|
@@ -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
|
+
}
|