Skip to content

Commit 9aae6b5

Browse files
refactor(cli): extract config command
Refs #262
1 parent ce03efa commit 9aae6b5

3 files changed

Lines changed: 168 additions & 161 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { resolve } from "node:path";
2+
import { loadCodeDecayConfig } from "@submuxhq/codedecay-config";
3+
import { write } from "../io";
4+
import { parseConfigArgs } from "../parsers/args";
5+
import { renderConfig } from "../renderers/config";
6+
import type { CliCommandContext } from "../types";
7+
8+
export function runConfigCommand(context: CliCommandContext): void {
9+
const options = parseConfigArgs(context.args);
10+
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
11+
const loadedConfig = loadCodeDecayConfig({ cwd });
12+
write(context.runtime.stdout, renderConfig(loadedConfig, options.format));
13+
}

packages/cli/src/index.ts

Lines changed: 1 addition & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { loadCodeDecaySkills } from "@submuxhq/codedecay-skills";
5353
import { createTestProofAudit } from "@submuxhq/codedecay-test-audit";
5454
import { createConfiguredToolHarnesses } from "@submuxhq/codedecay-tool-adapters";
5555
import YAML from "yaml";
56+
import { runConfigCommand } from "./commands/config";
5657
import { runUninstallCommand, runUpdateCommand, runVersionCommand } from "./commands/maintenance";
5758
import {
5859
runMemoryCommand as runMemoryCommandWithDependencies,
@@ -67,7 +68,6 @@ import {
6768
HelpRequested,
6869
parseAgentArgs,
6970
parseAnalyzeArgs,
70-
parseConfigArgs,
7171
parseDashboardArgs,
7272
parseDifferentialArgs,
7373
parseExecuteArgs,
@@ -85,7 +85,6 @@ import type {
8585
CliCommandHandler,
8686
CliRuntime,
8787
ConfigFormat,
88-
ConfigOptions,
8988
DashboardOptions,
9089
DifferentialOptions,
9190
DifferentialProbeResult,
@@ -258,13 +257,6 @@ async function run(args: string[], runtime: CliRuntime): Promise<void | number>
258257
});
259258
}
260259

261-
function runConfigCommand(context: CliCommandContext): void {
262-
const options = parseConfigArgs(context.args);
263-
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
264-
const loadedConfig = loadCodeDecayConfig({ cwd });
265-
write(context.runtime.stdout, renderConfig(loadedConfig, options.format));
266-
}
267-
268260
async function runMcpCommand(context: CliCommandContext): Promise<void> {
269261
const options = parseMcpArgs(context.args);
270262
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
@@ -1134,14 +1126,6 @@ function writeCliOutput(input: {
11341126
write(input.runtime.stdout, input.rendered);
11351127
}
11361128

1137-
function renderConfig(loadedConfig: LoadedCodeDecayConfig, format: ConfigFormat): string {
1138-
if (format === "markdown") {
1139-
return renderConfigMarkdown(loadedConfig);
1140-
}
1141-
1142-
return `${JSON.stringify(loadedConfig, null, 2)}\n`;
1143-
}
1144-
11451129
function renderLlmReviewReport(report: LlmReviewReport, format: ConfigFormat): string {
11461130
if (format === "json") {
11471131
return `${JSON.stringify(report, null, 2)}\n`;
@@ -5386,150 +5370,6 @@ function formatDifferentialStatus(status: DifferentialStatus): string {
53865370
return `${status.charAt(0).toUpperCase()}${status.slice(1)}`;
53875371
}
53885372

5389-
function renderConfigMarkdown(loadedConfig: LoadedCodeDecayConfig): string {
5390-
const { config, sourcePath } = loadedConfig;
5391-
const lines = [
5392-
"## CodeDecay Config",
5393-
"",
5394-
`**Source:** ${sourcePath ? `\`${sourcePath}\`` : "defaults (no config file found)"}`,
5395-
"",
5396-
"### Safety",
5397-
"",
5398-
"| Setting | Value |",
5399-
"| --- | ---: |",
5400-
`| Command timeout | ${config.safety.commandTimeoutMs}ms |`,
5401-
`| Allow configured commands | ${config.safety.allowCommands ? "yes" : "no"} |`,
5402-
"",
5403-
"### Commands",
5404-
"",
5405-
"| Type | Commands |",
5406-
"| --- | --- |",
5407-
`| Test | ${formatCommandList(config.commands.test)} |`,
5408-
`| Build | ${formatCommandList(config.commands.build)} |`,
5409-
`| Start | ${formatCommandList(config.commands.start)} |`,
5410-
"",
5411-
"### LLM",
5412-
"",
5413-
"| Setting | Value |",
5414-
"| --- | --- |",
5415-
`| Provider | ${config.llm.provider} |`,
5416-
`| Model | ${config.llm.model ? `\`${config.llm.model}\`` : "none"} |`,
5417-
`| Endpoint | ${config.llm.endpoint ? `\`${config.llm.endpoint}\`` : "none"} |`,
5418-
`| API key env | ${config.llm.apiKeyEnv ? `\`${config.llm.apiKeyEnv}\`` : "none"} |`,
5419-
`| Timeout | ${config.llm.timeoutMs}ms |`,
5420-
"",
5421-
"### Tool Adapters",
5422-
""
5423-
];
5424-
5425-
appendConfigToolAdapters(lines, config.toolAdapters);
5426-
5427-
lines.push("### Product Testing Targets", "");
5428-
appendConfigProductTargets(lines, config.productTesting.targets);
5429-
5430-
lines.push(
5431-
"### Probes",
5432-
""
5433-
);
5434-
5435-
if (config.probes.length === 0) {
5436-
lines.push("No probes configured.", "");
5437-
return `${lines.join("\n")}\n`;
5438-
}
5439-
5440-
lines.push("| Name | Command | Timeout |", "| --- | --- | ---: |");
5441-
for (const probe of config.probes) {
5442-
lines.push(
5443-
`| ${probe.name} | \`${probe.command}\` | ${probe.timeoutMs ? `${probe.timeoutMs}ms` : "default"} |`
5444-
);
5445-
}
5446-
lines.push("");
5447-
5448-
return `${lines.join("\n")}\n`;
5449-
}
5450-
5451-
function appendConfigToolAdapters(
5452-
lines: string[],
5453-
toolAdapters: LoadedCodeDecayConfig["config"]["toolAdapters"]
5454-
): void {
5455-
const rows = [
5456-
formatConfigToolAdapter("Agent Process", toolAdapters.agentProcess),
5457-
formatConfigToolAdapter("Playwright", toolAdapters.playwright),
5458-
formatConfigToolAdapter("StrykerJS", toolAdapters.stryker),
5459-
formatConfigToolAdapter("Schemathesis", toolAdapters.schemathesis),
5460-
formatConfigToolAdapter("Pact", toolAdapters.pact),
5461-
formatConfigToolAdapter("Semgrep", toolAdapters.semgrep),
5462-
formatConfigToolAdapter("Coverage", toolAdapters.coverage)
5463-
].filter((row): row is string => row !== undefined);
5464-
5465-
if (rows.length === 0) {
5466-
lines.push("No tool adapters configured.", "");
5467-
return;
5468-
}
5469-
5470-
lines.push("| Adapter | Enabled | Command/details | Timeout |", "| --- | --- | --- | ---: |", ...rows, "");
5471-
}
5472-
5473-
function formatConfigToolAdapter(
5474-
name: string,
5475-
adapter: LoadedCodeDecayConfig["config"]["toolAdapters"][keyof LoadedCodeDecayConfig["config"]["toolAdapters"]]
5476-
): string | undefined {
5477-
if (!adapter) {
5478-
return undefined;
5479-
}
5480-
5481-
const details = [
5482-
adapter.command ? `command: \`${adapter.command}\`` : "command: default",
5483-
"reportPath" in adapter && adapter.reportPath ? `reportPath: \`${adapter.reportPath}\`` : undefined,
5484-
"schema" in adapter && adapter.schema ? `schema: \`${adapter.schema}\`` : undefined,
5485-
"baseUrl" in adapter && adapter.baseUrl ? `baseUrl: \`${adapter.baseUrl}\`` : undefined,
5486-
"config" in adapter && adapter.config ? `config: \`${adapter.config}\`` : undefined,
5487-
"failOnSeverity" in adapter && adapter.failOnSeverity ? `failOnSeverity: ${adapter.failOnSeverity}` : undefined,
5488-
"profile" in adapter && adapter.profile ? `profile: ${adapter.profile}` : undefined,
5489-
"bundleFormat" in adapter && adapter.bundleFormat ? `bundleFormat: ${adapter.bundleFormat}` : undefined,
5490-
"reportPaths" in adapter && adapter.reportPaths ? `reportPaths: \`${adapter.reportPaths.join(", ")}\`` : undefined,
5491-
"failOn" in adapter && adapter.failOn ? `failOn: ${adapter.failOn}` : undefined
5492-
]
5493-
.filter((item): item is string => item !== undefined)
5494-
.join("<br>");
5495-
5496-
return `| ${name} | ${adapter.enabled ? "yes" : "no"} | ${details} | ${adapter.timeoutMs ? `${adapter.timeoutMs}ms` : "default"} |`;
5497-
}
5498-
5499-
function appendConfigProductTargets(
5500-
lines: string[],
5501-
targets: LoadedCodeDecayConfig["config"]["productTesting"]["targets"]
5502-
): void {
5503-
const entries = Object.values(targets);
5504-
if (entries.length === 0) {
5505-
lines.push("No product testing targets configured.", "");
5506-
return;
5507-
}
5508-
5509-
lines.push(
5510-
"| Target | Readiness | Effective URL | Commands | Health check | API endpoints | Timeout |",
5511-
"| --- | --- | --- | --- | --- | ---: | ---: |"
5512-
);
5513-
for (const target of entries) {
5514-
const effectiveUrl = target.readiness.effectiveBaseUrl ? `\`${target.readiness.effectiveBaseUrl}\`` : "none";
5515-
const commands = target.readiness.commandsRequired.length > 0
5516-
? target.readiness.commandsRequired.map((command) => `\`${command}\``).join("<br>")
5517-
: "none";
5518-
lines.push(
5519-
`| ${target.id} | ${target.readiness.status} (${target.readiness.mode}) | ${effectiveUrl} | ${commands} | ${target.healthCheck ? `\`${target.healthCheck}\`` : "none"} | ${target.apiEndpoints.length} | ${target.timeoutMs}ms |`
5520-
);
5521-
}
5522-
lines.push("", "Config inspection does not execute product target commands.", "");
5523-
}
5524-
5525-
function formatCommandList(commands: string[]): string {
5526-
if (commands.length === 0) {
5527-
return "none";
5528-
}
5529-
5530-
return commands.map((command) => `\`${command}\``).join("<br>");
5531-
}
5532-
55335373
function getRepoRootForCli(cwd: string, options: { base?: string | undefined; head?: string | undefined; format: string }): string {
55345374
try {
55355375
return getRepoRoot(cwd);
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { LoadedCodeDecayConfig } from "@submuxhq/codedecay-config";
2+
import type { ConfigFormat } from "../types";
3+
4+
export function renderConfig(loadedConfig: LoadedCodeDecayConfig, format: ConfigFormat): string {
5+
if (format === "markdown") {
6+
return renderConfigMarkdown(loadedConfig);
7+
}
8+
9+
return `${JSON.stringify(loadedConfig, null, 2)}\n`;
10+
}
11+
12+
function renderConfigMarkdown(loadedConfig: LoadedCodeDecayConfig): string {
13+
const { config, sourcePath } = loadedConfig;
14+
const lines = [
15+
"## CodeDecay Config",
16+
"",
17+
`**Source:** ${sourcePath ? `\`${sourcePath}\`` : "defaults (no config file found)"}`,
18+
"",
19+
"### Safety",
20+
"",
21+
"| Setting | Value |",
22+
"| --- | ---: |",
23+
`| Command timeout | ${config.safety.commandTimeoutMs}ms |`,
24+
`| Allow configured commands | ${config.safety.allowCommands ? "yes" : "no"} |`,
25+
"",
26+
"### Commands",
27+
"",
28+
"| Type | Commands |",
29+
"| --- | --- |",
30+
`| Test | ${formatCommandList(config.commands.test)} |`,
31+
`| Build | ${formatCommandList(config.commands.build)} |`,
32+
`| Start | ${formatCommandList(config.commands.start)} |`,
33+
"",
34+
"### LLM",
35+
"",
36+
"| Setting | Value |",
37+
"| --- | --- |",
38+
`| Provider | ${config.llm.provider} |`,
39+
`| Model | ${config.llm.model ? `\`${config.llm.model}\`` : "none"} |`,
40+
`| Endpoint | ${config.llm.endpoint ? `\`${config.llm.endpoint}\`` : "none"} |`,
41+
`| API key env | ${config.llm.apiKeyEnv ? `\`${config.llm.apiKeyEnv}\`` : "none"} |`,
42+
`| Timeout | ${config.llm.timeoutMs}ms |`,
43+
"",
44+
"### Tool Adapters",
45+
""
46+
];
47+
48+
appendConfigToolAdapters(lines, config.toolAdapters);
49+
50+
lines.push("### Product Testing Targets", "");
51+
appendConfigProductTargets(lines, config.productTesting.targets);
52+
53+
lines.push(
54+
"### Probes",
55+
""
56+
);
57+
58+
if (config.probes.length === 0) {
59+
lines.push("No probes configured.", "");
60+
return `${lines.join("\n")}\n`;
61+
}
62+
63+
lines.push("| Name | Command | Timeout |", "| --- | --- | ---: |");
64+
for (const probe of config.probes) {
65+
lines.push(
66+
`| ${probe.name} | \`${probe.command}\` | ${probe.timeoutMs ? `${probe.timeoutMs}ms` : "default"} |`
67+
);
68+
}
69+
lines.push("");
70+
71+
return `${lines.join("\n")}\n`;
72+
}
73+
74+
function appendConfigToolAdapters(
75+
lines: string[],
76+
toolAdapters: LoadedCodeDecayConfig["config"]["toolAdapters"]
77+
): void {
78+
const rows = [
79+
formatConfigToolAdapter("Agent Process", toolAdapters.agentProcess),
80+
formatConfigToolAdapter("Playwright", toolAdapters.playwright),
81+
formatConfigToolAdapter("StrykerJS", toolAdapters.stryker),
82+
formatConfigToolAdapter("Schemathesis", toolAdapters.schemathesis),
83+
formatConfigToolAdapter("Pact", toolAdapters.pact),
84+
formatConfigToolAdapter("Semgrep", toolAdapters.semgrep),
85+
formatConfigToolAdapter("Coverage", toolAdapters.coverage)
86+
].filter((row): row is string => row !== undefined);
87+
88+
if (rows.length === 0) {
89+
lines.push("No tool adapters configured.", "");
90+
return;
91+
}
92+
93+
lines.push("| Adapter | Enabled | Command/details | Timeout |", "| --- | --- | --- | ---: |", ...rows, "");
94+
}
95+
96+
function formatConfigToolAdapter(
97+
name: string,
98+
adapter: LoadedCodeDecayConfig["config"]["toolAdapters"][keyof LoadedCodeDecayConfig["config"]["toolAdapters"]]
99+
): string | undefined {
100+
if (!adapter) {
101+
return undefined;
102+
}
103+
104+
const details = [
105+
adapter.command ? `command: \`${adapter.command}\`` : "command: default",
106+
"reportPath" in adapter && adapter.reportPath ? `reportPath: \`${adapter.reportPath}\`` : undefined,
107+
"schema" in adapter && adapter.schema ? `schema: \`${adapter.schema}\`` : undefined,
108+
"baseUrl" in adapter && adapter.baseUrl ? `baseUrl: \`${adapter.baseUrl}\`` : undefined,
109+
"config" in adapter && adapter.config ? `config: \`${adapter.config}\`` : undefined,
110+
"failOnSeverity" in adapter && adapter.failOnSeverity ? `failOnSeverity: ${adapter.failOnSeverity}` : undefined,
111+
"profile" in adapter && adapter.profile ? `profile: ${adapter.profile}` : undefined,
112+
"bundleFormat" in adapter && adapter.bundleFormat ? `bundleFormat: ${adapter.bundleFormat}` : undefined,
113+
"reportPaths" in adapter && adapter.reportPaths ? `reportPaths: \`${adapter.reportPaths.join(", ")}\`` : undefined,
114+
"failOn" in adapter && adapter.failOn ? `failOn: ${adapter.failOn}` : undefined
115+
]
116+
.filter((item): item is string => item !== undefined)
117+
.join("<br>");
118+
119+
return `| ${name} | ${adapter.enabled ? "yes" : "no"} | ${details} | ${adapter.timeoutMs ? `${adapter.timeoutMs}ms` : "default"} |`;
120+
}
121+
122+
function appendConfigProductTargets(
123+
lines: string[],
124+
targets: LoadedCodeDecayConfig["config"]["productTesting"]["targets"]
125+
): void {
126+
const entries = Object.values(targets);
127+
if (entries.length === 0) {
128+
lines.push("No product testing targets configured.", "");
129+
return;
130+
}
131+
132+
lines.push(
133+
"| Target | Readiness | Effective URL | Commands | Health check | API endpoints | Timeout |",
134+
"| --- | --- | --- | --- | --- | ---: | ---: |"
135+
);
136+
for (const target of entries) {
137+
const effectiveUrl = target.readiness.effectiveBaseUrl ? `\`${target.readiness.effectiveBaseUrl}\`` : "none";
138+
const commands = target.readiness.commandsRequired.length > 0
139+
? target.readiness.commandsRequired.map((command) => `\`${command}\``).join("<br>")
140+
: "none";
141+
lines.push(
142+
`| ${target.id} | ${target.readiness.status} (${target.readiness.mode}) | ${effectiveUrl} | ${commands} | ${target.healthCheck ? `\`${target.healthCheck}\`` : "none"} | ${target.apiEndpoints.length} | ${target.timeoutMs}ms |`
143+
);
144+
}
145+
lines.push("", "Config inspection does not execute product target commands.", "");
146+
}
147+
148+
function formatCommandList(commands: string[]): string {
149+
if (commands.length === 0) {
150+
return "none";
151+
}
152+
153+
return commands.map((command) => `\`${command}\``).join("<br>");
154+
}

0 commit comments

Comments
 (0)