Skip to content

Commit ef81e36

Browse files
committed
feat(cli,container): doctor spinner, mount list, auth fix, system prompt
- Add progress spinner to doctor command for better UX - Add v9fs, virtiofs, fuse.virtiofs to slow filesystem detection - Add mount list command with compose volume support and tests - Fix setup-auth.sh error suppression for gh api calls - Add default system prompt template
1 parent 7643c97 commit ef81e36

6 files changed

Lines changed: 584 additions & 2 deletions

File tree

cli/src/commands/doctor/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spinner } from "@clack/prompts";
12
import type { Command } from "commander";
23
import { checkGhAuth, checkGitUser } from "./checks/auth.js";
34
import {
@@ -25,6 +26,10 @@ export function registerDoctorCommand(parent: Command): void {
2526
.option("--only <category>", "Filter fixes: auth, env, git, volumes, wsl")
2627
.action(async (options) => {
2728
const workspaceRoot = process.env.WORKSPACE_ROOT || "/workspaces";
29+
const showSpinner = options.format !== "json";
30+
const s = showSpinner ? spinner() : null;
31+
32+
s?.start("Checking authentication and environment...");
2833

2934
// Run auth + environment checks in parallel
3035
const [authChecks, envChecks] = await Promise.all([
@@ -42,6 +47,8 @@ export function registerDoctorCommand(parent: Command): void {
4247
(c) => c.name === "Workspace filesystem" && c.status === "warn",
4348
);
4449

50+
s?.message("Scanning workspace for git repos and volume candidates...");
51+
4552
// Run git + volume + WSL checks (WSL checks return null when not WSL)
4653
const [gitCheck, volumeChecks, wslConfigCheck, defenderCheck] =
4754
await Promise.all([
@@ -51,6 +58,8 @@ export function registerDoctorCommand(parent: Command): void {
5158
checkDefenderExclusions(isWsl),
5259
]);
5360

61+
s?.stop("Checks complete");
62+
5463
// Assemble all checks, filtering out nulls from WSL checks
5564
const wslChecks = [wslConfigCheck, defenderCheck].filter(
5665
(c): c is CheckResult => c !== null,

cli/src/commands/doctor/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ export const SLOW_FS_TYPES = [
22
"smb",
33
"smb2",
44
"9p",
5+
"v9fs",
56
"drvfs",
67
"cifs",
78
"nfs",
89
"fuse.drvfs",
10+
"virtiofs",
11+
"fuse.virtiofs",
912
];
1013

1114
export async function spawn(

cli/src/commands/mount/list.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import type { Command } from "commander";
4+
import { readMountsJson } from "../doctor/mounts.js";
5+
6+
const TIP =
7+
"Tip: use 'codeforge mount add <path>' to register a directory for volume mounting.";
8+
9+
interface ComposeVolume {
10+
name: string;
11+
mountPath: string;
12+
}
13+
14+
function readComposeVolumes(workspaceRoot: string): ComposeVolume[] {
15+
const composePath = join(
16+
workspaceRoot,
17+
".devcontainer",
18+
"docker-compose.yml",
19+
);
20+
try {
21+
const content = readFileSync(composePath, "utf-8");
22+
const volumes: ComposeVolume[] = [];
23+
24+
// Match named volume mount lines: "- volume-name:/path"
25+
// Named volumes start with a letter (not . or / like bind mounts)
26+
const regex = /^\s+-\s+([a-zA-Z][a-zA-Z0-9_.-]*):(\/.+)$/gm;
27+
let match = regex.exec(content);
28+
while (match !== null) {
29+
volumes.push({ name: match[1], mountPath: match[2] });
30+
match = regex.exec(content);
31+
}
32+
33+
return volumes;
34+
} catch {
35+
return [];
36+
}
37+
}
38+
39+
interface DisplayRow {
40+
path: string;
41+
source: string;
42+
signal: string;
43+
added: string;
44+
}
45+
46+
export function registerMountListCommand(parent: Command): void {
47+
parent
48+
.command("list")
49+
.description("List configured volume mount directories")
50+
.option("--format <format>", "Output format: text or json", "text")
51+
.action(async (options) => {
52+
const workspaceRoot = process.env.WORKSPACE_ROOT || "/workspaces";
53+
const mountsPath = join(workspaceRoot, ".codeforge", "mounts.json");
54+
const mounts = await readMountsJson(mountsPath);
55+
const composeVolumes = readComposeVolumes(workspaceRoot);
56+
57+
if (options.format === "json") {
58+
const output = {
59+
...mounts,
60+
composeVolumes: composeVolumes.map((v) => ({
61+
path: v.mountPath,
62+
source: "compose" as const,
63+
volumeName: v.name,
64+
})),
65+
};
66+
console.log(JSON.stringify(output, null, "\t"));
67+
console.log("");
68+
console.log(TIP);
69+
return;
70+
}
71+
72+
const rows: DisplayRow[] = [
73+
...composeVolumes.map((v) => ({
74+
path: v.mountPath,
75+
source: "compose",
76+
signal: v.name,
77+
added: "—",
78+
})),
79+
...mounts.volumes.map((v) => ({
80+
path: v.path,
81+
source: v.source,
82+
signal: v.signal,
83+
added: v.added,
84+
})),
85+
];
86+
87+
if (rows.length === 0) {
88+
console.log("No mounts configured.");
89+
console.log("");
90+
console.log(TIP);
91+
return;
92+
}
93+
94+
// Column widths — compute dynamically from data
95+
const headers = {
96+
path: "PATH",
97+
source: "SOURCE",
98+
signal: "SIGNAL",
99+
added: "ADDED",
100+
};
101+
const widths = {
102+
path: Math.max(
103+
headers.path.length,
104+
...rows.map((r) => r.path.length),
105+
),
106+
source: Math.max(
107+
headers.source.length,
108+
...rows.map((r) => r.source.length),
109+
),
110+
signal: Math.max(
111+
headers.signal.length,
112+
...rows.map((r) => r.signal.length),
113+
),
114+
added: Math.max(
115+
headers.added.length,
116+
...rows.map((r) => r.added.length),
117+
),
118+
};
119+
120+
const row = (p: string, s: string, sig: string, a: string) =>
121+
`${p.padEnd(widths.path)} ${s.padEnd(widths.source)} ${sig.padEnd(widths.signal)} ${a}`;
122+
123+
console.log(
124+
row(headers.path, headers.source, headers.signal, headers.added),
125+
);
126+
for (const r of rows) {
127+
console.log(row(r.path, r.source, r.signal, r.added));
128+
}
129+
console.log("");
130+
console.log(TIP);
131+
});
132+
}

0 commit comments

Comments
 (0)