Skip to content

Commit 2319a61

Browse files
author
Harald Kirschner
committed
feat: implement scaffolding for agentrc.config.json and enhance workspace status view
1 parent 483f683 commit 2319a61

7 files changed

Lines changed: 255 additions & 35 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import path from "path";
2+
3+
import { safeWriteFile } from "../utils/fs";
4+
5+
import { detectWorkspaces } from "./analyzer";
6+
import type { Area } from "./analyzer";
7+
import type { AgentrcConfig, AgentrcConfigArea } from "./analyzer/config";
8+
export type ScaffoldConfigResult = {
9+
wrote: boolean;
10+
configPath: string;
11+
};
12+
13+
/**
14+
* Scaffolds agentrc.config.json from a list of detected areas.
15+
* Detects workspaces automatically and maps standalone areas.
16+
* Returns null if there are no areas to write (nothing to scaffold).
17+
*/
18+
export async function scaffoldAgentrcConfig(
19+
repoPath: string,
20+
areas: Area[],
21+
force = false
22+
): Promise<ScaffoldConfigResult | null> {
23+
if (areas.length === 0) return null;
24+
25+
const configPath = path.join(repoPath, "agentrc.config.json");
26+
const workspaces = await detectWorkspaces(repoPath, areas);
27+
28+
const workspacePaths = workspaces.map((ws) => ws.path + "/");
29+
30+
const standaloneAreas: AgentrcConfigArea[] = areas
31+
.filter((a) => {
32+
if (!a.path) return true;
33+
const rel = path.relative(repoPath, a.path).replace(/\\/gu, "/");
34+
return !workspacePaths.some((prefix) => rel.startsWith(prefix));
35+
})
36+
.map((a) => ({
37+
name: a.name,
38+
applyTo: a.applyTo,
39+
...(a.description ? { description: a.description } : {})
40+
}));
41+
42+
const agentrcConfig: AgentrcConfig = {};
43+
if (workspaces.length > 0) agentrcConfig.workspaces = workspaces;
44+
if (standaloneAreas.length > 0) agentrcConfig.areas = standaloneAreas;
45+
46+
if (!agentrcConfig.workspaces && !agentrcConfig.areas) return null;
47+
48+
const configContent = JSON.stringify(agentrcConfig, null, 2) + "\n";
49+
const { wrote } = await safeWriteFile(configPath, configContent, force);
50+
return { wrote, configPath };
51+
}

src/commands/init.ts

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from "path";
22

3-
import { analyzeRepo, detectWorkspaces } from "@agentrc/core/services/analyzer";
4-
import type { AgentrcConfig, AgentrcConfigArea } from "@agentrc/core/services/analyzer";
3+
import { analyzeRepo } from "@agentrc/core/services/analyzer";
54
import type {
65
AzureDevOpsOrg,
76
AzureDevOpsProject,
@@ -13,6 +12,7 @@ import {
1312
listProjects,
1413
listRepos
1514
} from "@agentrc/core/services/azureDevops";
15+
import { scaffoldAgentrcConfig } from "@agentrc/core/services/configScaffold";
1616
import type { FileAction } from "@agentrc/core/services/generator";
1717
import { generateConfigs } from "@agentrc/core/services/generator";
1818
import { buildAuthedUrl, cloneRepo, isGitRepo, setRemoteUrl } from "@agentrc/core/services/git";
@@ -232,36 +232,14 @@ export async function initCommand(
232232

233233
// Bootstrap agentrc.config.json with detected workspaces and standalone areas
234234
if (analysis.areas && analysis.areas.length > 0) {
235-
const configPath = path.join(repoPath, "agentrc.config.json");
236-
const workspaces = await detectWorkspaces(repoPath, analysis.areas);
237-
238-
// Areas already claimed by a workspace (match by path prefix, not name)
239-
const workspacePaths = workspaces.map((ws) => ws.path + "/");
240-
241-
// Standalone areas not inside any workspace
242-
const standaloneAreas: AgentrcConfigArea[] = analysis.areas
243-
.filter((a) => {
244-
if (!a.path) return true;
245-
const rel = path.relative(repoPath, a.path).replace(/\\/gu, "/");
246-
return !workspacePaths.some((prefix) => rel.startsWith(prefix));
247-
})
248-
.map((a) => ({
249-
name: a.name,
250-
applyTo: a.applyTo,
251-
...(a.description ? { description: a.description } : {})
252-
}));
253-
254-
const agentrcConfig: AgentrcConfig = {};
255-
if (workspaces.length > 0) agentrcConfig.workspaces = workspaces;
256-
if (standaloneAreas.length > 0) agentrcConfig.areas = standaloneAreas;
257-
258-
if (agentrcConfig.workspaces || agentrcConfig.areas) {
259-
const configContent = JSON.stringify(agentrcConfig, null, 2) + "\n";
260-
const { wrote } = await safeWriteFile(configPath, configContent, Boolean(options.force));
261-
const rel = path.relative(process.cwd(), configPath);
262-
allFiles.push({ path: rel, action: wrote ? "wrote" : "skipped" });
235+
const result = await scaffoldAgentrcConfig(repoPath, analysis.areas, Boolean(options.force));
236+
if (result) {
237+
const rel = path.relative(process.cwd(), result.configPath);
238+
allFiles.push({ path: rel, action: result.wrote ? "wrote" : "skipped" });
263239
if (shouldLog(options)) {
264-
process.stderr.write((wrote ? `Wrote ${rel}` : `Skipped ${rel} (exists)`) + "\n");
240+
process.stderr.write(
241+
(result.wrote ? `Wrote ${rel}` : `Skipped ${rel} (exists)`) + "\n"
242+
);
265243
}
266244
}
267245
}

src/ui/tui.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "path";
44
import type { RepoApp, Area } from "@agentrc/core/services/analyzer";
55
import { analyzeRepo, loadAgentrcConfig } from "@agentrc/core/services/analyzer";
66
import { getAzureDevOpsToken } from "@agentrc/core/services/azureDevops";
7+
import { scaffoldAgentrcConfig } from "@agentrc/core/services/configScaffold";
78
import { listCopilotModels } from "@agentrc/core/services/copilot";
89
import { generateEvalScaffold } from "@agentrc/core/services/evalScaffold";
910
import type { EvalConfig } from "@agentrc/core/services/evalScaffold";
@@ -184,6 +185,7 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
184185
const [modelPickTarget, setModelPickTarget] = useState<"eval" | "judge">("eval");
185186
const [modelCursor, setModelCursor] = useState(0);
186187
const [hasEvalConfig, setHasEvalConfig] = useState<boolean | null>(null);
188+
const [hasAgentrcConfig, setHasAgentrcConfig] = useState<boolean | null>(null);
187189
const [activityLog, setActivityLog] = useState<LogEntry[]>([]);
188190
const [generateTarget, setGenerateTarget] = useState<"copilot-instructions" | "agents-md">(
189191
"copilot-instructions"
@@ -219,12 +221,26 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
219221
setStatus("idle");
220222
};
221223

222-
// Check for eval config and repo structure on mount
224+
// Check for eval config, agentrc config, and repo structure on mount
223225
useEffect(() => {
224226
const configPath = path.join(repoPath, "agentrc.eval.json");
225227
fs.access(configPath)
226228
.then(() => setHasEvalConfig(true))
227229
.catch(() => setHasEvalConfig(false));
230+
const agentrcConfigCandidates = [
231+
path.join(repoPath, "agentrc.config.json"),
232+
path.join(repoPath, ".github", "agentrc.config.json")
233+
];
234+
Promise.all(
235+
agentrcConfigCandidates.map((p) =>
236+
fs
237+
.access(p)
238+
.then(() => true)
239+
.catch(() => false)
240+
)
241+
)
242+
.then((results) => setHasAgentrcConfig(results.some(Boolean)))
243+
.catch(() => setHasAgentrcConfig(false));
228244
analyzeRepo(repoPath)
229245
.then((analysis) => {
230246
const apps = analysis.apps ?? [];
@@ -233,6 +249,7 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
233249
setRepoAreas(analysis.areas ?? []);
234250
})
235251
.catch((err) => {
252+
setRepoAreas([]);
236253
addLog(`Repo analysis failed: ${err instanceof Error ? err.message : "unknown"}`, "error");
237254
});
238255
}, [repoPath]);
@@ -832,6 +849,46 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
832849
return;
833850
}
834851

852+
if (input.toLowerCase() === "c") {
853+
if (hasAgentrcConfig) {
854+
setMessage("agentrc.config.json already exists.");
855+
return;
856+
}
857+
if (repoAreas.length === 0) {
858+
setMessage("No areas detected. Cannot scaffold agentrc.config.json.");
859+
return;
860+
}
861+
setStatus("generating");
862+
setMessage("Creating agentrc.config.json...");
863+
addLog("Scaffolding agentrc.config.json...", "progress");
864+
try {
865+
const result = await scaffoldAgentrcConfig(repoPath, repoAreas);
866+
if (!result) {
867+
setStatus("idle");
868+
setMessage("Nothing to scaffold — no areas or workspaces detected.");
869+
addLog("No areas to scaffold into config.", "info");
870+
} else if (result.wrote) {
871+
setHasAgentrcConfig(true);
872+
setStatus("done");
873+
const msg = `Created agentrc.config.json`;
874+
setMessage(msg);
875+
addLog(msg, "success");
876+
} else {
877+
setHasAgentrcConfig(true);
878+
setStatus("idle");
879+
setMessage("agentrc.config.json already exists (skipped).");
880+
addLog("agentrc.config.json already exists.", "info");
881+
}
882+
} catch (error) {
883+
setStatus("error");
884+
const msg =
885+
error instanceof Error ? error.message : "Failed to create config.";
886+
setMessage(msg);
887+
addLog(msg, "error");
888+
}
889+
return;
890+
}
891+
835892
if (input.toLowerCase() === "m") {
836893
if (hideModelPicker) {
837894
setMessage(
@@ -1011,6 +1068,18 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
10111068
<Text color="yellow">no eval config — press [I] to create</Text>
10121069
)}
10131070
</Text>
1071+
<Text>
1072+
<Text color="gray">Config </Text>
1073+
{hasAgentrcConfig === null ? (
1074+
<Text color="gray" dimColor>
1075+
checking...
1076+
</Text>
1077+
) : hasAgentrcConfig ? (
1078+
<Text color="green">agentrc.config.json found</Text>
1079+
) : (
1080+
<Text color="yellow">no config — press [C] to create</Text>
1081+
)}
1082+
</Text>
10141083
</Box>
10151084

10161085
{/* Activity */}
@@ -1314,6 +1383,7 @@ export function AgentRCTui({ repoPath, skipAnimation = false }: Props): React.JS
13141383
<Box>
13151384
<KeyHint k="M" label="Model" />
13161385
<KeyHint k="J" label="Judge" />
1386+
{!hasAgentrcConfig && <KeyHint k="C" label="Create config" />}
13171387
<KeyHint k="Q" label="Quit" />
13181388
</Box>
13191389
</Box>

vscode-extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
},
9595
"views": {
9696
"agentrc": [
97+
{
98+
"id": "agentrc.workspace",
99+
"name": "Workspace",
100+
"contextualTitle": "AgentRC"
101+
},
97102
{
98103
"id": "agentrc.analysis",
99104
"name": "Analysis",
@@ -107,6 +112,11 @@
107112
]
108113
},
109114
"viewsWelcome": [
115+
{
116+
"view": "agentrc.workspace",
117+
"contents": "Open a folder to get started.\n[$(folder-opened) Open Folder](command:vscode.openFolder)",
118+
"when": "workspaceFolderCount == 0"
119+
},
110120
{
111121
"view": "agentrc.analysis",
112122
"contents": "Discover languages, frameworks, and project structure in your repository.\n[$(search) Analyze Repository](command:agentrc.analyze)\nNew to AgentRC? [Get Started](command:workbench.action.openWalkthrough?%5B%22microsoft.agentrc-vscode%23agentrc.gettingStarted%22%5D)",

vscode-extension/src/extension.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { evalCommand, evalInitCommand } from "./commands/eval.js";
77
import { initCommand } from "./commands/init.js";
88
import { prCommand } from "./commands/pr.js";
99
import { batchInstructionsCommand } from "./commands/batch.js";
10-
import { analysisTreeProvider, readinessTreeProvider } from "./views/providers.js";
10+
import { analysisTreeProvider, readinessTreeProvider, workspaceStatusTreeProvider } from "./views/providers.js";
1111

1212
export function activate(context: vscode.ExtensionContext): void {
1313
// Status bar — only show after analysis
@@ -18,13 +18,16 @@ export function activate(context: vscode.ExtensionContext): void {
1818
context.subscriptions.push(statusBar);
1919

2020
// Tree views (createTreeView for description/badge support)
21+
const workspaceView = vscode.window.createTreeView("agentrc.workspace", {
22+
treeDataProvider: workspaceStatusTreeProvider
23+
});
2124
const analysisView = vscode.window.createTreeView("agentrc.analysis", {
2225
treeDataProvider: analysisTreeProvider
2326
});
2427
const readinessView = vscode.window.createTreeView("agentrc.readiness", {
2528
treeDataProvider: readinessTreeProvider
2629
});
27-
context.subscriptions.push(analysisView, readinessView);
30+
context.subscriptions.push(workspaceView, analysisView, readinessView);
2831

2932
function updateAnalysisView(): void {
3033
const analysis = getCachedAnalysis();
@@ -74,10 +77,14 @@ export function activate(context: vscode.ExtensionContext): void {
7477
updateStatusBar();
7578
}),
7679
vscode.commands.registerCommand("agentrc.eval", evalCommand),
77-
vscode.commands.registerCommand("agentrc.evalInit", evalInitCommand),
80+
vscode.commands.registerCommand("agentrc.evalInit", async () => {
81+
await evalInitCommand();
82+
workspaceStatusTreeProvider.refresh();
83+
}),
7884
vscode.commands.registerCommand("agentrc.init", async () => {
7985
await initCommand();
8086
analysisTreeProvider.refresh();
87+
workspaceStatusTreeProvider.refresh();
8188
updateAnalysisView();
8289
updateStatusBar();
8390
vscode.commands.executeCommand("agentrc.analysis.focus");

0 commit comments

Comments
 (0)