subconscious-cli 0.2.0 → 0.3.0
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/README.md +237 -54
- package/bin/agents.js +634 -114
- package/bin/auth.js +94 -20
- package/bin/branding.js +39 -0
- package/bin/cli.js +187 -17
- package/bin/colors.js +22 -10
- package/bin/profiles.js +852 -0
- package/bin/registry.generated.json +268 -0
- package/bin/runbook/README.md +25 -0
- package/bin/runbook/claude-code/install.sh +316 -0
- package/bin/runbook/claude-code/run.sh +164 -0
- package/bin/runbook/codex/hook.sh +112 -0
- package/bin/runbook/codex/hooks.json +29 -0
- package/bin/runbook/codex/install.sh +524 -0
- package/bin/runbook/codex/run.sh +264 -0
- package/bin/runbook/copilot/hook.sh +173 -0
- package/bin/runbook/copilot/hooks.json +19 -0
- package/bin/runbook/copilot/install.sh +469 -0
- package/bin/runbook/cursor/hook.sh +145 -0
- package/bin/runbook/cursor/hooks.json +17 -0
- package/bin/runbook/cursor/install.sh +259 -0
- package/bin/runbook/opencode/install.sh +298 -0
- package/bin/runbook/opencode/run.sh +107 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +100 -0
- package/bin/runbook/pi/install.sh +282 -0
- package/bin/runbook/pi/run.sh +25 -0
- package/bin/runbook/pi/subconscious-compaction.ts +151 -0
- package/package.json +10 -5
package/bin/agents.js
CHANGED
|
@@ -1,105 +1,96 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Coding-agent launcher.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `subc <agent>` resolves your saved API key and dispatches the vendored
|
|
5
|
+
* ol-runbook integration. Terminal agents launch ephemerally; IDE/config-based
|
|
6
|
+
* agents run their setup scripts.
|
|
7
|
+
*
|
|
8
|
+
* There is NO hardcoded agent data here: everything is read from
|
|
9
|
+
* `registry.generated.json` (shipped under `bin/`), which is generated from the
|
|
10
|
+
* single source of truth `agents/registry.json`. Run `pnpm generate` to update.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
13
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
12
15
|
import fs from 'node:fs/promises';
|
|
13
16
|
import { constants as fsConstants } from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
14
18
|
import path from 'node:path';
|
|
19
|
+
import readline from 'node:readline';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
15
21
|
import { c } from './colors.js';
|
|
16
22
|
import { getApiKey } from './auth.js';
|
|
23
|
+
import { profileSettingsForAgent, resolvedProfileValues } from './profiles.js';
|
|
24
|
+
|
|
25
|
+
// --- Registry (single source of truth, generated copy shipped in the package).
|
|
26
|
+
const registry = JSON.parse(
|
|
27
|
+
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
28
|
+
);
|
|
29
|
+
const DEFAULTS = registry.defaults;
|
|
30
|
+
const SUPPORTED_MODELS =
|
|
31
|
+
Array.isArray(DEFAULTS.models) && DEFAULTS.models.length
|
|
32
|
+
? DEFAULTS.models
|
|
33
|
+
: [DEFAULTS.model];
|
|
34
|
+
const RUNBOOK_DIR = fileURLToPath(new URL('./runbook/', import.meta.url));
|
|
35
|
+
|
|
36
|
+
// --- Token substitution — same rules as scripts/lib/registry.js.
|
|
37
|
+
// Replaces {apiKey}, {model}, {baseUrl}, {baseUrlV1}. NEVER touches {env:...}.
|
|
38
|
+
const TOKENS = ['apiKey', 'model', 'baseUrl', 'baseUrlV1'];
|
|
17
39
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
function substituteString(str, ctx) {
|
|
41
|
+
let out = str;
|
|
42
|
+
for (const token of TOKENS) {
|
|
43
|
+
if (ctx[token] === undefined) continue;
|
|
44
|
+
out = out.split(`{${token}}`).join(ctx[token]);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function substitute(value, ctx) {
|
|
50
|
+
if (typeof value === 'string') return substituteString(value, ctx);
|
|
51
|
+
if (Array.isArray(value)) return value.map((v) => substitute(v, ctx));
|
|
52
|
+
if (value && typeof value === 'object') {
|
|
53
|
+
const keys = Object.keys(value);
|
|
54
|
+
if (keys.length === 1 && keys[0] === '$json') {
|
|
55
|
+
return JSON.stringify(substitute(value.$json, ctx));
|
|
56
|
+
}
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const key of keys) out[substituteString(key, ctx)] = substitute(value[key], ctx);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
23
63
|
|
|
24
64
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* bin — executable we exec and probe on PATH
|
|
30
|
-
* env — (key, model) => extra env vars merged over process.env
|
|
31
|
-
* args — (model) => array of args passed to `bin`
|
|
65
|
+
* Resolve the install command for the current OS from a per-OS install object.
|
|
66
|
+
* Falls back to the linux command, then any string value present, if the exact
|
|
67
|
+
* `process.platform` key is missing. Tolerates a legacy plain-string `install`.
|
|
68
|
+
* Returns `{ command, fallback }` where `fallback` may be undefined.
|
|
32
69
|
*/
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
install
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ANTHROPIC_MODEL: model,
|
|
43
|
-
ANTHROPIC_SMALL_FAST_MODEL: model,
|
|
44
|
-
// Let Subconscious manage context instead of client-side compaction.
|
|
45
|
-
DISABLE_AUTO_COMPACT: 'true',
|
|
46
|
-
}),
|
|
47
|
-
args: () => [],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: 'OpenCode',
|
|
51
|
-
aliases: ['open-code', 'opencode'],
|
|
52
|
-
install: 'npm i -g opencode-ai',
|
|
53
|
-
bin: 'opencode',
|
|
54
|
-
env: (key, model) => ({
|
|
55
|
-
SUBCONSCIOUS_API_KEY: key,
|
|
56
|
-
// OpenCode deep-merges this at startup; nothing touches ~/.config/opencode.
|
|
57
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
58
|
-
$schema: 'https://opencode.ai/config.json',
|
|
59
|
-
provider: {
|
|
60
|
-
subconscious: {
|
|
61
|
-
npm: '@ai-sdk/openai-compatible',
|
|
62
|
-
name: 'Subconscious',
|
|
63
|
-
options: { baseURL: API_BASE_V1, apiKey: '{env:SUBCONSCIOUS_API_KEY}' },
|
|
64
|
-
models: { [model]: { name: 'Subconscious', tools: true } },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
model: `subconscious/${model}`,
|
|
68
|
-
}),
|
|
69
|
-
}),
|
|
70
|
-
args: () => [],
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'Aider',
|
|
74
|
-
aliases: ['aider'],
|
|
75
|
-
install: 'python -m pip install aider-install && aider-install',
|
|
76
|
-
bin: 'aider',
|
|
77
|
-
env: (key) => ({
|
|
78
|
-
OPENAI_API_BASE: API_BASE_V1,
|
|
79
|
-
OPENAI_API_KEY: key,
|
|
80
|
-
}),
|
|
81
|
-
args: (model) => ['--model', `openai/${model}`],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'Codex CLI',
|
|
85
|
-
aliases: ['codex'],
|
|
86
|
-
install: 'npm i -g @openai/codex',
|
|
87
|
-
bin: 'codex',
|
|
88
|
-
env: (key) => ({ SUBCONSCIOUS_API_KEY: key }),
|
|
89
|
-
// `-c model=…` (not `--model`, which is codex's Ollama shortcut).
|
|
90
|
-
args: (model) => [
|
|
91
|
-
'-c', 'model_providers.subconscious.name=Subconscious',
|
|
92
|
-
'-c', `model_providers.subconscious.base_url=${API_BASE_V1}`,
|
|
93
|
-
'-c', 'model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY',
|
|
94
|
-
'-c', 'model_provider=subconscious',
|
|
95
|
-
'-c', `model=${model}`,
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
];
|
|
70
|
+
function resolveInstall(install) {
|
|
71
|
+
if (typeof install === 'string') return { command: install, fallback: undefined };
|
|
72
|
+
if (!install || typeof install !== 'object') return { command: undefined, fallback: undefined };
|
|
73
|
+
const command =
|
|
74
|
+
install[process.platform] ||
|
|
75
|
+
install.linux ||
|
|
76
|
+
Object.values(install).find((v) => typeof v === 'string');
|
|
77
|
+
return { command, fallback: install.fallback };
|
|
78
|
+
}
|
|
99
79
|
|
|
80
|
+
// --- Build the in-memory registry + alias index.
|
|
81
|
+
// Each agent gets a resolved per-OS `install` (string) plus optional
|
|
82
|
+
// `installFallback`, while keeping the original per-OS object available.
|
|
83
|
+
const AGENTS = registry.agents
|
|
84
|
+
.filter((agent) => agent.cli !== false)
|
|
85
|
+
.map((agent) => {
|
|
86
|
+
const { command, fallback } = resolveInstall(agent.install);
|
|
87
|
+
return { ...agent, install: command, installFallback: fallback };
|
|
88
|
+
});
|
|
100
89
|
const BY_ALIAS = new Map();
|
|
101
90
|
for (const agent of AGENTS) {
|
|
102
|
-
|
|
91
|
+
BY_ALIAS.set(agent.id, agent);
|
|
92
|
+
if (agent.command) BY_ALIAS.set(agent.command, agent);
|
|
93
|
+
for (const alias of agent.aliases || []) BY_ALIAS.set(alias, agent);
|
|
103
94
|
}
|
|
104
95
|
|
|
105
96
|
export function resolveAgent(name) {
|
|
@@ -107,16 +98,194 @@ export function resolveAgent(name) {
|
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
export function agentList() {
|
|
110
|
-
return AGENTS.map((a) => ({
|
|
101
|
+
return AGENTS.map((a) => ({
|
|
102
|
+
name: a.name,
|
|
103
|
+
alias: a.command || a.id,
|
|
104
|
+
action: a.runbook?.mode === 'setup' ? 'Configure' : 'Launch',
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setupAgentList() {
|
|
109
|
+
return AGENTS.filter((agent) => agent.runbook?.setupScript);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SETUP_ACTIONS = new Set(['install', 'status', 'uninstall']);
|
|
113
|
+
const SETUP_HELPER_ACTIONS = new Set(['use', 'env', 'unset']);
|
|
114
|
+
const SETUP_HELPER_AGENT_IDS = new Set(['claude-code', 'codex']);
|
|
115
|
+
|
|
116
|
+
export function parseSetupRequest(argv = []) {
|
|
117
|
+
const [first, ...rest] = argv;
|
|
118
|
+
if (!first || SETUP_ACTIONS.has(first)) {
|
|
119
|
+
if (rest.length) {
|
|
120
|
+
throw new Error('Agent-specific setup options require a target agent');
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
agents: setupAgentList(),
|
|
124
|
+
args: first ? [first] : [],
|
|
125
|
+
action: first || 'install',
|
|
126
|
+
targeted: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const agent = resolveAgent(first);
|
|
131
|
+
if (!agent) throw new Error(`Unknown coding agent or setup action: ${first}`);
|
|
132
|
+
if (!agent.runbook?.setupScript) {
|
|
133
|
+
throw new Error(`No persistent setup integration is available for ${agent.name}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const requestedAction = rest[0];
|
|
137
|
+
const helperAction = SETUP_HELPER_ACTIONS.has(requestedAction);
|
|
138
|
+
if (helperAction && !SETUP_HELPER_AGENT_IDS.has(agent.id)) {
|
|
139
|
+
throw new Error(`${agent.name} does not support the '${requestedAction}' setup action`);
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
requestedAction &&
|
|
143
|
+
!requestedAction.startsWith('-') &&
|
|
144
|
+
!SETUP_ACTIONS.has(requestedAction) &&
|
|
145
|
+
!helperAction
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(`Unknown setup action for ${agent.name}: ${requestedAction}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
agents: [agent],
|
|
152
|
+
args: rest,
|
|
153
|
+
action:
|
|
154
|
+
SETUP_ACTIONS.has(requestedAction) || helperAction ? requestedAction : 'install',
|
|
155
|
+
targeted: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const AGENT_HELP = {
|
|
160
|
+
'claude-code': {
|
|
161
|
+
usage: 'subc [--profile NAME] claude [integration options] [Claude arguments...]',
|
|
162
|
+
behavior: 'Launches Claude Code with the active Subconscious profile and model picker.',
|
|
163
|
+
options: [
|
|
164
|
+
['--model MODEL', 'Override the profile model for this launch'],
|
|
165
|
+
['--compact-window N', 'Override the Claude auto-compact window'],
|
|
166
|
+
['--max-context-tokens N', 'Override the maximum context tokens'],
|
|
167
|
+
['-- ARGS...', 'Pass remaining arguments to Claude Code'],
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
codex: {
|
|
171
|
+
usage: 'subc [--profile NAME] codex [integration options] [Codex arguments...]',
|
|
172
|
+
behavior: 'Launches Codex with a temporary Subconscious provider catalog and compaction hooks.',
|
|
173
|
+
options: [
|
|
174
|
+
['--model MODEL', 'Override the profile model for this launch'],
|
|
175
|
+
['--context-window N', 'Override catalog context_window'],
|
|
176
|
+
['--max-context-window N', 'Override catalog max_context_window'],
|
|
177
|
+
['--auto-compact-token-limit N', 'Override the automatic compaction threshold'],
|
|
178
|
+
['--reasoning-effort LEVEL', 'Use none, low, medium, high, or max'],
|
|
179
|
+
['--external-tools', 'Enable Codex apps/plugins for this launch'],
|
|
180
|
+
['--subagents', 'Use the pinned legacy Codex subagent mode'],
|
|
181
|
+
['-- ARGS...', 'Pass remaining arguments to Codex'],
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
opencode: {
|
|
185
|
+
usage: 'subc [--profile NAME] opencode [OpenCode arguments...]',
|
|
186
|
+
behavior: 'Launches OpenCode with an ephemeral provider containing every Subconscious model.',
|
|
187
|
+
options: [
|
|
188
|
+
['--model MODEL', 'Override the profile model for this launch'],
|
|
189
|
+
['ARGS...', 'Pass arguments directly to OpenCode'],
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
cursor: {
|
|
193
|
+
usage: 'subc [--profile NAME] cursor [install|status|uninstall]',
|
|
194
|
+
behavior: 'Manages Cursor correlation hooks; model endpoint setup is completed in Cursor Settings.',
|
|
195
|
+
options: [
|
|
196
|
+
['install', 'Install or update the Cursor hooks (default action)'],
|
|
197
|
+
['status', 'Inspect the installed hook configuration'],
|
|
198
|
+
['uninstall', 'Remove only the Subconscious Cursor hooks'],
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
copilot: {
|
|
202
|
+
usage: 'subc [--profile NAME] copilot [install|status|uninstall]',
|
|
203
|
+
behavior: 'Manages the VS Code model provider and Copilot correlation hooks.',
|
|
204
|
+
options: [
|
|
205
|
+
['install', 'Install or update the provider and hooks (default action)'],
|
|
206
|
+
['status', 'Inspect the installed provider and hooks'],
|
|
207
|
+
['uninstall', 'Remove the Subconscious provider and hooks'],
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
pi: {
|
|
211
|
+
usage: 'subc [--profile NAME] pi [Pi arguments...]',
|
|
212
|
+
behavior: 'Launches Pi read-only against the provider previously configured by subc setup.',
|
|
213
|
+
options: [
|
|
214
|
+
['--model MODEL', 'Override the profile model for this launch'],
|
|
215
|
+
['ARGS...', 'Pass arguments directly to Pi'],
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export function isAgentHelpRequest(argv = []) {
|
|
221
|
+
return ['help', '-h', '--help'].includes(argv[0]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function displayProfileValue(setting, value, values) {
|
|
225
|
+
if (setting.type === 'secret') {
|
|
226
|
+
if (value) return '(set)';
|
|
227
|
+
return setting.key !== 'API_KEY' && values.API_KEY ? '(shared key)' : '(not set)';
|
|
228
|
+
}
|
|
229
|
+
return value || '(auto)';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function printAgentHelp(agent, profile) {
|
|
233
|
+
const details = AGENT_HELP[agent.id] || {
|
|
234
|
+
usage: `subc [--profile NAME] ${agent.command || agent.id} [arguments...]`,
|
|
235
|
+
behavior: agent.description,
|
|
236
|
+
options: [],
|
|
237
|
+
};
|
|
238
|
+
const settings = profileSettingsForAgent(agent.id);
|
|
239
|
+
const values = resolvedProfileValues(profile);
|
|
240
|
+
const optionWidth = Math.max(0, ...details.options.map(([option]) => option.length));
|
|
241
|
+
const settingWidth = Math.max(0, ...settings.map((setting) => setting.key.length));
|
|
242
|
+
|
|
243
|
+
console.log(`\n ${c.bold}${agent.name} + Subconscious${c.reset}\n`);
|
|
244
|
+
console.log(` ${details.behavior}\n`);
|
|
245
|
+
console.log(` ${c.bold}Usage${c.reset}\n ${details.usage}\n`);
|
|
246
|
+
if (details.options.length) {
|
|
247
|
+
console.log(` ${c.bold}Commands and options${c.reset}`);
|
|
248
|
+
for (const [option, description] of details.options) {
|
|
249
|
+
console.log(` ${c.cyan}${option.padEnd(optionWidth)}${c.reset} ${description}`);
|
|
250
|
+
}
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
console.log(` ${c.bold}Profile settings${c.reset} ${c.dim}(${profile?.name || 'default'})${c.reset}`);
|
|
254
|
+
for (const setting of settings) {
|
|
255
|
+
const value = displayProfileValue(setting, values[setting.key], values);
|
|
256
|
+
console.log(` ${c.cyan}${setting.key.padEnd(settingWidth)}${c.reset} ${value}`);
|
|
257
|
+
console.log(` ${' '.repeat(settingWidth)} ${c.dim}${setting.description}${c.reset}`);
|
|
258
|
+
}
|
|
259
|
+
console.log(`\n Edit these interactively with ${c.cyan}subc --profile ${profile?.name || 'default'} settings${c.reset}.`);
|
|
260
|
+
if (agent.runbook?.setupScript) {
|
|
261
|
+
console.log(` Apply persistent integration setup with ${c.cyan}subc setup ${agent.command || agent.id}${c.reset}.`);
|
|
262
|
+
}
|
|
263
|
+
console.log();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve the substitution context for a launch:
|
|
268
|
+
* model — --model flag → SUBCONSCIOUS_MODEL → registry default
|
|
269
|
+
* baseUrl — SUBCONSCIOUS_BASE_URL → registry default
|
|
270
|
+
* baseUrlV1 — `${baseUrl}/v1` (so an override flows to both)
|
|
271
|
+
*/
|
|
272
|
+
function buildContext(apiKey, model, profile) {
|
|
273
|
+
const baseUrl = (
|
|
274
|
+
process.env.SUBCONSCIOUS_BASE_URL?.trim() ||
|
|
275
|
+
profile?.values?.GATEWAY_URL?.trim() ||
|
|
276
|
+
DEFAULTS.baseUrl
|
|
277
|
+
).replace(/\/+$/, '');
|
|
278
|
+
return { apiKey, model, baseUrl, baseUrlV1: `${baseUrl}/v1` };
|
|
111
279
|
}
|
|
112
280
|
|
|
113
281
|
/**
|
|
114
282
|
* Pull a `--model <value>` / `--model=<value>` flag out of the passthrough
|
|
115
283
|
* args (so it sets the Subconscious model rather than reaching the agent).
|
|
116
|
-
* Falls back to SUBCONSCIOUS_MODEL, then the default.
|
|
284
|
+
* Falls back to SUBCONSCIOUS_MODEL, then the registry default.
|
|
117
285
|
*/
|
|
118
|
-
function extractModel(argv) {
|
|
119
|
-
let model =
|
|
286
|
+
function extractModel(argv, profile) {
|
|
287
|
+
let model =
|
|
288
|
+
process.env.SUBCONSCIOUS_MODEL?.trim() || profile?.values?.MODEL?.trim() || DEFAULTS.model;
|
|
120
289
|
const rest = [];
|
|
121
290
|
for (let i = 0; i < argv.length; i++) {
|
|
122
291
|
const a = argv[i];
|
|
@@ -137,67 +306,418 @@ function extractModel(argv) {
|
|
|
137
306
|
return { model, rest };
|
|
138
307
|
}
|
|
139
308
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Common locations a freshly-installed coding-agent binary lands in but which
|
|
311
|
+
* are often NOT on the current process's PATH (e.g. aider/claude install into
|
|
312
|
+
* `~/.local/bin`; npm globals into the npm prefix bin). Best-effort, deduped.
|
|
313
|
+
*/
|
|
314
|
+
function candidateBinDirs() {
|
|
315
|
+
const home = os.homedir();
|
|
316
|
+
const dirs = [];
|
|
317
|
+
|
|
318
|
+
if (process.platform === 'win32') {
|
|
319
|
+
if (process.env.APPDATA) dirs.push(path.join(process.env.APPDATA, 'npm'));
|
|
320
|
+
if (process.env.USERPROFILE) {
|
|
321
|
+
dirs.push(path.join(process.env.USERPROFILE, '.local', 'bin'));
|
|
322
|
+
}
|
|
323
|
+
if (home) dirs.push(path.join(home, '.local', 'bin'));
|
|
324
|
+
} else {
|
|
325
|
+
dirs.push(path.join(home, '.local', 'bin'));
|
|
326
|
+
dirs.push('/opt/homebrew/bin');
|
|
327
|
+
dirs.push('/usr/local/bin');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// npm global bin (best-effort — npm may be absent).
|
|
331
|
+
try {
|
|
332
|
+
const prefix = execFileSync('npm', ['prefix', '-g'], {
|
|
333
|
+
encoding: 'utf-8',
|
|
334
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
335
|
+
}).trim();
|
|
336
|
+
if (prefix) {
|
|
337
|
+
dirs.push(process.platform === 'win32' ? prefix : path.join(prefix, 'bin'));
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// npm not available — skip.
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Dedupe, drop empties.
|
|
344
|
+
return [...new Set(dirs.filter(Boolean))];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Executable extensions to probe (Windows uses PATHEXT). */
|
|
348
|
+
function binExts() {
|
|
349
|
+
return process.platform === 'win32'
|
|
350
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
351
|
+
: [''];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve `bin` against PATH plus the candidate bin dirs. Returns the directory
|
|
356
|
+
* containing the executable if found, otherwise null. Searching the candidate
|
|
357
|
+
* dirs lets us find binaries installed this session that aren't on PATH yet.
|
|
358
|
+
*/
|
|
359
|
+
async function resolveBinPath(bin) {
|
|
360
|
+
const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
361
|
+
const dirs = [...pathDirs, ...candidateBinDirs()];
|
|
362
|
+
const exts = binExts();
|
|
147
363
|
for (const dir of dirs) {
|
|
148
364
|
for (const ext of exts) {
|
|
149
365
|
const candidate = path.join(dir, bin + ext);
|
|
150
366
|
try {
|
|
151
367
|
await fs.access(candidate, fsConstants.F_OK);
|
|
152
|
-
return
|
|
368
|
+
return dir;
|
|
153
369
|
} catch {
|
|
154
370
|
// keep scanning
|
|
155
371
|
}
|
|
156
372
|
}
|
|
157
373
|
}
|
|
158
|
-
return
|
|
374
|
+
return null;
|
|
159
375
|
}
|
|
160
376
|
|
|
161
377
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
378
|
+
* Build a PATH string with `extraDirs` prepended (deduped against PATH).
|
|
379
|
+
* Returns the augmented PATH value for use in a child env.
|
|
164
380
|
*/
|
|
165
|
-
|
|
166
|
-
const
|
|
381
|
+
function augmentPath(extraDirs) {
|
|
382
|
+
const current = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
383
|
+
const seen = new Set(current);
|
|
384
|
+
const prepend = extraDirs.filter((d) => d && !seen.has(d));
|
|
385
|
+
return [...prepend, ...current].join(path.delimiter);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Ask a yes/no question on the TTY. Empty answer counts as yes. */
|
|
389
|
+
function askYesNo(question) {
|
|
390
|
+
return new Promise((resolve) => {
|
|
391
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
392
|
+
rl.question(question, (answer) => {
|
|
393
|
+
rl.close();
|
|
394
|
+
const a = answer.trim().toLowerCase();
|
|
395
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
167
399
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
400
|
+
/** Run the agent's install command (may contain `&&`, so shell:true). */
|
|
401
|
+
function runInstaller(install) {
|
|
402
|
+
return new Promise((resolve) => {
|
|
403
|
+
const child = spawn(install, { shell: true, stdio: 'inherit' });
|
|
404
|
+
child.on('error', () => resolve(false));
|
|
405
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Print the resolved install command (plus any fallback) for an agent. */
|
|
410
|
+
function printInstallCommands(agent) {
|
|
411
|
+
console.error(` ${c.cyan}${agent.install}${c.reset}`);
|
|
412
|
+
if (agent.installFallback) {
|
|
413
|
+
console.error(` ${c.dim}or, as a fallback:${c.reset}`);
|
|
414
|
+
console.error(` ${c.cyan}${agent.installFallback}${c.reset}`);
|
|
415
|
+
}
|
|
416
|
+
console.error('');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Ensure the agent's binary is resolvable. If missing:
|
|
421
|
+
* - interactive TTY: offer to run the per-OS installer (with fallback), then
|
|
422
|
+
* re-resolve against PATH + candidate dirs.
|
|
423
|
+
* - non-interactive: print the resolved install command (+ fallback) and
|
|
424
|
+
* exit 127 without running anything.
|
|
425
|
+
*
|
|
426
|
+
* Returns the directory containing the bin (to prepend to the child's PATH) on
|
|
427
|
+
* success. May exit the process on failure or when manual action is needed.
|
|
428
|
+
*/
|
|
429
|
+
async function ensureInstalled(agent) {
|
|
430
|
+
const existing = await resolveBinPath(agent.bin);
|
|
431
|
+
if (existing) return existing;
|
|
432
|
+
|
|
433
|
+
// Agents without an installer are launch-only. Their setup integration may
|
|
434
|
+
// configure the provider, but `subc <agent>` must never install the binary.
|
|
435
|
+
if (!agent.install) {
|
|
171
436
|
console.error(
|
|
172
|
-
|
|
437
|
+
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
173
438
|
);
|
|
174
|
-
|
|
439
|
+
console.error(` Install ${agent.name} separately, then re-run ${c.cyan}subc ${agent.command || agent.id}${c.reset}.\n`);
|
|
440
|
+
process.exit(127);
|
|
175
441
|
}
|
|
176
442
|
|
|
177
|
-
|
|
443
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
444
|
+
|
|
445
|
+
if (!interactive) {
|
|
178
446
|
console.error(
|
|
179
447
|
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
180
448
|
);
|
|
181
449
|
console.error(` Install it with:\n`);
|
|
182
|
-
|
|
450
|
+
printInstallCommands(agent);
|
|
183
451
|
process.exit(127);
|
|
184
452
|
}
|
|
185
453
|
|
|
186
|
-
|
|
187
|
-
const
|
|
454
|
+
console.error(`\n ${c.bold}${agent.name}${c.reset} isn't installed.`);
|
|
455
|
+
const ok = await askYesNo(` Install it now? ${c.dim}[Y/n]${c.reset} `);
|
|
456
|
+
if (!ok) {
|
|
457
|
+
console.error(`\n No problem. Install it yourself with:\n`);
|
|
458
|
+
printInstallCommands(agent);
|
|
459
|
+
process.exit(127);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
console.error(`\n ${c.dim}Running ${c.reset}${c.cyan}${agent.install}${c.reset}\n`);
|
|
463
|
+
let installed = await runInstaller(agent.install);
|
|
464
|
+
|
|
465
|
+
// Primary failed and a fallback exists — try it once.
|
|
466
|
+
if (!installed && agent.installFallback) {
|
|
467
|
+
console.error(
|
|
468
|
+
`\n ${c.dim}That didn't work. Trying the fallback: ${c.reset}${c.cyan}${agent.installFallback}${c.reset}\n`,
|
|
469
|
+
);
|
|
470
|
+
installed = await runInstaller(agent.installFallback);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!installed) {
|
|
474
|
+
console.error(`\n ${c.red}Install failed.${c.reset} Try it manually:\n`);
|
|
475
|
+
printInstallCommands(agent);
|
|
476
|
+
process.exit(127);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// PATH hardening: the freshly-installed binary is often not on the current
|
|
480
|
+
// process's PATH. Re-resolve against PATH + candidate dirs.
|
|
481
|
+
const found = await resolveBinPath(agent.bin);
|
|
482
|
+
if (found) return found;
|
|
483
|
+
|
|
484
|
+
console.error(
|
|
485
|
+
`\n ${c.dim}Installed ${agent.name}, but it isn't on this shell's PATH yet. ` +
|
|
486
|
+
`Open a new terminal (or add a bin dir to PATH) and re-run \`subc ${agent.command || agent.id}\`.${c.reset}\n`,
|
|
487
|
+
);
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Resolve and validate a script inside the packaged ol-runbook snapshot. */
|
|
492
|
+
function runbookScriptPath(agent, relativeScript = agent.runbook.script) {
|
|
493
|
+
const script = path.resolve(RUNBOOK_DIR, relativeScript);
|
|
494
|
+
const relative = path.relative(RUNBOOK_DIR, script);
|
|
495
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
496
|
+
throw new Error(`Invalid runbook script path for ${agent.name}`);
|
|
497
|
+
}
|
|
498
|
+
return script;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Spawn a runbook script and mirror its exit status/signals. */
|
|
502
|
+
function spawnRunbook(agent, args, env, relativeScript) {
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
const script = runbookScriptPath(agent, relativeScript);
|
|
505
|
+
const child = spawn('bash', [script, ...args], { stdio: 'inherit', env });
|
|
506
|
+
|
|
507
|
+
child.on('error', (error) => {
|
|
508
|
+
if (error.code === 'ENOENT') {
|
|
509
|
+
reject(
|
|
510
|
+
new Error('The ol-runbook integrations require `bash`, but it was not found on PATH.'),
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
reject(error);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
child.on('exit', (code, signal) => {
|
|
518
|
+
if (signal) {
|
|
519
|
+
process.kill(process.pid, signal);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (code) process.exitCode = code;
|
|
523
|
+
resolve(code ?? 0);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function isSetupWithoutAuth(argv) {
|
|
529
|
+
return ['status', 'uninstall', 'use', 'env', 'unset', '-h', '--help'].includes(argv[0]);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function optionValue(argv, name) {
|
|
533
|
+
const index = argv.indexOf(name);
|
|
534
|
+
return index >= 0 ? argv[index + 1]?.trim() || null : null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function agentApiKeySetting(agent) {
|
|
538
|
+
return profileSettingsForAgent(agent.id).find(
|
|
539
|
+
(setting) => setting.key !== 'API_KEY' && setting.key.endsWith('_API_KEY'),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function getAgentApiKey(profile, agent) {
|
|
544
|
+
const specificSetting = agentApiKeySetting(agent);
|
|
545
|
+
const specificKey = specificSetting?.key;
|
|
546
|
+
const specificEnvKey = specificKey && process.env[specificKey]?.trim();
|
|
547
|
+
if (specificEnvKey) return { key: specificEnvKey, source: `${specificKey} env var` };
|
|
548
|
+
|
|
549
|
+
const sharedEnvKey = process.env.SUBCONSCIOUS_API_KEY?.trim();
|
|
550
|
+
if (sharedEnvKey) {
|
|
551
|
+
return { key: sharedEnvKey, source: 'SUBCONSCIOUS_API_KEY env var' };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const profileKey = specificKey && profile?.values?.[specificKey]?.trim();
|
|
555
|
+
if (profileKey) return { key: profileKey, source: profile.path };
|
|
556
|
+
|
|
557
|
+
return getApiKey(profile);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function requireApiKey(profile, agent) {
|
|
561
|
+
const auth = await getAgentApiKey(profile, agent);
|
|
562
|
+
if (auth) return auth.key;
|
|
563
|
+
const login =
|
|
564
|
+
profile?.name && profile.name !== 'default'
|
|
565
|
+
? `subc --profile ${profile.name} login`
|
|
566
|
+
: 'subc login';
|
|
567
|
+
|
|
568
|
+
console.error(`\n ${c.red}Not logged in.${c.reset}`);
|
|
569
|
+
console.error(
|
|
570
|
+
` Run ${c.cyan}${login}${c.reset} (or set ${c.dim}SUBCONSCIOUS_API_KEY${c.reset}) first.\n`,
|
|
571
|
+
);
|
|
572
|
+
process.exitCode = 1;
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const CLAUDE_MODEL_PICKER_KEYS = [
|
|
577
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
578
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
|
|
579
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
|
|
580
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
581
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
|
582
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
|
583
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
584
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
585
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
function claudeModelPickerEnv(agent, ctx) {
|
|
589
|
+
if (agent.id !== 'claude-code') return {};
|
|
590
|
+
const configured = substitute(agent.env || {}, ctx);
|
|
591
|
+
return Object.fromEntries(
|
|
592
|
+
CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, configured[key]]).filter(([, value]) => value),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function runbookEnv(apiKey, model, binDir, profile, agent) {
|
|
597
|
+
const ctx = buildContext(apiKey, model, profile);
|
|
598
|
+
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
599
|
+
const specificApiKey = agentApiKeySetting(agent)?.key;
|
|
600
|
+
return {
|
|
601
|
+
...(profile?.values || {}),
|
|
602
|
+
...process.env,
|
|
603
|
+
...claudeModelPickerEnv(agent, ctx),
|
|
604
|
+
GATEWAY_URL: ctx.baseUrl,
|
|
605
|
+
API_KEY: apiKey,
|
|
606
|
+
...(specificApiKey ? { [specificApiKey]: apiKey } : {}),
|
|
607
|
+
MODEL: model,
|
|
608
|
+
SUBCONSCIOUS_MODELS: SUPPORTED_MODELS.join('\n'),
|
|
609
|
+
MBTA_ENV_FILE: os.devNull,
|
|
610
|
+
PATH: augmentPath(extraDirs),
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runbook.script) {
|
|
615
|
+
if (isSetupWithoutAuth(argv)) {
|
|
616
|
+
return spawnRunbook(agent, argv, {
|
|
617
|
+
...(profile?.values || {}),
|
|
618
|
+
...process.env,
|
|
619
|
+
MBTA_ENV_FILE: os.devNull,
|
|
620
|
+
}, relativeScript);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const { model, rest } = extractModel(argv, profile);
|
|
624
|
+
const apiKey = optionValue(rest, '--api-key') || (await requireApiKey(profile, agent));
|
|
625
|
+
if (!apiKey) return 1;
|
|
626
|
+
const ctx = buildContext(apiKey, model, profile);
|
|
627
|
+
const authArgs = substitute(agent.runbook.authArgs || [], ctx);
|
|
628
|
+
|
|
629
|
+
console.log(
|
|
630
|
+
` ${c.dim}Configuring ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}for Subconscious (${model})${c.reset}\n`,
|
|
631
|
+
);
|
|
632
|
+
const code = await spawnRunbook(
|
|
633
|
+
agent,
|
|
634
|
+
[...authArgs, ...rest],
|
|
635
|
+
runbookEnv(apiKey, model, undefined, profile, agent),
|
|
636
|
+
relativeScript,
|
|
637
|
+
);
|
|
638
|
+
const installed = !['status', 'uninstall'].includes(rest[0]);
|
|
639
|
+
if (code !== 0 || !installed) return code;
|
|
640
|
+
|
|
641
|
+
if (agent.id === 'cursor') {
|
|
642
|
+
const modelList = SUPPORTED_MODELS.map(
|
|
643
|
+
(supportedModel) => ` ${c.cyan}${supportedModel}${c.reset}`,
|
|
644
|
+
).join('\n');
|
|
645
|
+
console.log(
|
|
646
|
+
`\n ${c.bold}Finish in Cursor Settings${c.reset}\n` +
|
|
647
|
+
` Enable OpenAI API Key Override, then use:\n` +
|
|
648
|
+
` Base URL: ${c.cyan}${ctx.baseUrl}${c.reset}\n` +
|
|
649
|
+
` Add the available custom models:\n${modelList}\n` +
|
|
650
|
+
` Select ${c.cyan}${model}${c.reset} for this profile.\n` +
|
|
651
|
+
` Paste your Subconscious API key there, then fully restart Cursor.\n`,
|
|
652
|
+
);
|
|
653
|
+
} else if (agent.id === 'pi') {
|
|
654
|
+
console.log(`\n ${c.dim}Start a fresh session with ${c.reset}${c.cyan}subc pi${c.reset}${c.dim}.${c.reset}\n`);
|
|
655
|
+
}
|
|
656
|
+
return code;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Launch a coding agent against Subconscious. `argv` is everything after the
|
|
661
|
+
* agent name; unknown flags pass straight through to the underlying CLI.
|
|
662
|
+
*/
|
|
663
|
+
export async function runAgent(agent, argv, options = {}) {
|
|
664
|
+
const profile = options.profile;
|
|
665
|
+
if (options.setup) {
|
|
666
|
+
if (!agent.runbook?.setupScript) {
|
|
667
|
+
throw new Error(`No setup integration is available for ${agent.name}`);
|
|
668
|
+
}
|
|
669
|
+
return runRunbookSetup(agent, argv, profile, agent.runbook.setupScript);
|
|
670
|
+
}
|
|
671
|
+
if (isAgentHelpRequest(argv)) {
|
|
672
|
+
printAgentHelp(agent, profile);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
if (agent.runbook?.mode === 'setup') {
|
|
676
|
+
return runRunbookSetup(agent, argv, profile);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const { model, rest } = extractModel(argv, profile);
|
|
680
|
+
const apiKey = await requireApiKey(profile, agent);
|
|
681
|
+
if (!apiKey) return 1;
|
|
682
|
+
|
|
683
|
+
const binDir = await ensureInstalled(agent);
|
|
684
|
+
|
|
685
|
+
if (agent.runbook?.mode === 'launch') {
|
|
686
|
+
console.log(
|
|
687
|
+
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
688
|
+
);
|
|
689
|
+
return spawnRunbook(agent, rest, runbookEnv(apiKey, model, binDir, profile, agent));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const ctx = buildContext(apiKey, model, profile);
|
|
693
|
+
const launch = substituteString(agent.launch, ctx);
|
|
694
|
+
const [bin, ...launchArgs] = launch.split(' ').filter(Boolean);
|
|
695
|
+
const envMap = substitute(agent.env, ctx);
|
|
696
|
+
|
|
697
|
+
// Prepend the resolved bin dir + candidate dirs to the child's PATH so the
|
|
698
|
+
// agent (and any subprocess it spawns) resolves correctly this session, even
|
|
699
|
+
// if it was installed into a dir not yet on the parent shell's PATH.
|
|
700
|
+
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
701
|
+
const env = {
|
|
702
|
+
...(profile?.values || {}),
|
|
703
|
+
...process.env,
|
|
704
|
+
...envMap,
|
|
705
|
+
PATH: augmentPath(extraDirs),
|
|
706
|
+
};
|
|
707
|
+
const args = [...launchArgs, ...rest];
|
|
188
708
|
|
|
189
709
|
console.log(
|
|
190
710
|
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
191
711
|
);
|
|
192
712
|
|
|
193
|
-
const child = spawn(
|
|
713
|
+
const child = spawn(bin, args, { stdio: 'inherit', env });
|
|
194
714
|
|
|
195
715
|
child.on('error', (err) => {
|
|
196
716
|
if (err.code === 'ENOENT') {
|
|
197
717
|
console.error(
|
|
198
|
-
`\n ${c.red}Could not launch \`${
|
|
718
|
+
`\n ${c.red}Could not launch \`${bin}\`.${c.reset} Install it with:\n`,
|
|
199
719
|
);
|
|
200
|
-
|
|
720
|
+
printInstallCommands(agent);
|
|
201
721
|
process.exit(127);
|
|
202
722
|
}
|
|
203
723
|
console.error(`\n ${c.red}${err.message}${c.reset}\n`);
|