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.
@@ -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
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 { spawnWindowsSync } from './windows/process.js';
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
- encoding: 'utf8',
405
- timeout: options.timeout ?? 5_000,
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(execute, 'opencode', [
432
- 'db',
433
- '--format',
434
- 'json',
435
- query,
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
- Promise.resolve(discoverOpenCodeSessions(execute, max)),
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
- const remote = providedState
462
- ? Promise.resolve()
463
- : loadRemoteTuiUpdates(state, {
464
- ...options,
465
- signal: controller.signal,
466
- writePatch,
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
- { cwd: executable.cwd, spawn: options.spawn, stdio: options.stdio },
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(() => {});
@@ -1,6 +1,6 @@
1
1
  // Windows process handling. Unix callers keep using their existing launch paths.
2
2
 
3
- import { execFileSync, spawn, spawnSync } from 'node:child_process';
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.0",
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 .",