subconscious-cli 6.2.0 → 6.2.1
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/bin/command-output.js +75 -0
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/sessions.js +19 -24
- package/bin/tui.js +18 -8
- package/bin/windows/process.js +1 -18
- package/package.json +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { spawnWindows } from './windows/process.js';
|
|
3
|
+
|
|
4
|
+
export const COMMAND_TIMEOUT_MS = 5_000;
|
|
5
|
+
const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run a command and collect its stdout without blocking the event loop.
|
|
9
|
+
*
|
|
10
|
+
* A synchronous spawn with a timeout only sends SIGTERM and then waits for the
|
|
11
|
+
* child to exit, so a child that ignores SIGTERM blocks the process for as long
|
|
12
|
+
* as it runs. This sends SIGKILL at the deadline and stops reading at once,
|
|
13
|
+
* even when a grandchild still holds the pipe open.
|
|
14
|
+
*/
|
|
15
|
+
export function runCommand(command, args, options = {}) {
|
|
16
|
+
const spawnImpl =
|
|
17
|
+
options.spawn || (process.platform === 'win32' ? spawnWindows : spawn);
|
|
18
|
+
const timeoutMs = options.timeout ?? COMMAND_TIMEOUT_MS;
|
|
19
|
+
const signal = options.signal;
|
|
20
|
+
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
resolve({ status: null, stdout: '', error: new Error('cancelled') });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let child;
|
|
28
|
+
try {
|
|
29
|
+
child = spawnImpl(command, args, {
|
|
30
|
+
env: options.env || process.env,
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
resolve({ status: null, stdout: '', error });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let stdout = '';
|
|
40
|
+
let settled = false;
|
|
41
|
+
let timer;
|
|
42
|
+
const finish = (result) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
signal?.removeEventListener('abort', onAbort);
|
|
47
|
+
resolve(result);
|
|
48
|
+
};
|
|
49
|
+
const stop = (message) => {
|
|
50
|
+
try {
|
|
51
|
+
child.kill('SIGKILL');
|
|
52
|
+
} catch {
|
|
53
|
+
// The child already exited.
|
|
54
|
+
}
|
|
55
|
+
child.stdout?.destroy();
|
|
56
|
+
finish({ status: null, stdout: '', error: new Error(message) });
|
|
57
|
+
};
|
|
58
|
+
const onAbort = () => stop('cancelled');
|
|
59
|
+
|
|
60
|
+
timer = setTimeout(
|
|
61
|
+
() => stop(`${command} timed out after ${timeoutMs}ms`),
|
|
62
|
+
timeoutMs,
|
|
63
|
+
);
|
|
64
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
|
|
66
|
+
child.stdout?.setEncoding('utf8');
|
|
67
|
+
child.stdout?.on('data', (chunk) => {
|
|
68
|
+
stdout += chunk;
|
|
69
|
+
if (stdout.length > MAX_OUTPUT_BYTES) stop(`${command} output too large`);
|
|
70
|
+
});
|
|
71
|
+
child.stdout?.on('error', () => {});
|
|
72
|
+
child.once('error', (error) => finish({ status: null, stdout: '', error }));
|
|
73
|
+
child.once('close', (status) => finish({ status, stdout }));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/sessions.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
1
|
import fs from 'node:fs/promises';
|
|
3
2
|
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
@@ -6,7 +5,7 @@ import { AGENTS, agentById, agentCommandName } from './agent-data.js';
|
|
|
6
5
|
import { resolveAgent, runAgent } from './agents.js';
|
|
7
6
|
|
|
8
7
|
import { c } from './colors.js';
|
|
9
|
-
import {
|
|
8
|
+
import { runCommand } from './command-output.js';
|
|
10
9
|
|
|
11
10
|
const MAX_CANDIDATE_FILES = 160;
|
|
12
11
|
const MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
@@ -399,12 +398,11 @@ async function discoverCodexIndex(root, indexFile, max) {
|
|
|
399
398
|
);
|
|
400
399
|
}
|
|
401
400
|
|
|
402
|
-
function commandResult(execute, command, args, options = {}) {
|
|
403
|
-
const result = execute(command, args, {
|
|
404
|
-
|
|
405
|
-
|
|
401
|
+
async function commandResult(execute, command, args, options = {}) {
|
|
402
|
+
const result = await execute(command, args, {
|
|
403
|
+
timeout: options.timeout,
|
|
404
|
+
signal: options.signal,
|
|
406
405
|
env: { ...process.env, NO_COLOR: '1' },
|
|
407
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
408
406
|
});
|
|
409
407
|
if (result.error || result.status !== 0) return '';
|
|
410
408
|
return String(result.stdout || '').trim();
|
|
@@ -422,18 +420,18 @@ function sqlString(value) {
|
|
|
422
420
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
423
421
|
}
|
|
424
422
|
|
|
425
|
-
function discoverOpenCodeSessions(execute, max) {
|
|
423
|
+
async function discoverOpenCodeSessions(execute, max, options = {}) {
|
|
426
424
|
const query = [
|
|
427
425
|
'select id, title, directory, time_created, time_updated, model',
|
|
428
426
|
'from session where time_archived is null',
|
|
429
427
|
`order by time_updated desc limit ${Math.max(1, Math.min(500, max))}`,
|
|
430
428
|
].join(' ');
|
|
431
|
-
const output = commandResult(
|
|
432
|
-
|
|
433
|
-
'
|
|
434
|
-
'json',
|
|
435
|
-
|
|
436
|
-
|
|
429
|
+
const output = await commandResult(
|
|
430
|
+
execute,
|
|
431
|
+
'opencode',
|
|
432
|
+
['db', '--format', 'json', query],
|
|
433
|
+
options,
|
|
434
|
+
);
|
|
437
435
|
if (!output) return [];
|
|
438
436
|
try {
|
|
439
437
|
return JSON.parse(output).map((record) =>
|
|
@@ -452,9 +450,7 @@ function discoverOpenCodeSessions(execute, max) {
|
|
|
452
450
|
export async function discoverSessions(options = {}) {
|
|
453
451
|
const home = options.home || os.homedir();
|
|
454
452
|
const max = options.max || 500;
|
|
455
|
-
const execute =
|
|
456
|
-
options.execute ||
|
|
457
|
-
(process.platform === 'win32' ? spawnWindowsSync : spawnSync);
|
|
453
|
+
const execute = options.execute || runCommand;
|
|
458
454
|
const roots = options.roots || {
|
|
459
455
|
claude: path.join(home, '.claude', 'projects'),
|
|
460
456
|
codex: path.join(home, '.codex', 'sessions'),
|
|
@@ -476,7 +472,10 @@ export async function discoverSessions(options = {}) {
|
|
|
476
472
|
const groups = await Promise.all([
|
|
477
473
|
claude || discoverFileSessions(roots.claude, 'claude', max),
|
|
478
474
|
codex || discoverFileSessions(roots.codex, 'codex', max),
|
|
479
|
-
|
|
475
|
+
discoverOpenCodeSessions(execute, max, {
|
|
476
|
+
timeout: options.commandTimeoutMs,
|
|
477
|
+
signal: options.signal,
|
|
478
|
+
}),
|
|
480
479
|
discoverFileSessions(roots.pi, 'pi', max),
|
|
481
480
|
]);
|
|
482
481
|
const seen = new Set();
|
|
@@ -500,7 +499,7 @@ async function openCodeMessages(session, execute) {
|
|
|
500
499
|
"and json_extract(part.data, '$.type') = 'text'",
|
|
501
500
|
'order by message.time_created asc, part.time_created asc',
|
|
502
501
|
].join(' ');
|
|
503
|
-
const output = commandResult(execute, 'opencode', [
|
|
502
|
+
const output = await commandResult(execute, 'opencode', [
|
|
504
503
|
'db',
|
|
505
504
|
'--format',
|
|
506
505
|
'json',
|
|
@@ -521,11 +520,7 @@ async function openCodeMessages(session, execute) {
|
|
|
521
520
|
|
|
522
521
|
export async function readSessionMessages(session, options = {}) {
|
|
523
522
|
if (session.harness === 'opencode') {
|
|
524
|
-
return openCodeMessages(
|
|
525
|
-
session,
|
|
526
|
-
options.execute ||
|
|
527
|
-
(process.platform === 'win32' ? spawnWindowsSync : spawnSync),
|
|
528
|
-
);
|
|
523
|
+
return openCodeMessages(session, options.execute || runCommand);
|
|
529
524
|
}
|
|
530
525
|
if (!session.sourcePath) return [];
|
|
531
526
|
const records = parseLines(await readBounded(session.sourcePath));
|
package/bin/tui.js
CHANGED
|
@@ -411,6 +411,7 @@ function spawnAndWait(command, args, options = {}) {
|
|
|
411
411
|
stdio: options.stdio ?? 'inherit',
|
|
412
412
|
});
|
|
413
413
|
child.once('error', reject);
|
|
414
|
+
options.onSpawn?.();
|
|
414
415
|
child.once('exit', (code, signal) => {
|
|
415
416
|
if (signal) {
|
|
416
417
|
reject(new Error(`Subconscious TUI exited with signal ${signal}`));
|
|
@@ -458,13 +459,17 @@ async function runTuiOnce(options) {
|
|
|
458
459
|
}
|
|
459
460
|
};
|
|
460
461
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
462
|
+
// Remote work starts only once the TUI process exists, so nothing it does
|
|
463
|
+
// (a slow gateway, a stuck session probe) can delay the first frame.
|
|
464
|
+
let remote = Promise.resolve();
|
|
465
|
+
const startRemote = () => {
|
|
466
|
+
if (providedState) return;
|
|
467
|
+
remote = loadRemoteTuiUpdates(state, {
|
|
468
|
+
...options,
|
|
469
|
+
signal: controller.signal,
|
|
470
|
+
writePatch,
|
|
471
|
+
});
|
|
472
|
+
};
|
|
468
473
|
|
|
469
474
|
try {
|
|
470
475
|
// This state intentionally contains only display data. API keys are used
|
|
@@ -483,7 +488,12 @@ async function runTuiOnce(options) {
|
|
|
483
488
|
'--updates',
|
|
484
489
|
updatesPath,
|
|
485
490
|
],
|
|
486
|
-
{
|
|
491
|
+
{
|
|
492
|
+
cwd: executable.cwd,
|
|
493
|
+
spawn: options.spawn,
|
|
494
|
+
stdio: options.stdio,
|
|
495
|
+
onSpawn: startRemote,
|
|
496
|
+
},
|
|
487
497
|
);
|
|
488
498
|
controller.abort();
|
|
489
499
|
await remote.catch(() => {});
|
package/bin/windows/process.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Windows process handling. Unix callers keep using their existing launch paths.
|
|
2
2
|
|
|
3
|
-
import { execFileSync, spawn
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -189,23 +189,6 @@ export function spawnWindows(command, args, options = {}) {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
export function spawnWindowsSync(command, args, options = {}) {
|
|
193
|
-
try {
|
|
194
|
-
const invocation = windowsInvocation(
|
|
195
|
-
command,
|
|
196
|
-
args,
|
|
197
|
-
options.env || process.env,
|
|
198
|
-
);
|
|
199
|
-
return spawnSync(invocation.command, invocation.args, {
|
|
200
|
-
...options,
|
|
201
|
-
env: invocation.env,
|
|
202
|
-
shell: false,
|
|
203
|
-
});
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return { error, status: null, stdout: '', stderr: '' };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
192
|
export function runWindows(command, args, options = {}) {
|
|
210
193
|
const { input, ...spawnOptions } = options;
|
|
211
194
|
return new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subconscious-cli",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"description": "CLI for Subconscious — run Subconscious Code, Claude Code, Codex, OpenCode, DeepSeek Harness, Cursor, Copilot, and Pi",
|
|
5
5
|
"bin": {
|
|
6
6
|
"subc": "bin/cli.js"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"build:tui": "node scripts/build-tui.js",
|
|
10
10
|
"build:tui:host": "node scripts/build-tui.js --host",
|
|
11
11
|
"test": "node --test",
|
|
12
|
-
"test:windows": "node --test test/windows.test.js test/models.test.js test/model-capabilities.test.js test/auth.test.js test/tui.test.js test/update-check.test.js test/sessions.test.js test/branding.test.js test/claude-version.test.js test/usage.test.js test/feedback.test.js test/release.test.js test/github-govuln-issue.test.js test/harness-manifest.test.js test/agent-files.test.js test/agent-launch.test.js test/agent-install-policy.test.js",
|
|
12
|
+
"test:windows": "node --test test/windows.test.js test/models.test.js test/model-capabilities.test.js test/auth.test.js test/tui.test.js test/update-check.test.js test/sessions.test.js test/command-output.test.js test/branding.test.js test/claude-version.test.js test/usage.test.js test/feedback.test.js test/release.test.js test/github-govuln-issue.test.js test/harness-manifest.test.js test/agent-files.test.js test/agent-launch.test.js test/agent-install-policy.test.js",
|
|
13
13
|
"test:tui": "go -C tui test ./...",
|
|
14
14
|
"lint": "biome ci .",
|
|
15
15
|
"format": "biome check --write .",
|