@voiden/runner 2.3.0-beta.14 → 2.3.0-beta.16

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,209 @@
1
+ /**
2
+ * Headless env-profile discovery/resolution for the `list_environments`/
3
+ * `select_environment` fixed MCP tools (mcpServing.ts). A deliberately
4
+ * simplified port of apps/electron/src/main/env.ts's profile logic — same
5
+ * precedent as plugins/voiden-mcp-tool/src/lib/toolCapability.ts's
6
+ * loadProjectEnvironmentVars, which already does this for its own narrower
7
+ * needs. No Electron dependency (this package is a standalone CLI/library),
8
+ * no UI "active profile" state, no nested sub-project scanning, no legacy
9
+ * dotted `.env.foo.bar` hierarchy chain — see mcpServing.ts's callers for
10
+ * what's actually needed here.
11
+ */
12
+ import { existsSync, readdirSync, readFileSync } from 'fs';
13
+ import { join, resolve, extname } from 'path';
14
+ import { parseYamlEnv, listYamlEnvironmentNames, mergeYamlEnvTrees, findEnvironmentByPath, loadEnvFile } from './envFile.js';
15
+ import YAML from 'yaml';
16
+ const VOIDEN_DIR = '.voiden';
17
+ const PROFILE_FILE_PATTERN = /^env-([a-z0-9-]+)-(public|private)\.yaml$/;
18
+ function profileFileNames(projectRoot, profile) {
19
+ const dir = join(projectRoot, VOIDEN_DIR);
20
+ if (profile === 'default') {
21
+ return { publicFile: join(dir, 'env-public.yaml'), privateFile: join(dir, 'env-private.yaml') };
22
+ }
23
+ return { publicFile: join(dir, `env-${profile}-public.yaml`), privateFile: join(dir, `env-${profile}-private.yaml`) };
24
+ }
25
+ /** Mirrors env.ts's discoverProfiles — scan .voiden/ for named profile
26
+ * files, "default" always included even with no files on disk yet. */
27
+ function discoverProfileNames(projectRoot) {
28
+ const names = new Set(['default']);
29
+ const dir = join(projectRoot, VOIDEN_DIR);
30
+ let entries;
31
+ try {
32
+ entries = readdirSync(dir);
33
+ }
34
+ catch {
35
+ return Array.from(names);
36
+ }
37
+ for (const entry of entries) {
38
+ const match = PROFILE_FILE_PATTERN.exec(entry);
39
+ if (match)
40
+ names.add(match[1]);
41
+ }
42
+ return Array.from(names);
43
+ }
44
+ function readIfExists(path) {
45
+ return existsSync(path) ? readFileSync(path, 'utf-8') : undefined;
46
+ }
47
+ /** project root + .voiden/, non-recursive — same scope as env.ts's
48
+ * loadProjectEnv fallback, minus the dotted-chain hierarchy machinery
49
+ * (out of scope here, see the plan's "Not doing" section). */
50
+ function discoverDotEnvFiles(projectRoot) {
51
+ const dirs = [projectRoot, join(projectRoot, VOIDEN_DIR)];
52
+ const found = [];
53
+ for (const dir of dirs) {
54
+ let entries;
55
+ try {
56
+ entries = readdirSync(dir);
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ for (const entry of entries) {
62
+ if (entry.startsWith('.env'))
63
+ found.push(join(dir, entry));
64
+ }
65
+ }
66
+ return found;
67
+ }
68
+ export function discoverEnvProfiles(projectRoot) {
69
+ return discoverProfileNames(projectRoot).map((name) => {
70
+ const { publicFile, privateFile } = profileFileNames(projectRoot, name);
71
+ const publicContent = readIfExists(publicFile);
72
+ const privateContent = readIfExists(privateFile);
73
+ if (publicContent || privateContent) {
74
+ const merged = mergeYamlEnvTrees(publicContent ? YAML.parse(publicContent) : {}, privateContent ? YAML.parse(privateContent) : {});
75
+ const environments = listYamlEnvironmentNames(YAML.stringify(merged));
76
+ if (environments.length > 0) {
77
+ return {
78
+ name,
79
+ source: 'yaml',
80
+ ...(publicContent ? { publicFile } : {}),
81
+ ...(privateContent ? { privateFile } : {}),
82
+ environments,
83
+ };
84
+ }
85
+ }
86
+ return { name, source: 'legacy-dotenv', dotEnvFiles: discoverDotEnvFiles(projectRoot) };
87
+ });
88
+ }
89
+ /** Resolves the final variable map for one profile (+ optional named
90
+ * environment within it). Throws if the profile doesn't exist or the
91
+ * named environment isn't found in it (same "available: ..." style error
92
+ * envFile.ts's own parseYamlEnv already throws for an unknown name). */
93
+ export function resolveEnvProfile(projectRoot, profile, environmentName) {
94
+ const profiles = discoverEnvProfiles(projectRoot);
95
+ const info = profiles.find((p) => p.name === profile);
96
+ if (!info) {
97
+ throw new Error(`Unknown profile "${profile}". Available: ${profiles.map((p) => p.name).join(', ')}`);
98
+ }
99
+ if (info.source === 'legacy-dotenv') {
100
+ const env = {};
101
+ for (const path of info.dotEnvFiles ?? []) {
102
+ try {
103
+ Object.assign(env, parseDotEnvLoose(readFileSync(path, 'utf-8')));
104
+ }
105
+ catch {
106
+ // A malformed .env* file shouldn't block resolving the others —
107
+ // select_environment's job is "best available", not strict parsing.
108
+ }
109
+ }
110
+ return env;
111
+ }
112
+ const { publicFile, privateFile } = profileFileNames(projectRoot, profile);
113
+ const publicContent = readIfExists(publicFile) ?? '';
114
+ const privateContent = readIfExists(privateFile) ?? '';
115
+ const merged = mergeYamlEnvTrees(publicContent ? YAML.parse(publicContent) : {}, privateContent ? YAML.parse(privateContent) : {});
116
+ if (environmentName) {
117
+ // Path-based, not envFile.ts's own bare-name findEnvironment — that one
118
+ // searches for a matching key ANYWHERE in the tree (fine for the CLI's
119
+ // simpler --environment <name> flag), but list_environments returns
120
+ // full dotted paths specifically to disambiguate same-named children
121
+ // under different parents (e.g. "staging.eu" vs "prod.eu") — only a
122
+ // full-path walk resolves the right one.
123
+ const resolved = findEnvironmentByPath(merged, environmentName);
124
+ if (!resolved) {
125
+ const available = listYamlEnvironmentNames(YAML.stringify(merged));
126
+ throw new Error(`Environment "${environmentName}" not found in profile "${profile}". Available: ${available.join(', ')}`);
127
+ }
128
+ return resolved;
129
+ }
130
+ return parseYamlEnv(YAML.stringify(merged));
131
+ }
132
+ /** Thrown by resolveCliEnv for a usage mistake (not found, ambiguous
133
+ * flags) — callers already wrap their existing --env loading in a
134
+ * try/catch that prints the message and exits with EXIT_USAGE_ERROR; this
135
+ * just gives that same catch something to catch for the --profile path
136
+ * too, instead of duplicating that error-reporting shape here. */
137
+ export class EnvCliOptsError extends Error {
138
+ }
139
+ /**
140
+ * Resolves the env-selection flags shared across voiden-runner's
141
+ * run/mcp serve/tool verify commands into a merged variable map, layered on
142
+ * top of whatever base env the caller already has (typically process.env).
143
+ *
144
+ * Two independent ways to point at variables — --profile and --env — are
145
+ * deliberately mutually exclusive: mixing "use the profile system" and
146
+ * "use this one specific file instead" has no obvious shared meaning, so
147
+ * this throws rather than silently picking one. Passing neither is
148
+ * unchanged from before this existed: nothing gets loaded, only baseEnv
149
+ * comes back — an existing invocation with no env flags at all keeps
150
+ * working exactly as it did.
151
+ */
152
+ export function resolveCliEnv(opts, projectRoot, baseEnv) {
153
+ if (opts.env && opts.profile) {
154
+ throw new EnvCliOptsError('--env and --profile are mutually exclusive — pick one way to point at variables.');
155
+ }
156
+ const env = { ...baseEnv };
157
+ if (opts.profile !== undefined) {
158
+ const profileName = opts.profile === true ? 'default' : opts.profile;
159
+ try {
160
+ Object.assign(env, resolveEnvProfile(projectRoot, profileName, opts.environment));
161
+ }
162
+ catch (err) {
163
+ throw new EnvCliOptsError(err?.message ?? String(err));
164
+ }
165
+ return env;
166
+ }
167
+ if (opts.env) {
168
+ const envPath = resolve(opts.env);
169
+ if (!existsSync(envPath)) {
170
+ throw new EnvCliOptsError(`Env file not found: ${envPath}`);
171
+ }
172
+ const ext = extname(envPath).toLowerCase();
173
+ if (ext === '.yaml' || ext === '.yml') {
174
+ throw new EnvCliOptsError('--env only accepts a plain .env file now. A YAML profile is always a pair of files ' +
175
+ '(env-<profile>-public.yaml + -private.yaml, merged) — a single --env path can\'t represent ' +
176
+ 'that correctly. Use --profile <name> instead (bare --profile means "default").');
177
+ }
178
+ if (opts.environment) {
179
+ throw new EnvCliOptsError('--environment only applies with --profile — a plain .env file has no named-environment concept to scope to.');
180
+ }
181
+ try {
182
+ Object.assign(env, loadEnvFile(envPath));
183
+ }
184
+ catch (err) {
185
+ throw new EnvCliOptsError(err?.message ?? String(err));
186
+ }
187
+ }
188
+ return env;
189
+ }
190
+ /** Same shape as envFile.ts's private parseDotEnv, duplicated rather than
191
+ * exported from there since that one throws on a malformed line (correct
192
+ * for an explicit --env <path>) where discovery here wants best-effort. */
193
+ function parseDotEnvLoose(content) {
194
+ const env = {};
195
+ for (const rawLine of content.split('\n')) {
196
+ const line = rawLine.trim();
197
+ if (!line || line.startsWith('#'))
198
+ continue;
199
+ const eq = line.indexOf('=');
200
+ if (eq === -1)
201
+ continue;
202
+ const key = line.slice(0, eq).trim();
203
+ if (!key)
204
+ continue;
205
+ env[key] = line.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
206
+ }
207
+ return env;
208
+ }
209
+ //# sourceMappingURL=envProfiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envProfiles.js","sourceRoot":"","sources":["../src/envProfiles.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import { checkForPluginUpdates } from './plugins/updateCheck.js';
23
23
  import { getInstalledPluginInfo } from './plugins/versionInfo.js';
24
24
  import { classifyBlockVersion, parseVoidFile, installMcpIntegration, uninstallMcpIntegration, getMcpStatus, MCP_SKILL_MARKDOWN, } from '@voiden/executors';
25
25
  import { loadEnvFile } from './envFile.js';
26
+ import { resolveCliEnv } from './envProfiles.js';
26
27
  import { appendSessionResults, loadSessionResults, clearSession, } from './session.js';
27
28
  // ─────────────────────────────────────────────────────────────────────────────
28
29
  // Exit codes — a stable, documented contract CI pipelines can branch on.
@@ -436,9 +437,10 @@ program
436
437
  ' voiden-runner run ./requests/\n' +
437
438
  ' voiden-runner run auth.void users.void ./smoke/\n' +
438
439
  ' voiden-runner run ./ --env .env.staging --bail\n' +
439
- ' voiden-runner run ./ --env .voiden/env-public.yaml --environment staging\n')
440
- .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
441
- .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
440
+ ' voiden-runner run ./ --profile staging --environment staging.eu\n')
441
+ .option('-e, --env <path>', 'Path to one plain .env file for variable substitution — for a file outside the project\'s profile convention (e.g. a CI-provided secrets path). Mutually exclusive with --profile.')
442
+ .option('--profile [name]', 'Use a project env profile (.voiden/env-<profile>-{public,private}.yaml, or that profile\'s legacy .env* fallback) — the same profile system the MCP select_environment tool exposes to an agent, now reachable from the command line too. Bare --profile (no name) means "default". Mutually exclusive with --env.')
443
+ .option('--environment <name>', 'Scope --env or --profile to one named environment within it (e.g. "dev", or a dotted child like "staging.eu") instead of merging every environment together')
442
444
  .option('--env-var <key=value>', 'Individual environment variable override (can be used multiple times)', (val, memo) => {
443
445
  memo.push(val);
444
446
  return memo;
@@ -470,20 +472,13 @@ program
470
472
  // and any CI/CD platform vars are automatically available as {{KEY}}
471
473
  // without needing an --env file.
472
474
  const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
473
- // 1. Load --env file (overrides system)
474
- if (opts.env) {
475
- const envPath = resolve(opts.env);
476
- if (!existsSync(envPath)) {
477
- console.error(chalk.red(`Env file not found: ${envPath}`));
478
- process.exit(EXIT_USAGE_ERROR);
479
- }
480
- try {
481
- Object.assign(env, loadEnvFile(envPath, opts.environment));
482
- }
483
- catch (err) {
484
- console.error(chalk.red(` ✗ ${err.message}`));
485
- process.exit(EXIT_USAGE_ERROR);
486
- }
475
+ // 1. Load --env file or --profile (overrides system)
476
+ try {
477
+ Object.assign(env, resolveCliEnv(opts, process.cwd(), env));
478
+ }
479
+ catch (err) {
480
+ console.error(chalk.red(` ✗ ${err.message}`));
481
+ process.exit(EXIT_USAGE_ERROR);
487
482
  }
488
483
  // 2. Individual --env-var overrides
489
484
  if (opts.envVar && Array.isArray(opts.envVar)) {
@@ -1307,12 +1302,24 @@ mcpCmd
1307
1302
  .option('--claude', 'Install for Claude Code only')
1308
1303
  .option('--codex', 'Install for Codex only')
1309
1304
  .option('-p, --project <path>', 'Project directory to register the MCP server against', '.')
1310
- .option('--local-server <path>', 'Use `node <path> mcp serve` instead of `npx -y @voiden/runner mcp serve` — for testing against a local build')
1305
+ .option('--local-server <path>', 'Use `node <path> mcp serve` instead of `npx -y @voiden/runner@<this version> mcp serve` — for testing against a local build')
1311
1306
  .action((opts) => {
1312
1307
  const targets = resolveMcpTargets(opts);
1308
+ // Pin to the exact version of this CLI the user is running `mcp install`
1309
+ // from — never a bare, unpinned `npx -y @voiden/runner`. Unpinned resolves
1310
+ // to whatever npm's "latest" dist-tag happens to point at, which can sit
1311
+ // far behind the actively-developed prerelease line this MCP tooling
1312
+ // actually lives on (e.g. "latest" stuck on a stable 2.2.0 cut from
1313
+ // before write_result/list_requests/the plugin registry existed, while
1314
+ // real fixes ship under the "beta" tag) — every agent session would
1315
+ // silently run that stale version forever, with no way to notice short of
1316
+ // diffing tool output against what this CLI's own docs say it does.
1317
+ // Pinning to what's actually installed right now is deterministic: it's
1318
+ // exactly what was tested when `mcp install` ran, and picks up newer
1319
+ // fixes the moment the user updates this CLI and re-runs `mcp install`.
1313
1320
  const serverCommand = opts.localServer
1314
1321
  ? { command: 'node', args: [resolve(opts.localServer), 'mcp', 'serve', resolve(opts.project)] }
1315
- : undefined;
1322
+ : { command: 'npx', args: ['-y', `@voiden/runner@${pkg.version}`, 'mcp', 'serve', resolve(opts.project)] };
1316
1323
  const installed = installMcpIntegration(opts.project, targets, MCP_SKILL_MARKDOWN, serverCommand);
1317
1324
  if (installed.length === 0) {
1318
1325
  console.log(chalk.yellow(' Nothing to install.'));
@@ -1322,9 +1329,12 @@ mcpCmd
1322
1329
  for (const target of installed) {
1323
1330
  console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, fixed-tools MCP server registered for ${resolve(opts.project)}`));
1324
1331
  }
1325
- if (serverCommand) {
1332
+ if (opts.localServer) {
1326
1333
  console.log(chalk.gray(` Using local build: node ${serverCommand.args[0]}`));
1327
1334
  }
1335
+ else {
1336
+ console.log(chalk.gray(` Pinned to this CLI's version: @voiden/runner@${pkg.version}`));
1337
+ }
1328
1338
  console.log();
1329
1339
  console.log(chalk.gray(' Restart Claude Code / Codex (or run /mcp) to pick up the new server.'));
1330
1340
  });
@@ -1373,25 +1383,19 @@ mcpCmd
1373
1383
  .option('--http', 'Serve over streamable HTTP instead of stdio')
1374
1384
  .option('-p, --port <port>', 'HTTP port (only with --http)', '3000')
1375
1385
  .option('--host <host>', 'HTTP bind address (only with --http) — binding beyond 127.0.0.1 is a real exposure risk', '127.0.0.1')
1376
- .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1377
- .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1386
+ .option('-e, --env <path>', 'Path to one plain .env file for variable substitution — for a file outside the project\'s profile convention. Mutually exclusive with --profile.')
1387
+ .option('--profile [name]', 'Use a project env profile (.voiden/env-<profile>-{public,private}.yaml, or its legacy .env* fallback) — same profile system the select_environment tool exposes to an agent, as the server\'s initial env before any select_environment call. Bare --profile means "default". Mutually exclusive with --env.')
1388
+ .option('--environment <name>', 'Scope --env or --profile to one named environment within it (e.g. "dev", or a dotted child like "staging.eu")')
1378
1389
  .option('--check', 'Print what would be served and exit, without starting a live server')
1379
1390
  .action(async (path, opts) => {
1380
1391
  const projectRoot = resolve(path ?? '.');
1381
1392
  const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1382
- if (opts.env) {
1383
- const envPath = resolve(opts.env);
1384
- if (!existsSync(envPath)) {
1385
- console.error(chalk.red(`Env file not found: ${envPath}`));
1386
- process.exit(EXIT_USAGE_ERROR);
1387
- }
1388
- try {
1389
- Object.assign(env, loadEnvFile(envPath, opts.environment));
1390
- }
1391
- catch (err) {
1392
- console.error(chalk.red(` ✗ ${err.message}`));
1393
- process.exit(EXIT_USAGE_ERROR);
1394
- }
1393
+ try {
1394
+ Object.assign(env, resolveCliEnv(opts, projectRoot, env));
1395
+ }
1396
+ catch (err) {
1397
+ console.error(chalk.red(` ✗ ${err.message}`));
1398
+ process.exit(EXIT_USAGE_ERROR);
1395
1399
  }
1396
1400
  if (opts.check) {
1397
1401
  // Dry run — no live server. Same underlying decision function real
@@ -1424,6 +1428,9 @@ mcpCmd
1424
1428
  // Shared across calls so {{process.xxx}} runtime variables chain the
1425
1429
  // same way they do for the stdio path and for @voiden/mcp.
1426
1430
  const runtimeVars = {};
1431
+ // Set by select_environment, read by run_request as its base env layer
1432
+ // — shared by reference across calls the same way runtimeVars is.
1433
+ const selectedEnv = { vars: {} };
1427
1434
  if (opts.http) {
1428
1435
  const port = Number(opts.port);
1429
1436
  const host = opts.host;
@@ -1435,7 +1442,7 @@ mcpCmd
1435
1442
  const httpServer = createHttpServer(async (req, res) => {
1436
1443
  try {
1437
1444
  const requestServer = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1438
- registerFixedTools(requestServer, projectRoot, runtimeVars, activePlugins);
1445
+ registerFixedTools(requestServer, projectRoot, runtimeVars, activePlugins, selectedEnv);
1439
1446
  registerToolsFromDecisions(requestServer, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1440
1447
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
1441
1448
  await requestServer.connect(transport);
@@ -1456,7 +1463,7 @@ mcpCmd
1456
1463
  });
1457
1464
  httpServer.listen(port, host, () => {
1458
1465
  console.error(chalk.green(` ✓ voiden-runner mcp serve — listening on http://${host}:${port}/mcp`));
1459
- console.error(chalk.gray(` ${servedCount} tool(s) served (plus list_void_files, list_requests, run_request, write_result)`));
1466
+ console.error(chalk.gray(` ${servedCount} tool(s) served (plus list_void_files, list_requests, run_request, write_result, list_environments, select_environment)`));
1460
1467
  if (host !== '127.0.0.1' && host !== 'localhost') {
1461
1468
  console.error(chalk.red(` ⚠ Bound to ${host} — reachable beyond this machine. Make sure that's intended.`));
1462
1469
  }
@@ -1469,7 +1476,7 @@ mcpCmd
1469
1476
  // Startup info goes to stderr only, same discipline @voiden/mcp's
1470
1477
  // own entrypoint already follows (it prints nothing).
1471
1478
  const server = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1472
- registerFixedTools(server, projectRoot, runtimeVars, activePlugins);
1479
+ registerFixedTools(server, projectRoot, runtimeVars, activePlugins, selectedEnv);
1473
1480
  registerToolsFromDecisions(server, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1474
1481
  await server.connect(new StdioServerTransport());
1475
1482
  }
@@ -1527,24 +1534,18 @@ toolCmd
1527
1534
  .option('--cadence <tag>', 'Only run verification requests tagged with this cadence — omit to run every entry regardless of tag')
1528
1535
  .option('--json', 'Output as JSON (suppresses normal output — useful for CI)')
1529
1536
  .option('--write', 'Write the computed status back into each /tool block. Off by default — verification always recomputes fresh and never trusts a stale write-back')
1530
- .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1531
- .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1537
+ .option('-e, --env <path>', 'Path to one plain .env file for variable substitution — for a file outside the project\'s profile convention. Mutually exclusive with --profile.')
1538
+ .option('--profile [name]', 'Use a project env profile (.voiden/env-<profile>-{public,private}.yaml, or its legacy .env* fallback). Bare --profile means "default". Mutually exclusive with --env.')
1539
+ .option('--environment <name>', 'Scope --env or --profile to one named environment within it (e.g. "dev", or a dotted child like "staging.eu")')
1532
1540
  .action(async (paths, opts) => {
1533
1541
  const targets = paths.length > 0 ? paths : ['.'];
1534
1542
  const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1535
- if (opts.env) {
1536
- const envPath = resolve(opts.env);
1537
- if (!existsSync(envPath)) {
1538
- console.error(chalk.red(`Env file not found: ${envPath}`));
1539
- process.exit(EXIT_USAGE_ERROR);
1540
- }
1541
- try {
1542
- Object.assign(env, loadEnvFile(envPath, opts.environment));
1543
- }
1544
- catch (err) {
1545
- console.error(chalk.red(` ✗ ${err.message}`));
1546
- process.exit(EXIT_USAGE_ERROR);
1547
- }
1543
+ try {
1544
+ Object.assign(env, resolveCliEnv(opts, process.cwd(), env));
1545
+ }
1546
+ catch (err) {
1547
+ console.error(chalk.red(` ✗ ${err.message}`));
1548
+ process.exit(EXIT_USAGE_ERROR);
1548
1549
  }
1549
1550
  const activePlugins = await loadEnabledPlugins();
1550
1551
  const allTools = [];