@kbediako/codex-orchestrator 0.1.0 → 0.1.2

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 CHANGED
@@ -80,6 +80,7 @@ Use `npx codex-orchestrator resume --run <run-id>` to continue interrupted runs;
80
80
  - `codex-orchestrator mcp serve [--repo <path>] [--dry-run] [-- <extra args>]`: launch the MCP stdio server (delegates to `codex mcp-server`; stdout guard keeps protocol-only output, logs to stderr).
81
81
  - `codex-orchestrator init codex [--cwd <path>] [--force]`: copy starter templates into a repo (no overwrite unless `--force`).
82
82
  - `codex-orchestrator doctor [--format json]`: check optional tooling dependencies and print install commands.
83
+ - `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
83
84
  - `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
84
85
  - `codex-orchestrator --version`: print the package version.
85
86
 
@@ -196,10 +197,28 @@ Use an explicit handoff note for reviewers. `NOTES` is required for review runs;
196
197
 
197
198
  Template: `Goal: ... | Summary: ... | Risks: ... | Questions (optional): ...`
198
199
 
199
- To enable Chrome DevTools for review runs, set `CODEX_REVIEW_DEVTOOLS=1` (uses `scripts/codex-devtools.sh` when executable; otherwise falls back to `codex -c ...`).
200
+ To enable Chrome DevTools for review runs, set `CODEX_REVIEW_DEVTOOLS=1` (uses a codex config override; no repo scripts required).
200
201
  Default to the standard `implementation-gate` for general reviews; use `implementation-gate-devtools` only when the review needs Chrome DevTools capabilities (visual/layout checks, network/perf diagnostics). After fixing review feedback, rerun the same gate and include any follow-up questions in `NOTES`.
201
202
  To run the full implementation gate with DevTools-enabled review, use `npx codex-orchestrator start implementation-gate-devtools --format json --no-interactive --task <task-id>`.
202
203
 
204
+ ## Frontend Testing
205
+ Frontend testing is a first-class pipeline with DevTools off by default. The shipped pipelines already set `CODEX_NON_INTERACTIVE=1`; add it explicitly for custom automation or when you want the `frontend-test` shortcut to suppress Codex prompts:
206
+ - `CODEX_NON_INTERACTIVE=1 npx codex-orchestrator start frontend-testing --format json --no-interactive --task <task-id>`
207
+ - `CODEX_NON_INTERACTIVE=1 npx codex-orchestrator start frontend-testing-devtools --format json --no-interactive --task <task-id>` (DevTools enabled)
208
+ - `CODEX_NON_INTERACTIVE=1 codex-orchestrator frontend-test` (shortcut; add `--devtools` to enable DevTools)
209
+
210
+ If you run the pipelines from this repo, run `npm run build` first so `dist/` stays current (the pipeline executes the compiled runner).
211
+
212
+ Note: the frontend-testing pipelines toggle the shared `CODEX_REVIEW_DEVTOOLS` flag under the hood; prefer `--devtools` or the devtools pipeline instead of setting it manually.
213
+
214
+ Optional prompt overrides:
215
+ - `CODEX_FRONTEND_TEST_PROMPT` (inline prompt)
216
+ - `CODEX_FRONTEND_TEST_PROMPT_PATH` (path to a prompt file)
217
+
218
+ `--no-interactive` disables the HUD only; set `CODEX_NON_INTERACTIVE=1` when you need to suppress Codex prompts (e.g., shortcut runs or custom automation).
219
+
220
+ Check readiness with `codex-orchestrator doctor --format json` (reports DevTools skill + MCP config availability). Use `codex-orchestrator devtools setup` to print setup steps.
221
+
203
222
  ## Mirror Workflows
204
223
  - `npm run mirror:fetch -- --project <name> [--dry-run] [--force]`: reads `packages/<project>/mirror.config.json` (origin, routes, asset roots, rewrite/block/allow lists), caches downloads **per project** under `.runs/<task>/mirror/<project>/cache`, strips tracker patterns, rewrites externals to `/external/<host>/...`, localizes OG/twitter preview images, rewrites share links off tracker-heavy hosts, and stages into `.runs/<task>/mirror/<project>/<timestamp>/staging/public` before promoting to `packages/<project>/public`. Non-origin assets fall back to Web Archive when the primary host is down; promotion is skipped if errors are detected unless `--force` is set. Manifests live at `.runs/<task>/mirror/<project>/<timestamp>/manifest.json` (warns when `MCP_RUNNER_TASK_ID` is unset; honors `compliance/permit.json` when present).
205
224
  - `npm run mirror:serve -- --project <name> [--port <port>] [--csp <self|strict|off>] [--no-range]`: shared local-mirror server with traversal guard, HTML no-cache/asset immutability, optional CSP, optional Range support, and directory-listing blocks.
@@ -9,6 +9,7 @@ import { evaluateInteractiveGate } from '../orchestrator/src/cli/utils/interacti
9
9
  import { buildSelfCheckResult } from '../orchestrator/src/cli/selfCheck.js';
10
10
  import { initCodexTemplates, formatInitSummary } from '../orchestrator/src/cli/init.js';
11
11
  import { runDoctor, formatDoctorSummary } from '../orchestrator/src/cli/doctor.js';
12
+ import { formatDevtoolsSetupSummary, runDevtoolsSetup } from '../orchestrator/src/cli/devtoolsSetup.js';
12
13
  import { loadPackageInfo } from '../orchestrator/src/cli/utils/packageInfo.js';
13
14
  import { serveMcp } from '../orchestrator/src/cli/mcp.js';
14
15
  async function main() {
@@ -28,6 +29,9 @@ async function main() {
28
29
  case 'start':
29
30
  await handleStart(orchestrator, args);
30
31
  break;
32
+ case 'frontend-test':
33
+ await handleFrontendTest(orchestrator, args);
34
+ break;
31
35
  case 'plan':
32
36
  await handlePlan(orchestrator, args);
33
37
  break;
@@ -49,6 +53,9 @@ async function main() {
49
53
  case 'doctor':
50
54
  await handleDoctor(args);
51
55
  break;
56
+ case 'devtools':
57
+ await handleDevtools(args);
58
+ break;
52
59
  case 'mcp':
53
60
  await handleMcp(args);
54
61
  break;
@@ -155,6 +162,61 @@ async function handleStart(orchestrator, rawArgs) {
155
162
  runEvents.dispose();
156
163
  }
157
164
  }
165
+ async function handleFrontendTest(orchestrator, rawArgs) {
166
+ const { positionals, flags } = parseArgs(rawArgs);
167
+ const format = flags['format'] === 'json' ? 'json' : 'text';
168
+ const devtools = Boolean(flags['devtools']);
169
+ const interactiveRequested = Boolean(flags['interactive'] || flags['ui']);
170
+ const interactiveDisabled = Boolean(flags['no-interactive']);
171
+ const runEvents = new RunEventEmitter();
172
+ const gate = evaluateInteractiveGate({
173
+ requested: interactiveRequested,
174
+ disabled: interactiveDisabled,
175
+ format,
176
+ stdoutIsTTY: process.stdout.isTTY === true,
177
+ stderrIsTTY: process.stderr.isTTY === true,
178
+ term: process.env.TERM ?? null
179
+ });
180
+ const hud = await maybeStartHud(gate, runEvents);
181
+ if (!gate.enabled && interactiveRequested && !interactiveDisabled && gate.reason) {
182
+ console.error(`[HUD disabled] ${gate.reason}`);
183
+ }
184
+ if (positionals.length > 0) {
185
+ console.error(`[frontend-test] ignoring extra arguments: ${positionals.join(' ')}`);
186
+ }
187
+ try {
188
+ const pipelineId = devtools ? 'frontend-testing-devtools' : 'frontend-testing';
189
+ const result = await orchestrator.start({
190
+ pipelineId,
191
+ taskId: typeof flags['task'] === 'string' ? flags['task'] : undefined,
192
+ parentRunId: typeof flags['parent-run'] === 'string' ? flags['parent-run'] : undefined,
193
+ approvalPolicy: typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined,
194
+ targetStageId: resolveTargetStageId(flags),
195
+ runEvents
196
+ });
197
+ hud?.stop();
198
+ const payload = {
199
+ run_id: result.manifest.run_id,
200
+ status: result.manifest.status,
201
+ artifact_root: result.manifest.artifact_root,
202
+ manifest: `${result.manifest.artifact_root}/manifest.json`,
203
+ log_path: result.manifest.log_path
204
+ };
205
+ if (format === 'json') {
206
+ console.log(JSON.stringify(payload, null, 2));
207
+ }
208
+ else {
209
+ console.log(`Run started: ${payload.run_id}`);
210
+ console.log(`Status: ${payload.status}`);
211
+ console.log(`Manifest: ${payload.manifest}`);
212
+ console.log(`Log: ${payload.log_path}`);
213
+ }
214
+ }
215
+ finally {
216
+ hud?.stop();
217
+ runEvents.dispose();
218
+ }
219
+ }
158
220
  async function handlePlan(orchestrator, rawArgs) {
159
221
  const { positionals, flags } = parseArgs(rawArgs);
160
222
  const pipelineId = positionals[0];
@@ -330,6 +392,30 @@ async function handleDoctor(rawArgs) {
330
392
  console.log(line);
331
393
  }
332
394
  }
395
+ async function handleDevtools(rawArgs) {
396
+ const { positionals, flags } = parseArgs(rawArgs);
397
+ const subcommand = positionals.shift();
398
+ if (!subcommand) {
399
+ throw new Error('devtools requires a subcommand (setup).');
400
+ }
401
+ if (subcommand !== 'setup') {
402
+ throw new Error(`Unknown devtools subcommand: ${subcommand}`);
403
+ }
404
+ const format = flags['format'] === 'json' ? 'json' : 'text';
405
+ const apply = Boolean(flags['yes']);
406
+ if (format === 'json' && apply) {
407
+ throw new Error('devtools setup does not support --format json with --yes.');
408
+ }
409
+ const result = await runDevtoolsSetup({ apply });
410
+ if (format === 'json') {
411
+ console.log(JSON.stringify(result, null, 2));
412
+ return;
413
+ }
414
+ const summary = formatDevtoolsSetupSummary(result);
415
+ for (const line of summary) {
416
+ console.log(line);
417
+ }
418
+ }
333
419
  async function handleMcp(rawArgs) {
334
420
  const { positionals, flags } = parseArgs(rawArgs);
335
421
  const subcommand = positionals.shift();
@@ -467,6 +553,16 @@ Commands:
467
553
  --interactive | --ui Enable read-only HUD when running in a TTY.
468
554
  --no-interactive Force disable HUD (default is off unless requested).
469
555
 
556
+ frontend-test Run frontend testing pipeline.
557
+ --devtools Enable Chrome DevTools MCP for this run.
558
+ --task <id> Override task identifier (defaults to MCP_RUNNER_TASK_ID).
559
+ --parent-run <id> Link run to parent run id.
560
+ --approval-policy <p> Record approval policy metadata.
561
+ --format json Emit machine-readable output.
562
+ --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
563
+ --interactive | --ui Enable read-only HUD when running in a TTY.
564
+ --no-interactive Force disable HUD (default is off unless requested).
565
+
470
566
  plan [pipeline] Preview pipeline stages without executing.
471
567
  --task <id> Override task identifier.
472
568
  --format json Emit machine-readable output.
@@ -494,6 +590,9 @@ Commands:
494
590
  self-check [--format json]
495
591
  init codex [--cwd <path>] [--force]
496
592
  doctor [--format json]
593
+ devtools setup Print DevTools MCP setup instructions.
594
+ --yes Apply setup by running "codex mcp add ...".
595
+ --format json Emit machine-readable output (dry-run only).
497
596
  mcp serve [--repo <path>] [--dry-run] [-- <extra args>]
498
597
  version | --version
499
598
 
@@ -0,0 +1,66 @@
1
+ import { spawn } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { buildDevtoolsSetupPlan, resolveDevtoolsReadiness } from './utils/devtools.js';
4
+ export async function runDevtoolsSetup(options = {}) {
5
+ const env = options.env ?? process.env;
6
+ const plan = buildDevtoolsSetupPlan(env);
7
+ const readiness = resolveDevtoolsReadiness(env);
8
+ if (!options.apply) {
9
+ return { status: 'planned', plan, readiness };
10
+ }
11
+ if (readiness.config.status === 'ok') {
12
+ return {
13
+ status: 'skipped',
14
+ reason: 'DevTools MCP is already configured.',
15
+ plan,
16
+ readiness
17
+ };
18
+ }
19
+ if (readiness.config.status === 'invalid') {
20
+ throw new Error(`Cannot apply DevTools setup because config.toml is invalid: ${readiness.config.path}`);
21
+ }
22
+ await applyDevtoolsSetup(plan, env);
23
+ return { status: 'applied', plan, readiness };
24
+ }
25
+ export function formatDevtoolsSetupSummary(result) {
26
+ const lines = [];
27
+ lines.push(`DevTools setup: ${result.status}`);
28
+ if (result.reason) {
29
+ lines.push(`Note: ${result.reason}`);
30
+ }
31
+ lines.push(`- Codex home: ${result.plan.codexHome}`);
32
+ lines.push(`- Skill: ${result.readiness.skill.status} (${result.readiness.skill.path})`);
33
+ const configLabel = result.readiness.config.status === 'invalid'
34
+ ? `invalid (${result.readiness.config.path})`
35
+ : `${result.readiness.config.status} (${result.readiness.config.path})`;
36
+ lines.push(`- Config: ${configLabel}`);
37
+ if (result.readiness.config.detail) {
38
+ lines.push(` detail: ${result.readiness.config.detail}`);
39
+ }
40
+ if (result.readiness.config.error) {
41
+ lines.push(` error: ${result.readiness.config.error}`);
42
+ }
43
+ lines.push(`- Command: ${result.plan.commandLine}`);
44
+ lines.push('- Config snippet:');
45
+ for (const line of result.plan.configSnippet.split('\n')) {
46
+ lines.push(` ${line}`);
47
+ }
48
+ if (result.status === 'planned') {
49
+ lines.push('Run with --yes to apply this setup.');
50
+ }
51
+ return lines;
52
+ }
53
+ async function applyDevtoolsSetup(plan, env) {
54
+ await new Promise((resolve, reject) => {
55
+ const child = spawn(plan.command, plan.args, { stdio: 'inherit', env });
56
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
57
+ child.once('exit', (code) => {
58
+ if (code === 0) {
59
+ resolve();
60
+ }
61
+ else {
62
+ reject(new Error(`codex mcp add exited with code ${code ?? 'unknown'}`));
63
+ }
64
+ });
65
+ });
66
+ }
@@ -1,4 +1,5 @@
1
1
  import process from 'node:process';
2
+ import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
2
3
  import { resolveOptionalDependency } from './utils/optionalDeps.js';
3
4
  const OPTIONAL_DEPENDENCIES = [
4
5
  {
@@ -22,11 +23,53 @@ export function runDoctor(cwd = process.cwd()) {
22
23
  install: entry.install
23
24
  };
24
25
  });
26
+ const readiness = resolveDevtoolsReadiness();
27
+ const setupPlan = buildDevtoolsSetupPlan();
28
+ const devtools = {
29
+ status: readiness.status,
30
+ skill: {
31
+ name: DEVTOOLS_SKILL_NAME,
32
+ status: readiness.skill.status,
33
+ path: readiness.skill.path,
34
+ install: readiness.skill.status === 'ok'
35
+ ? undefined
36
+ : [
37
+ `Copy the ${DEVTOOLS_SKILL_NAME} skill into ${setupPlan.codexHome}/skills/${DEVTOOLS_SKILL_NAME}`,
38
+ `Expected file: ${readiness.skill.path}`
39
+ ]
40
+ },
41
+ config: {
42
+ status: readiness.config.status,
43
+ path: readiness.config.path,
44
+ detail: readiness.config.detail,
45
+ error: readiness.config.error,
46
+ install: readiness.config.status === 'ok'
47
+ ? undefined
48
+ : [
49
+ 'Run: codex-orchestrator devtools setup',
50
+ `Run: ${setupPlan.commandLine}`,
51
+ `Config path: ${setupPlan.configPath}`,
52
+ 'Config snippet:',
53
+ ...setupPlan.configSnippet.split('\n')
54
+ ]
55
+ },
56
+ enablement: [
57
+ 'Enable DevTools for a run with CODEX_REVIEW_DEVTOOLS=1',
58
+ "Or run Codex with: codex -c 'mcp_servers.chrome-devtools.enabled=true' ..."
59
+ ]
60
+ };
25
61
  const missing = dependencies.filter((dep) => dep.status === 'missing').map((dep) => dep.name);
62
+ if (readiness.skill.status === 'missing') {
63
+ missing.push(DEVTOOLS_SKILL_NAME);
64
+ }
65
+ if (readiness.config.status !== 'ok') {
66
+ missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
67
+ }
26
68
  return {
27
69
  status: missing.length === 0 ? 'ok' : 'warning',
28
70
  missing,
29
- dependencies
71
+ dependencies,
72
+ devtools
30
73
  };
31
74
  }
32
75
  export function formatDoctorSummary(result) {
@@ -44,5 +87,36 @@ export function formatDoctorSummary(result) {
44
87
  }
45
88
  }
46
89
  }
90
+ lines.push(`DevTools: ${result.devtools.status}`);
91
+ if (result.devtools.skill.status === 'ok') {
92
+ lines.push(` - ${result.devtools.skill.name}: ok (${result.devtools.skill.path})`);
93
+ }
94
+ else {
95
+ lines.push(` - ${result.devtools.skill.name}: missing`);
96
+ for (const instruction of result.devtools.skill.install ?? []) {
97
+ lines.push(` install: ${instruction}`);
98
+ }
99
+ }
100
+ if (result.devtools.config.status === 'ok') {
101
+ lines.push(` - config.toml: ok (${result.devtools.config.path})`);
102
+ }
103
+ else {
104
+ const label = result.devtools.config.status === 'invalid'
105
+ ? `invalid (${result.devtools.config.path})`
106
+ : `missing (${result.devtools.config.path})`;
107
+ lines.push(` - config.toml: ${label}`);
108
+ if (result.devtools.config.detail) {
109
+ lines.push(` detail: ${result.devtools.config.detail}`);
110
+ }
111
+ if (result.devtools.config.error) {
112
+ lines.push(` error: ${result.devtools.config.error}`);
113
+ }
114
+ for (const instruction of result.devtools.config.install ?? []) {
115
+ lines.push(` install: ${instruction}`);
116
+ }
117
+ }
118
+ for (const line of result.devtools.enablement) {
119
+ lines.push(` - ${line}`);
120
+ }
47
121
  return lines;
48
122
  }
@@ -0,0 +1,94 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import process from 'node:process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { logger } from '../logger.js';
7
+ import { resolveCodexCommand } from './utils/devtools.js';
8
+ const DEFAULT_PROMPT = [
9
+ 'You are running frontend testing for the current project.',
10
+ '',
11
+ 'Goals:',
12
+ '- Validate critical user flows and layout responsiveness.',
13
+ '- Check the browser console and network panels for errors.',
14
+ '- If Chrome DevTools MCP is enabled, use it.',
15
+ '',
16
+ 'Output a concise report with:',
17
+ '- Summary',
18
+ '- Steps executed',
19
+ '- Issues (severity, repro)',
20
+ '- Follow-up recommendations',
21
+ '',
22
+ 'Do not modify code unless explicitly asked.'
23
+ ].join('\n');
24
+ export async function loadFrontendTestingPrompt(env = process.env) {
25
+ const promptPath = env.CODEX_FRONTEND_TEST_PROMPT_PATH?.trim();
26
+ if (promptPath) {
27
+ const raw = await readFile(resolve(promptPath), 'utf8');
28
+ const trimmed = raw.trim();
29
+ if (!trimmed) {
30
+ throw new Error(`Frontend testing prompt file is empty: ${promptPath}`);
31
+ }
32
+ return trimmed;
33
+ }
34
+ const inlinePrompt = env.CODEX_FRONTEND_TEST_PROMPT?.trim();
35
+ if (inlinePrompt) {
36
+ return inlinePrompt;
37
+ }
38
+ return DEFAULT_PROMPT;
39
+ }
40
+ export function resolveFrontendTestingCommand(prompt, env = process.env) {
41
+ const args = ['exec', prompt];
42
+ return resolveCodexCommand(args, env);
43
+ }
44
+ function envFlagEnabled(value) {
45
+ if (!value) {
46
+ return false;
47
+ }
48
+ const normalized = value.trim().toLowerCase();
49
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
50
+ }
51
+ function shouldForceNonInteractive(env) {
52
+ const stdinIsTTY = process.stdin?.isTTY === true;
53
+ return (!stdinIsTTY ||
54
+ envFlagEnabled(env.CI) ||
55
+ envFlagEnabled(env.CODEX_REVIEW_NON_INTERACTIVE) ||
56
+ envFlagEnabled(env.CODEX_NON_INTERACTIVE) ||
57
+ envFlagEnabled(env.CODEX_NONINTERACTIVE) ||
58
+ envFlagEnabled(env.CODEX_NO_INTERACTIVE));
59
+ }
60
+ export async function runFrontendTesting(env = process.env) {
61
+ const prompt = await loadFrontendTestingPrompt(env);
62
+ const { command, args } = resolveFrontendTestingCommand(prompt, env);
63
+ const nonInteractive = shouldForceNonInteractive(env);
64
+ const childEnv = { ...process.env, ...env };
65
+ if (nonInteractive) {
66
+ childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
67
+ childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
68
+ childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
69
+ }
70
+ const stdio = nonInteractive ? ['ignore', 'inherit', 'inherit'] : 'inherit';
71
+ const child = spawn(command, args, { stdio, env: childEnv });
72
+ await new Promise((resolvePromise, reject) => {
73
+ child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
74
+ child.once('exit', (code) => {
75
+ if (code === 0) {
76
+ resolvePromise();
77
+ }
78
+ else {
79
+ reject(new Error(`codex exec exited with code ${code ?? 'unknown'}`));
80
+ }
81
+ });
82
+ });
83
+ }
84
+ async function main() {
85
+ await runFrontendTesting();
86
+ }
87
+ const entry = process.argv[1] ? resolve(process.argv[1]) : null;
88
+ const self = resolve(fileURLToPath(import.meta.url));
89
+ if (entry && entry === self) {
90
+ main().catch((error) => {
91
+ logger.error(error instanceof Error ? error.message : String(error));
92
+ process.exitCode = 1;
93
+ });
94
+ }
@@ -0,0 +1,196 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
5
+ import { EnvUtils } from '../../../../packages/shared/config/env.js';
6
+ export const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
7
+ export const DEVTOOLS_CONFIG_OVERRIDE = 'mcp_servers.chrome-devtools.enabled=true';
8
+ const DEVTOOLS_CONFIG_FILENAME = 'config.toml';
9
+ const DEVTOOLS_MCP_COMMAND = [
10
+ 'mcp',
11
+ 'add',
12
+ DEVTOOLS_SKILL_NAME,
13
+ '--',
14
+ 'npx',
15
+ '-y',
16
+ 'chrome-devtools-mcp@latest',
17
+ '--categoryEmulation',
18
+ '--categoryPerformance',
19
+ '--categoryNetwork'
20
+ ];
21
+ const DEVTOOLS_CONFIG_SNIPPET = [
22
+ '[mcp_servers.chrome-devtools]',
23
+ 'command = "npx"',
24
+ 'args = ["-y", "chrome-devtools-mcp@latest", "--categoryEmulation", "--categoryPerformance", "--categoryNetwork"]',
25
+ 'enabled = false'
26
+ ].join('\n');
27
+ export function isDevtoolsEnabled(env = process.env) {
28
+ const raw = env.CODEX_REVIEW_DEVTOOLS;
29
+ if (!raw) {
30
+ return false;
31
+ }
32
+ return EnvUtils.isTrue(raw.trim().toLowerCase());
33
+ }
34
+ export function resolveCodexHome(env = process.env) {
35
+ const override = env.CODEX_HOME?.trim();
36
+ if (override) {
37
+ return override;
38
+ }
39
+ return join(homedir(), '.codex');
40
+ }
41
+ export function resolveCodexConfigPath(env = process.env) {
42
+ return join(resolveCodexHome(env), DEVTOOLS_CONFIG_FILENAME);
43
+ }
44
+ export function resolveDevtoolsReadiness(env = process.env) {
45
+ const codexHome = resolveCodexHome(env);
46
+ const skillPath = join(codexHome, 'skills', DEVTOOLS_SKILL_NAME, 'SKILL.md');
47
+ const skillInstalled = existsSync(skillPath);
48
+ const config = inspectDevtoolsConfig(env);
49
+ const configReady = config.status === 'ok';
50
+ let status;
51
+ if (config.status === 'invalid') {
52
+ status = 'invalid-config';
53
+ }
54
+ else if (!skillInstalled && !configReady) {
55
+ status = 'missing-both';
56
+ }
57
+ else if (!skillInstalled) {
58
+ status = 'missing-skill';
59
+ }
60
+ else if (!configReady) {
61
+ status = 'missing-config';
62
+ }
63
+ else {
64
+ status = 'ok';
65
+ }
66
+ return {
67
+ status,
68
+ skill: {
69
+ status: skillInstalled ? 'ok' : 'missing',
70
+ path: skillPath
71
+ },
72
+ config
73
+ };
74
+ }
75
+ export function buildDevtoolsSetupPlan(env = process.env) {
76
+ const codexHome = resolveCodexHome(env);
77
+ const configPath = resolveCodexConfigPath(env);
78
+ const args = [...DEVTOOLS_MCP_COMMAND];
79
+ return {
80
+ codexHome,
81
+ configPath,
82
+ command: 'codex',
83
+ args,
84
+ commandLine: ['codex', ...args].join(' '),
85
+ configSnippet: DEVTOOLS_CONFIG_SNIPPET
86
+ };
87
+ }
88
+ export function resolveCodexCommand(args, env = process.env) {
89
+ if (!isDevtoolsEnabled(env)) {
90
+ return { command: 'codex', args };
91
+ }
92
+ const readiness = resolveDevtoolsReadiness(env);
93
+ if (readiness.status !== 'ok') {
94
+ throw new Error(formatDevtoolsPreflightError(readiness));
95
+ }
96
+ return {
97
+ command: 'codex',
98
+ args: ['-c', DEVTOOLS_CONFIG_OVERRIDE, ...args]
99
+ };
100
+ }
101
+ export function formatDevtoolsPreflightError(readiness) {
102
+ const lines = ['DevTools MCP is not ready for this run.'];
103
+ lines.push(`- Skill: ${readiness.skill.status} (${readiness.skill.path})`);
104
+ const configStatus = readiness.config.status === 'invalid'
105
+ ? `invalid (${readiness.config.path})`
106
+ : `${readiness.config.status} (${readiness.config.path})`;
107
+ lines.push(`- Config: ${configStatus}`);
108
+ if (readiness.config.detail) {
109
+ lines.push(` detail: ${readiness.config.detail}`);
110
+ }
111
+ if (readiness.config.error) {
112
+ lines.push(` error: ${readiness.config.error}`);
113
+ }
114
+ lines.push('Run `codex-orchestrator doctor --format json` for details.');
115
+ lines.push('Run `codex-orchestrator devtools setup` to configure the MCP server.');
116
+ return lines.join('\n');
117
+ }
118
+ function inspectDevtoolsConfig(env = process.env) {
119
+ const configPath = resolveCodexConfigPath(env);
120
+ if (!existsSync(configPath)) {
121
+ return { status: 'missing', path: configPath, detail: 'config.toml not found' };
122
+ }
123
+ let raw;
124
+ try {
125
+ raw = readFileSync(configPath, 'utf8');
126
+ }
127
+ catch (error) {
128
+ return {
129
+ status: 'invalid',
130
+ path: configPath,
131
+ error: error instanceof Error ? error.message : String(error)
132
+ };
133
+ }
134
+ const hasEntry = hasDevtoolsConfigEntry(raw);
135
+ if (hasEntry) {
136
+ return { status: 'ok', path: configPath };
137
+ }
138
+ return {
139
+ status: 'missing',
140
+ path: configPath,
141
+ detail: 'chrome-devtools entry not found'
142
+ };
143
+ }
144
+ function hasDevtoolsConfigEntry(raw) {
145
+ const lines = raw.split('\n');
146
+ let currentTable = null;
147
+ for (const line of lines) {
148
+ const trimmed = stripTomlComment(line).trim();
149
+ if (!trimmed) {
150
+ continue;
151
+ }
152
+ const tableMatch = trimmed.match(/^\[(.+)\]$/);
153
+ if (tableMatch) {
154
+ currentTable = tableMatch[1]?.trim() ?? null;
155
+ if (currentTable === 'mcp_servers.chrome-devtools' ||
156
+ currentTable === 'mcp_servers."chrome-devtools"' ||
157
+ currentTable === "mcp_servers.'chrome-devtools'") {
158
+ return true;
159
+ }
160
+ continue;
161
+ }
162
+ if (trimmed.startsWith('mcp_servers.')) {
163
+ if (trimmed.startsWith('mcp_servers."chrome-devtools".')) {
164
+ return true;
165
+ }
166
+ if (trimmed.startsWith("mcp_servers.'chrome-devtools'.")) {
167
+ return true;
168
+ }
169
+ if (trimmed.startsWith('mcp_servers.chrome-devtools.')) {
170
+ return true;
171
+ }
172
+ if (trimmed.startsWith('mcp_servers."chrome-devtools"=')) {
173
+ return true;
174
+ }
175
+ if (trimmed.startsWith("mcp_servers.'chrome-devtools'=")) {
176
+ return true;
177
+ }
178
+ if (trimmed.startsWith('mcp_servers.chrome-devtools=')) {
179
+ return true;
180
+ }
181
+ }
182
+ if (currentTable === 'mcp_servers') {
183
+ if (/^"?chrome-devtools"?\s*=/.test(trimmed)) {
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+ function stripTomlComment(line) {
191
+ const index = line.indexOf('#');
192
+ if (index === -1) {
193
+ return line;
194
+ }
195
+ return line.slice(0, index);
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -80,10 +80,18 @@
80
80
  "pngjs": "^7.0.0"
81
81
  },
82
82
  "peerDependenciesMeta": {
83
- "cheerio": { "optional": true },
84
- "pixelmatch": { "optional": true },
85
- "playwright": { "optional": true },
86
- "pngjs": { "optional": true }
83
+ "cheerio": {
84
+ "optional": true
85
+ },
86
+ "pixelmatch": {
87
+ "optional": true
88
+ },
89
+ "playwright": {
90
+ "optional": true
91
+ },
92
+ "pngjs": {
93
+ "optional": true
94
+ }
87
95
  },
88
96
  "overrides": {
89
97
  "esbuild": "^0.25.11"