Skip to content

Commit 84e5cd3

Browse files
authored
Merge pull request #21 from spectacular-voyage/testing-review
Testing review
2 parents ed87730 + b35aa4f commit 84e5cd3

63 files changed

Lines changed: 19306 additions & 9016 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
# Deno cache and coverage artifacts
55
.deno/
66
coverage/
7-
.coverage/
7+
.coverage
8+
.coverage.*
89
coverage.lcov
910
.test-tmp/
10-
.coverage-par/
1111

1212
# Editor and OS artifacts
1313
.DS_Store
1414
*.swp
1515

1616
# Claude Code working directory
17-
.claude
17+
.claude

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ code-assistant CLI,
1919

2020
## Compatibility
2121

22-
| Provider | VSCode | CLI | Stand-alone App | Web App |
23-
| --- | --- | --- | --- | --- |
24-
| Codex | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![X](https://img.shields.io/badge/-%E2%9C%95-d1242f?style=flat-square) |
22+
| Provider | VSCode | CLI | Local App | Web App |
23+
| ----------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
24+
| Codex | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![X](https://img.shields.io/badge/-%E2%9C%95-d1242f?style=flat-square) |
2525
| Claude Code | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![X](https://img.shields.io/badge/-%E2%9C%95-d1242f?style=flat-square) |
26-
| Gemini | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![N/A](https://img.shields.io/badge/N%2FA-Not%20available-9ea7b3?style=flat-square) | ![X](https://img.shields.io/badge/-%E2%9C%95-d1242f?style=flat-square) |
26+
| Gemini | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | ![Check](https://img.shields.io/badge/-%E2%9C%93-2ea043?style=flat-square) | N/A | ![X](https://img.shields.io/badge/-%E2%9C%95-d1242f?style=flat-square) |
2727

2828
`N/A` means the provider does not currently offer that interface.
2929

apps/cli/src/commands/status.ts

Lines changed: 22 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { join } from "@std/path";
44
import type { DaemonCliCommandContext } from "./context.ts";
55
import { isStatusSnapshotStale } from "@kato/runtime";
66
import { CLI_APP_VERSION } from "../version.ts";
7-
import type { RegisteredWorkspace } from "@kato/runtime";
87
import {
98
loadWorkspaceConfigOverrides,
109
readWorkspaceConfigWorkspaceId,
@@ -15,6 +14,14 @@ import {
1514
resolveStatusErrorCursorPath,
1615
saveSuppressedRecentErrorKeys,
1716
} from "./status_error_cursor.ts";
17+
import {
18+
loadWorkspaceStatusSummary,
19+
type WorkspaceStatusSummary,
20+
} from "./status_workspace.ts";
21+
export type {
22+
WorkspaceStatusRow,
23+
WorkspaceStatusSummary,
24+
} from "./status_workspace.ts";
1825

1926
const LIVE_REFRESH_MS = 2_000;
2027
const LIVE_SESSION_CAP = 5;
@@ -43,22 +50,6 @@ const ANSI_CSI_PATTERN = new RegExp(
4350
"g",
4451
);
4552

46-
export interface WorkspaceStatusRow {
47-
workspaceId: string;
48-
alias: string;
49-
workspaceRoot: string;
50-
configPath: string;
51-
valid: boolean;
52-
invalidReason?: string;
53-
}
54-
55-
export interface WorkspaceStatusSummary {
56-
activeCount: number;
57-
invalidCount: number;
58-
rows: WorkspaceStatusRow[];
59-
unavailableReason?: string;
60-
}
61-
6253
export interface StatusRecentError {
6354
timestamp: string;
6455
level: "warn" | "error";
@@ -171,92 +162,6 @@ function resolveTerminalWidth(): number {
171162
}
172163
}
173164

174-
function formatWorkspaceValidationError(error: unknown): string {
175-
if (error instanceof Deno.errors.NotFound) {
176-
return "config file not found";
177-
}
178-
if (error instanceof Deno.errors.PermissionDenied) {
179-
return "permission denied while reading config";
180-
}
181-
if (error instanceof Error && error.message.trim().length > 0) {
182-
return sanitizeInlineText(error.message);
183-
}
184-
return sanitizeInlineText(String(error));
185-
}
186-
187-
function toWorkspaceStatusRow(
188-
entry: RegisteredWorkspace,
189-
opts: { valid: boolean; invalidReason?: string },
190-
): WorkspaceStatusRow {
191-
return {
192-
workspaceId: entry.workspaceId,
193-
alias: entry.alias,
194-
workspaceRoot: entry.workspaceRoot,
195-
configPath: entry.configPath,
196-
valid: opts.valid,
197-
...(opts.invalidReason ? { invalidReason: opts.invalidReason } : {}),
198-
};
199-
}
200-
201-
async function validateWorkspaceEntry(
202-
entry: RegisteredWorkspace,
203-
): Promise<WorkspaceStatusRow> {
204-
try {
205-
await loadWorkspaceConfigOverrides(entry.configPath);
206-
const configuredWorkspaceId = await readWorkspaceConfigWorkspaceId(
207-
entry.configPath,
208-
{ allowMissing: true },
209-
);
210-
if (
211-
configuredWorkspaceId &&
212-
configuredWorkspaceId !== entry.workspaceId
213-
) {
214-
return toWorkspaceStatusRow(entry, {
215-
valid: false,
216-
invalidReason:
217-
`workspaceId mismatch (registry=${entry.workspaceId}, config=${configuredWorkspaceId})`,
218-
});
219-
}
220-
return toWorkspaceStatusRow(entry, { valid: true });
221-
} catch (error) {
222-
return toWorkspaceStatusRow(entry, {
223-
valid: false,
224-
invalidReason: formatWorkspaceValidationError(error),
225-
});
226-
}
227-
}
228-
229-
async function loadWorkspaceStatusSummary(
230-
ctx: DaemonCliCommandContext,
231-
): Promise<WorkspaceStatusSummary> {
232-
let entries: RegisteredWorkspace[];
233-
try {
234-
entries = await resolveWorkspaceRegistryStore(ctx).load();
235-
} catch (error) {
236-
return {
237-
activeCount: 0,
238-
invalidCount: 0,
239-
rows: [],
240-
unavailableReason: formatWorkspaceValidationError(error),
241-
};
242-
}
243-
244-
const rows = await Promise.all(
245-
entries.map((entry) => validateWorkspaceEntry(entry)),
246-
);
247-
rows.sort((a, b) =>
248-
a.alias.localeCompare(b.alias) ||
249-
a.workspaceId.localeCompare(b.workspaceId)
250-
);
251-
252-
const activeCount = rows.filter((row) => row.valid).length;
253-
return {
254-
activeCount,
255-
invalidCount: rows.length - activeCount,
256-
rows,
257-
};
258-
}
259-
260165
async function readTailText(
261166
filePath: string,
262167
maxBytes: number,
@@ -1003,7 +908,13 @@ async function runLiveMode(
1003908
now,
1004909
);
1005910
const [workspaceStatus, recentErrors] = await Promise.all([
1006-
loadWorkspaceStatusSummary(ctx),
911+
loadWorkspaceStatusSummary(
912+
() => resolveWorkspaceRegistryStore(ctx).load(),
913+
{
914+
loadWorkspaceConfigOverrides,
915+
readWorkspaceConfigWorkspaceId,
916+
},
917+
),
1007918
loadRecentStatusErrors(ctx),
1008919
]);
1009920
const stale = isStatusSnapshotStale(snapshot, now);
@@ -1090,9 +1001,13 @@ export async function runStatusCommand(
10901001
const statusErrorCursorPath = resolveStatusErrorCursorPath(
10911002
ctx.runtime.runtimeDir,
10921003
);
1093-
const workspaceStatus = asJson
1094-
? undefined
1095-
: await loadWorkspaceStatusSummary(ctx);
1004+
const workspaceStatus = asJson ? undefined : await loadWorkspaceStatusSummary(
1005+
() => resolveWorkspaceRegistryStore(ctx).load(),
1006+
{
1007+
loadWorkspaceConfigOverrides,
1008+
readWorkspaceConfigWorkspaceId,
1009+
},
1010+
);
10961011
const stale = isStatusSnapshotStale(snapshot, now);
10971012
const recentErrors = asJson ? undefined : await loadRecentStatusErrors(ctx);
10981013
const suppressedRecentErrorKeys = asJson
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { RegisteredWorkspace } from "@kato/runtime";
2+
3+
export interface WorkspaceStatusRow {
4+
workspaceId: string;
5+
alias: string;
6+
workspaceRoot: string;
7+
configPath: string;
8+
valid: boolean;
9+
invalidReason?: string;
10+
}
11+
12+
export interface WorkspaceStatusSummary {
13+
activeCount: number;
14+
invalidCount: number;
15+
rows: WorkspaceStatusRow[];
16+
unavailableReason?: string;
17+
}
18+
19+
function sanitizeInlineText(text: string): string {
20+
return text.replace(/\s+/g, " ").trim();
21+
}
22+
23+
function formatWorkspaceValidationError(error: unknown): string {
24+
if (error instanceof Deno.errors.NotFound) {
25+
return "config file not found";
26+
}
27+
if (error instanceof Deno.errors.PermissionDenied) {
28+
return "permission denied while reading config";
29+
}
30+
if (error instanceof Error && error.message.trim().length > 0) {
31+
return sanitizeInlineText(error.message);
32+
}
33+
return sanitizeInlineText(String(error));
34+
}
35+
36+
function formatWorkspaceRegistryValidationError(error: unknown): string {
37+
if (error instanceof Deno.errors.NotFound) {
38+
return "workspace registry file not found";
39+
}
40+
if (error instanceof Deno.errors.PermissionDenied) {
41+
return "permission denied while reading workspace registry";
42+
}
43+
if (error instanceof Error && error.message.trim().length > 0) {
44+
return `workspace registry load failed: ${
45+
sanitizeInlineText(error.message)
46+
}`;
47+
}
48+
return `workspace registry load failed: ${sanitizeInlineText(String(error))}`;
49+
}
50+
51+
function toWorkspaceStatusRow(
52+
entry: RegisteredWorkspace,
53+
opts: { valid: boolean; invalidReason?: string },
54+
): WorkspaceStatusRow {
55+
return {
56+
workspaceId: entry.workspaceId,
57+
alias: entry.alias,
58+
workspaceRoot: entry.workspaceRoot,
59+
configPath: entry.configPath,
60+
valid: opts.valid,
61+
...(opts.invalidReason ? { invalidReason: opts.invalidReason } : {}),
62+
};
63+
}
64+
65+
async function validateWorkspaceEntry(
66+
entry: RegisteredWorkspace,
67+
deps: {
68+
loadWorkspaceConfigOverrides: (configPath: string) => Promise<unknown>;
69+
readWorkspaceConfigWorkspaceId: (
70+
configPath: string,
71+
options: { allowMissing: boolean },
72+
) => Promise<string | undefined>;
73+
},
74+
): Promise<WorkspaceStatusRow> {
75+
try {
76+
await deps.loadWorkspaceConfigOverrides(entry.configPath);
77+
const configuredWorkspaceId = await deps.readWorkspaceConfigWorkspaceId(
78+
entry.configPath,
79+
{ allowMissing: true },
80+
);
81+
if (
82+
configuredWorkspaceId &&
83+
configuredWorkspaceId !== entry.workspaceId
84+
) {
85+
return toWorkspaceStatusRow(entry, {
86+
valid: false,
87+
invalidReason:
88+
`workspaceId mismatch (registry=${entry.workspaceId}, config=${configuredWorkspaceId})`,
89+
});
90+
}
91+
return toWorkspaceStatusRow(entry, { valid: true });
92+
} catch (error) {
93+
return toWorkspaceStatusRow(entry, {
94+
valid: false,
95+
invalidReason: formatWorkspaceValidationError(error),
96+
});
97+
}
98+
}
99+
100+
export async function buildWorkspaceStatusSummary(
101+
entries: RegisteredWorkspace[],
102+
deps: {
103+
loadWorkspaceConfigOverrides: (configPath: string) => Promise<unknown>;
104+
readWorkspaceConfigWorkspaceId: (
105+
configPath: string,
106+
options: { allowMissing: boolean },
107+
) => Promise<string | undefined>;
108+
},
109+
): Promise<WorkspaceStatusSummary> {
110+
const rows = await Promise.all(
111+
entries.map((entry) => validateWorkspaceEntry(entry, deps)),
112+
);
113+
rows.sort((a, b) =>
114+
a.alias.localeCompare(b.alias) ||
115+
a.workspaceId.localeCompare(b.workspaceId)
116+
);
117+
118+
const activeCount = rows.filter((row) => row.valid).length;
119+
return {
120+
activeCount,
121+
invalidCount: rows.length - activeCount,
122+
rows,
123+
};
124+
}
125+
126+
export async function loadWorkspaceStatusSummary(
127+
loadEntries: () => Promise<RegisteredWorkspace[]>,
128+
deps: {
129+
loadWorkspaceConfigOverrides: (configPath: string) => Promise<unknown>;
130+
readWorkspaceConfigWorkspaceId: (
131+
configPath: string,
132+
options: { allowMissing: boolean },
133+
) => Promise<string | undefined>;
134+
},
135+
): Promise<WorkspaceStatusSummary> {
136+
let entries: RegisteredWorkspace[];
137+
try {
138+
entries = await loadEntries();
139+
} catch (error) {
140+
return {
141+
activeCount: 0,
142+
invalidCount: 0,
143+
rows: [],
144+
unavailableReason: formatWorkspaceRegistryValidationError(error),
145+
};
146+
}
147+
148+
return await buildWorkspaceStatusSummary(entries, deps);
149+
}

0 commit comments

Comments
 (0)