@kbediako/codex-orchestrator 0.1.32 → 0.1.34
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 +96 -12
- package/codex.orchestrator.json +448 -0
- package/dist/bin/codex-orchestrator.js +703 -136
- package/dist/orchestrator/src/cli/codexCliSetup.js +1 -0
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +22 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +20 -9
- package/dist/orchestrator/src/cli/delegationSetup.js +111 -14
- package/dist/orchestrator/src/cli/doctor.js +264 -8
- package/dist/orchestrator/src/cli/doctorIssueLog.js +350 -0
- package/dist/orchestrator/src/cli/doctorUsage.js +150 -8
- package/dist/orchestrator/src/cli/init.js +24 -1
- package/dist/orchestrator/src/cli/mcpEnable.js +392 -0
- package/dist/orchestrator/src/cli/orchestrator.js +180 -5
- package/dist/orchestrator/src/cli/rlmRunner.js +289 -35
- package/dist/orchestrator/src/cli/run/manifest.js +31 -6
- package/dist/orchestrator/src/cli/services/commandRunner.js +10 -2
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +70 -18
- package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/skills.js +3 -8
- package/dist/orchestrator/src/cli/utils/advancedAutopilot.js +114 -0
- package/dist/orchestrator/src/cli/utils/codexCli.js +21 -0
- package/dist/orchestrator/src/cli/utils/commandPreview.js +10 -0
- package/dist/orchestrator/src/cli/utils/delegationGuardRunner.js +85 -8
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -1
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +79 -19
- package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +46 -6
- package/dist/orchestrator/src/control-plane/request-builder.js +9 -8
- package/dist/scripts/lib/pr-watch-merge.js +367 -3
- package/docs/README.md +17 -11
- package/package.json +2 -1
- package/schemas/manifest.json +27 -0
- package/skills/collab-deliberation/SKILL.md +6 -0
- package/skills/collab-evals/SKILL.md +4 -0
- package/skills/collab-subagents-first/SKILL.md +29 -7
- package/skills/delegation-usage/DELEGATION_GUIDE.md +31 -5
- package/skills/delegation-usage/SKILL.md +29 -4
- package/skills/elegance-review/SKILL.md +14 -3
- package/skills/standalone-review/SKILL.md +8 -2
- package/templates/README.md +1 -1
- package/templates/codex/AGENTS.md +12 -1
|
@@ -2,6 +2,8 @@ import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
4
|
import { findPackageRoot } from './utils/packageInfo.js';
|
|
5
|
+
const CODEX_TEMPLATE = 'codex';
|
|
6
|
+
const CODEX_PIPELINE_CONFIG = 'codex.orchestrator.json';
|
|
5
7
|
export async function initCodexTemplates(options) {
|
|
6
8
|
const root = findPackageRoot();
|
|
7
9
|
const templateRoot = join(root, 'templates', options.template);
|
|
@@ -13,6 +15,13 @@ export async function initCodexTemplates(options) {
|
|
|
13
15
|
written,
|
|
14
16
|
skipped
|
|
15
17
|
});
|
|
18
|
+
if (options.template === CODEX_TEMPLATE) {
|
|
19
|
+
await copyTemplateFile(join(root, CODEX_PIPELINE_CONFIG), join(options.cwd, CODEX_PIPELINE_CONFIG), {
|
|
20
|
+
force: options.force,
|
|
21
|
+
written,
|
|
22
|
+
skipped
|
|
23
|
+
});
|
|
24
|
+
}
|
|
16
25
|
return { written, skipped, templateRoot };
|
|
17
26
|
}
|
|
18
27
|
async function assertDirectory(path) {
|
|
@@ -43,6 +52,19 @@ async function copyTemplateDir(sourceDir, targetDir, options) {
|
|
|
43
52
|
options.written.push(targetPath);
|
|
44
53
|
}
|
|
45
54
|
}
|
|
55
|
+
async function copyTemplateFile(sourcePath, targetPath, options) {
|
|
56
|
+
const info = await stat(sourcePath).catch(() => null);
|
|
57
|
+
if (!info || !info.isFile()) {
|
|
58
|
+
throw new Error(`Template file not found: ${sourcePath}`);
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(targetPath) && !options.force) {
|
|
61
|
+
options.skipped.push(targetPath);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
65
|
+
await copyFile(sourcePath, targetPath);
|
|
66
|
+
options.written.push(targetPath);
|
|
67
|
+
}
|
|
46
68
|
export function formatInitSummary(result, cwd) {
|
|
47
69
|
const lines = [];
|
|
48
70
|
if (result.written.length > 0) {
|
|
@@ -61,7 +83,8 @@ export function formatInitSummary(result, cwd) {
|
|
|
61
83
|
lines.push('No files written.');
|
|
62
84
|
}
|
|
63
85
|
lines.push('Next steps (recommended):');
|
|
86
|
+
lines.push(' - Review codex.orchestrator.json and adjust pipeline commands to your repository toolchain');
|
|
64
87
|
lines.push(' - codex-orchestrator setup --yes # installs bundled skills + configures delegation/devtools wiring');
|
|
65
|
-
lines.push(' - codex-orchestrator codex setup # optional
|
|
88
|
+
lines.push(' - codex-orchestrator codex setup # optional managed/pinned Codex CLI (activate with CODEX_CLI_USE_MANAGED=1; stock codex is default)');
|
|
66
89
|
return lines;
|
|
67
90
|
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
4
|
+
const DEFAULT_MCP_COMMAND_TIMEOUT_MS = 30_000;
|
|
5
|
+
export async function runMcpEnable(options = {}) {
|
|
6
|
+
const env = options.env ?? process.env;
|
|
7
|
+
const codexBin = resolveCodexCliBin(env);
|
|
8
|
+
const commandRunner = options.commandRunner ?? defaultMcpCommandRunner;
|
|
9
|
+
const listResult = await commandRunner({
|
|
10
|
+
command: codexBin,
|
|
11
|
+
args: ['mcp', 'list', '--json'],
|
|
12
|
+
env
|
|
13
|
+
});
|
|
14
|
+
if (listResult.exitCode !== 0) {
|
|
15
|
+
throw new Error(`codex mcp list failed: ${compactError(listResult.stderr, listResult.stdout)}`);
|
|
16
|
+
}
|
|
17
|
+
let servers;
|
|
18
|
+
try {
|
|
19
|
+
servers = parseMcpServerList(listResult.stdout);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
23
|
+
throw new Error(`codex mcp list --json returned invalid output: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
const requestedNames = dedupeNames(options.serverNames ?? []);
|
|
26
|
+
const targetNames = requestedNames.length > 0
|
|
27
|
+
? requestedNames
|
|
28
|
+
: servers
|
|
29
|
+
.filter((server) => !server.enabled)
|
|
30
|
+
.map((server) => server.name);
|
|
31
|
+
const actions = [];
|
|
32
|
+
for (const targetName of targetNames) {
|
|
33
|
+
const server = servers.find((item) => item.name === targetName);
|
|
34
|
+
if (!server) {
|
|
35
|
+
actions.push({
|
|
36
|
+
name: targetName,
|
|
37
|
+
status: 'missing',
|
|
38
|
+
reason: 'MCP server not found in codex mcp list output.'
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (server.enabled) {
|
|
43
|
+
actions.push({ name: targetName, status: 'already_enabled' });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const enablePlan = buildEnablePlan(server);
|
|
47
|
+
if (!enablePlan.ok) {
|
|
48
|
+
actions.push({
|
|
49
|
+
name: targetName,
|
|
50
|
+
status: 'unsupported',
|
|
51
|
+
reason: enablePlan.reason
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!options.apply) {
|
|
56
|
+
const displayArgs = redactArgsForDisplay(enablePlan.args);
|
|
57
|
+
actions.push({
|
|
58
|
+
name: targetName,
|
|
59
|
+
status: 'planned',
|
|
60
|
+
command_line: `${shellEscape(codexBin)} ${displayArgs.map(shellEscape).join(' ')}`
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const applyResult = await commandRunner({
|
|
65
|
+
command: codexBin,
|
|
66
|
+
args: enablePlan.args,
|
|
67
|
+
env
|
|
68
|
+
});
|
|
69
|
+
if (applyResult.exitCode !== 0) {
|
|
70
|
+
actions.push({
|
|
71
|
+
name: targetName,
|
|
72
|
+
status: 'failed',
|
|
73
|
+
reason: compactError(applyResult.stderr, applyResult.stdout)
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
actions.push({ name: targetName, status: 'enabled' });
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
status: options.apply ? 'applied' : 'planned',
|
|
81
|
+
codex_bin: codexBin,
|
|
82
|
+
targets: targetNames,
|
|
83
|
+
actions
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function formatMcpEnableSummary(result) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push(`MCP enable: ${result.status}`);
|
|
89
|
+
lines.push(`- Codex bin: ${result.codex_bin}`);
|
|
90
|
+
lines.push(`- Targets: ${result.targets.length > 0 ? result.targets.join(', ') : '<none>'}`);
|
|
91
|
+
const byStatus = summarizeByStatus(result.actions);
|
|
92
|
+
lines.push(`- Results: enabled=${byStatus.enabled}, planned=${byStatus.planned}, already_enabled=${byStatus.already_enabled}, missing=${byStatus.missing}, unsupported=${byStatus.unsupported}, failed=${byStatus.failed}`);
|
|
93
|
+
for (const action of result.actions) {
|
|
94
|
+
if (action.status === 'planned' && action.command_line) {
|
|
95
|
+
lines.push(` - ${action.name}: planned -> ${action.command_line}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (action.reason) {
|
|
99
|
+
lines.push(` - ${action.name}: ${action.status} (${action.reason})`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
lines.push(` - ${action.name}: ${action.status}`);
|
|
103
|
+
}
|
|
104
|
+
if (result.status === 'planned' && byStatus.planned > 0) {
|
|
105
|
+
lines.push('Run with --yes to apply.');
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
function parseMcpServerList(raw) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new Error('invalid JSON payload.');
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(parsed)) {
|
|
118
|
+
throw new Error('expected top-level JSON array.');
|
|
119
|
+
}
|
|
120
|
+
const servers = [];
|
|
121
|
+
for (const item of parsed) {
|
|
122
|
+
if (!item || typeof item !== 'object') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const record = item;
|
|
126
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
127
|
+
if (!name) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeof record.enabled !== 'boolean') {
|
|
131
|
+
throw new Error(`expected boolean "enabled" for server "${name}".`);
|
|
132
|
+
}
|
|
133
|
+
servers.push({
|
|
134
|
+
name,
|
|
135
|
+
enabled: record.enabled,
|
|
136
|
+
startupTimeoutSec: typeof record.startup_timeout_sec === 'number' && Number.isFinite(record.startup_timeout_sec)
|
|
137
|
+
? record.startup_timeout_sec
|
|
138
|
+
: null,
|
|
139
|
+
toolTimeoutSec: typeof record.tool_timeout_sec === 'number' && Number.isFinite(record.tool_timeout_sec)
|
|
140
|
+
? record.tool_timeout_sec
|
|
141
|
+
: null,
|
|
142
|
+
transport: record.transport ?? undefined
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return servers;
|
|
146
|
+
}
|
|
147
|
+
function buildEnablePlan(server) {
|
|
148
|
+
if (server.startupTimeoutSec !== null || server.toolTimeoutSec !== null) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: 'Server defines startup/tool timeout settings; codex mcp add cannot preserve those fields. Enable this server manually.'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const transport = server.transport;
|
|
155
|
+
if (!transport || typeof transport !== 'object') {
|
|
156
|
+
return { ok: false, reason: 'Server transport details are missing.' };
|
|
157
|
+
}
|
|
158
|
+
const type = typeof transport.type === 'string' ? transport.type.trim() : '';
|
|
159
|
+
if (type === 'stdio') {
|
|
160
|
+
const command = typeof transport.command === 'string' ? transport.command.trim() : '';
|
|
161
|
+
if (!command) {
|
|
162
|
+
return { ok: false, reason: 'stdio transport is missing command.' };
|
|
163
|
+
}
|
|
164
|
+
const hasUnsupportedStdioFields = Array.isArray(transport.env_vars) && transport.env_vars.length > 0
|
|
165
|
+
|| hasRecordEntries(transport.env_vars)
|
|
166
|
+
|| (typeof transport.cwd === 'string' && transport.cwd.trim().length > 0);
|
|
167
|
+
if (hasUnsupportedStdioFields) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
reason: 'stdio env_vars/cwd settings are configured; codex mcp add cannot preserve these fields. Enable this server manually.'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const args = ['mcp', 'add', server.name];
|
|
174
|
+
const envObject = normalizeStringRecord(transport.env);
|
|
175
|
+
for (const [key, value] of Object.entries(envObject)) {
|
|
176
|
+
args.push('--env', `${key}=${value}`);
|
|
177
|
+
}
|
|
178
|
+
args.push('--', command, ...normalizeStringArray(transport.args));
|
|
179
|
+
return { ok: true, args };
|
|
180
|
+
}
|
|
181
|
+
if (type === 'streamable_http') {
|
|
182
|
+
const url = typeof transport.url === 'string' ? transport.url.trim() : '';
|
|
183
|
+
if (!url) {
|
|
184
|
+
return { ok: false, reason: 'streamable_http transport is missing url.' };
|
|
185
|
+
}
|
|
186
|
+
const args = ['mcp', 'add', server.name, '--url', url];
|
|
187
|
+
const bearerTokenEnvVar = typeof transport.bearer_token_env_var === 'string' ? transport.bearer_token_env_var.trim() : '';
|
|
188
|
+
if (bearerTokenEnvVar) {
|
|
189
|
+
args.push('--bearer-token-env-var', bearerTokenEnvVar);
|
|
190
|
+
}
|
|
191
|
+
const hasUnsupportedHeaders = hasRecordEntries(transport.http_headers)
|
|
192
|
+
|| hasRecordEntries(transport.env_http_headers)
|
|
193
|
+
|| (Array.isArray(transport.http_headers) && transport.http_headers.length > 0)
|
|
194
|
+
|| (Array.isArray(transport.env_http_headers) && transport.env_http_headers.length > 0);
|
|
195
|
+
if (hasUnsupportedHeaders) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
reason: 'streamable_http headers/env_http_headers are configured; codex mcp add does not expose equivalent flags. Enable this server manually.'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, args };
|
|
202
|
+
}
|
|
203
|
+
return { ok: false, reason: `Unsupported transport type "${type || 'unknown'}".` };
|
|
204
|
+
}
|
|
205
|
+
function normalizeStringArray(value) {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return value
|
|
210
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
211
|
+
.filter((item) => item.length > 0);
|
|
212
|
+
}
|
|
213
|
+
function normalizeStringRecord(value) {
|
|
214
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
const record = {};
|
|
218
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
219
|
+
if (typeof entry !== 'string') {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const normalizedKey = key.trim();
|
|
223
|
+
if (!normalizedKey) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
record[normalizedKey] = entry;
|
|
227
|
+
}
|
|
228
|
+
return record;
|
|
229
|
+
}
|
|
230
|
+
function hasRecordEntries(value) {
|
|
231
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return Object.keys(value).length > 0;
|
|
235
|
+
}
|
|
236
|
+
function dedupeNames(items) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const normalized = [];
|
|
239
|
+
for (const item of items) {
|
|
240
|
+
const value = item.trim();
|
|
241
|
+
if (!value || seen.has(value)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
seen.add(value);
|
|
245
|
+
normalized.push(value);
|
|
246
|
+
}
|
|
247
|
+
return normalized;
|
|
248
|
+
}
|
|
249
|
+
function summarizeByStatus(actions) {
|
|
250
|
+
return actions.reduce((acc, action) => {
|
|
251
|
+
acc[action.status] += 1;
|
|
252
|
+
return acc;
|
|
253
|
+
}, {
|
|
254
|
+
planned: 0,
|
|
255
|
+
enabled: 0,
|
|
256
|
+
already_enabled: 0,
|
|
257
|
+
missing: 0,
|
|
258
|
+
unsupported: 0,
|
|
259
|
+
failed: 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function defaultMcpCommandRunner(request) {
|
|
263
|
+
return await new Promise((resolve) => {
|
|
264
|
+
const child = spawn(request.command, request.args, {
|
|
265
|
+
env: request.env,
|
|
266
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
267
|
+
});
|
|
268
|
+
const timeoutMs = Math.max(1, request.timeoutMs ?? DEFAULT_MCP_COMMAND_TIMEOUT_MS);
|
|
269
|
+
let stdout = '';
|
|
270
|
+
let stderr = '';
|
|
271
|
+
let settled = false;
|
|
272
|
+
const finalize = (result) => {
|
|
273
|
+
if (settled) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
settled = true;
|
|
277
|
+
clearTimeout(timeoutHandle);
|
|
278
|
+
resolve(result);
|
|
279
|
+
};
|
|
280
|
+
const timeoutHandle = setTimeout(() => {
|
|
281
|
+
child.kill('SIGTERM');
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
if (!settled) {
|
|
284
|
+
child.kill('SIGKILL');
|
|
285
|
+
}
|
|
286
|
+
}, 2_000).unref();
|
|
287
|
+
finalize({
|
|
288
|
+
exitCode: 124,
|
|
289
|
+
stdout,
|
|
290
|
+
stderr: `${stderr}\ncommand timed out after ${timeoutMs}ms`.trim()
|
|
291
|
+
});
|
|
292
|
+
}, timeoutMs);
|
|
293
|
+
timeoutHandle.unref();
|
|
294
|
+
child.stdout?.on('data', (chunk) => {
|
|
295
|
+
stdout += chunk.toString();
|
|
296
|
+
});
|
|
297
|
+
child.stderr?.on('data', (chunk) => {
|
|
298
|
+
stderr += chunk.toString();
|
|
299
|
+
});
|
|
300
|
+
child.once('error', (error) => {
|
|
301
|
+
finalize({
|
|
302
|
+
exitCode: 1,
|
|
303
|
+
stdout,
|
|
304
|
+
stderr: `${stderr}\n${error.message}`.trim()
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
child.once('close', (code) => {
|
|
308
|
+
finalize({
|
|
309
|
+
exitCode: typeof code === 'number' ? code : 1,
|
|
310
|
+
stdout,
|
|
311
|
+
stderr
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function compactError(...values) {
|
|
317
|
+
const text = values
|
|
318
|
+
.map((value) => value.trim())
|
|
319
|
+
.filter((value) => value.length > 0)
|
|
320
|
+
.join(' | ');
|
|
321
|
+
return text.length > 0 ? text : 'no stderr/stdout captured';
|
|
322
|
+
}
|
|
323
|
+
function shellEscape(value) {
|
|
324
|
+
if (/^[A-Za-z0-9_./:-]+$/u.test(value)) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
return `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
328
|
+
}
|
|
329
|
+
function redactArgsForDisplay(args) {
|
|
330
|
+
const redacted = [...args];
|
|
331
|
+
for (let index = 0; index < redacted.length - 1; index += 1) {
|
|
332
|
+
if (redacted[index] !== '--env') {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const envPair = redacted[index + 1];
|
|
336
|
+
if (!envPair) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const delimiter = envPair.indexOf('=');
|
|
340
|
+
if (delimiter <= 0) {
|
|
341
|
+
redacted[index + 1] = '<redacted>';
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const key = envPair.slice(0, delimiter);
|
|
345
|
+
redacted[index + 1] = `${key}=<redacted>`;
|
|
346
|
+
}
|
|
347
|
+
for (let index = 0; index < redacted.length; index += 1) {
|
|
348
|
+
const token = redacted[index] ?? '';
|
|
349
|
+
const longWithEquals = token.match(/^--([^=\s]+)=(.+)$/u);
|
|
350
|
+
if (longWithEquals
|
|
351
|
+
&& (looksSensitiveFlag(longWithEquals[1] ?? '') || looksSensitiveValue(longWithEquals[2] ?? ''))) {
|
|
352
|
+
redacted[index] = `--${longWithEquals[1]}=<redacted>`;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const longFlag = token.match(/^--([A-Za-z0-9_.-]+)$/u);
|
|
356
|
+
if (!longFlag) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const next = redacted[index + 1];
|
|
360
|
+
if (!next || next.startsWith('-')) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (!looksSensitiveFlag(longFlag[1] ?? '') && !looksSensitiveValue(next)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
redacted[index + 1] = '<redacted>';
|
|
367
|
+
}
|
|
368
|
+
// Command payload after "--" can contain arbitrary user args. Keep only the
|
|
369
|
+
// command token + first argument for operator context, redact the rest.
|
|
370
|
+
const separatorIndex = redacted.indexOf('--');
|
|
371
|
+
if (separatorIndex >= 0) {
|
|
372
|
+
const commandIndex = separatorIndex + 1;
|
|
373
|
+
if (commandIndex < redacted.length && looksSensitiveValue(redacted[commandIndex] ?? '')) {
|
|
374
|
+
redacted[commandIndex] = '<redacted>';
|
|
375
|
+
}
|
|
376
|
+
for (let index = separatorIndex + 2; index < redacted.length; index += 1) {
|
|
377
|
+
redacted[index] = '<redacted>';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return redacted;
|
|
381
|
+
}
|
|
382
|
+
function looksSensitiveFlag(flagName) {
|
|
383
|
+
const normalized = flagName.toLowerCase();
|
|
384
|
+
return /(api[-_]?key|token|secret|password|passwd|bearer|auth|cookie|credential)/u.test(normalized);
|
|
385
|
+
}
|
|
386
|
+
function looksSensitiveValue(value) {
|
|
387
|
+
const normalized = value.toLowerCase();
|
|
388
|
+
if (/(api[-_]?key|token|secret|password|passwd|bearer|auth|cookie|credential)/u.test(normalized)) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return /^[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^@\s]+@/iu.test(value);
|
|
392
|
+
}
|