aidevops 3.21.12 → 3.22.0
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 +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +3 -2
- package/bun.lock +573 -0
- package/package.json +12 -1
- package/packages/gui-api/package.json +9 -0
- package/packages/gui-api/src/app.ts +49 -0
- package/packages/gui-api/src/file-adapter.ts +207 -0
- package/packages/gui-api/src/server.ts +12 -0
- package/packages/gui-api/src/status-adapter-utils.ts +108 -0
- package/packages/gui-api/src/status-adapter.ts +484 -0
- package/packages/gui-api/src/status-local-repos.ts +239 -0
- package/packages/gui-desktop/package.json +6 -0
- package/packages/gui-desktop/scripts/install-macos-app.sh +1020 -0
- package/packages/gui-shared/package.json +9 -0
- package/packages/gui-shared/src/contracts.ts +341 -0
- package/packages/gui-shared/src/fixtures.ts +250 -0
- package/packages/gui-shared/src/index.ts +2 -0
- package/packages/gui-web/index.html +24 -0
- package/packages/gui-web/package.json +25 -0
- package/packages/gui-web/src/App.tsx +269 -0
- package/packages/gui-web/src/AppNavigation.tsx +379 -0
- package/packages/gui-web/src/AppWorkspace.tsx +114 -0
- package/packages/gui-web/src/FileExplorerSurface.tsx +153 -0
- package/packages/gui-web/src/InventorySurfaces.tsx +117 -0
- package/packages/gui-web/src/PathActions.tsx +17 -0
- package/packages/gui-web/src/StatusSurfaces.tsx +396 -0
- package/packages/gui-web/src/app-model.ts +546 -0
- package/packages/gui-web/src/dashboard.ts +79 -0
- package/packages/gui-web/src/main.tsx +24 -0
- package/packages/gui-web/src/status-client.ts +93 -0
- package/packages/gui-web/src/styles.css +1540 -0
- package/packages/gui-web/vite.config.ts +14 -0
- package/setup.sh +78 -3
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { hostname, networkInterfaces, userInfo } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
assertNoSecretSentinels,
|
|
7
|
+
createEnvelope,
|
|
8
|
+
type GuiAiAppSummary,
|
|
9
|
+
type GuiAiProviderId,
|
|
10
|
+
type GuiOAuthPoolSummary,
|
|
11
|
+
type GuiOAuthProviderSummary,
|
|
12
|
+
type GuiRepoRegistrySummary,
|
|
13
|
+
type GuiRepoSummary,
|
|
14
|
+
type GuiResponseEnvelope,
|
|
15
|
+
type GuiSettingsSummary,
|
|
16
|
+
type GuiSetupTargetSummary,
|
|
17
|
+
type GuiStatusData,
|
|
18
|
+
statusFixture,
|
|
19
|
+
} from "../../gui-shared/src";
|
|
20
|
+
import {
|
|
21
|
+
collapseHome,
|
|
22
|
+
cooldownReady,
|
|
23
|
+
expandHome,
|
|
24
|
+
formatEpochField,
|
|
25
|
+
formatNullableEpochField,
|
|
26
|
+
isRecord,
|
|
27
|
+
numberField,
|
|
28
|
+
readJsonObject,
|
|
29
|
+
readOptionalText,
|
|
30
|
+
stringField,
|
|
31
|
+
} from "./status-adapter-utils";
|
|
32
|
+
import { readLocalReposSetupSummary } from "./status-local-repos";
|
|
33
|
+
|
|
34
|
+
export interface StatusAdapterOptions {
|
|
35
|
+
repoRoot?: string;
|
|
36
|
+
observedAt?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const STATUS_ADAPTER_COMMAND = ["aidevops", "status"] as const;
|
|
40
|
+
|
|
41
|
+
export function readStatus(
|
|
42
|
+
options: StatusAdapterOptions = {},
|
|
43
|
+
): GuiResponseEnvelope<GuiStatusData> {
|
|
44
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
45
|
+
const versionPath = join(repoRoot, "VERSION");
|
|
46
|
+
const settingsPathRef = "~/.config/aidevops/settings.json";
|
|
47
|
+
const reposPathRef = "~/.config/aidevops/repos.json";
|
|
48
|
+
const oauthPoolPathRef = "~/.aidevops/oauth-pool.json";
|
|
49
|
+
const agentsPathRef = "~/.aidevops/agents";
|
|
50
|
+
const settingsPath = expandHome(settingsPathRef);
|
|
51
|
+
const reposPath = expandHome(reposPathRef);
|
|
52
|
+
const oauthPoolPath = expandHome(oauthPoolPathRef);
|
|
53
|
+
const aidevopsVersion = existsSync(versionPath)
|
|
54
|
+
? readFileSync(versionPath, "utf8").trim()
|
|
55
|
+
: statusFixture.aidevops_version;
|
|
56
|
+
const installedVersion = readOptionalText(expandHome("~/.aidevops/agents/VERSION")) ?? aidevopsVersion;
|
|
57
|
+
const restartRequired = installedVersion !== aidevopsVersion;
|
|
58
|
+
const setupTargets = readSetupTargets(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
59
|
+
const aiApps = readAiApps(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
60
|
+
const sourcePathRefs = [
|
|
61
|
+
agentsPathRef,
|
|
62
|
+
settingsPathRef,
|
|
63
|
+
reposPathRef,
|
|
64
|
+
oauthPoolPathRef,
|
|
65
|
+
"VERSION",
|
|
66
|
+
...setupTargets.map((target) => target.path_ref),
|
|
67
|
+
...aiApps.flatMap((app) => [app.app_path_ref, app.binary_path_ref, app.config_path_ref, app.aidevops_target_path_ref]),
|
|
68
|
+
].filter(isSourcePathRef);
|
|
69
|
+
|
|
70
|
+
const data: GuiStatusData = {
|
|
71
|
+
...statusFixture,
|
|
72
|
+
aidevops_version: aidevopsVersion || "unknown",
|
|
73
|
+
update: {
|
|
74
|
+
running_version: aidevopsVersion || "unknown",
|
|
75
|
+
installed_version: installedVersion || "unknown",
|
|
76
|
+
restart_required: restartRequired,
|
|
77
|
+
message: restartRequired
|
|
78
|
+
? "aidevops has updated in the background. Restart the GUI app to use the latest installed version."
|
|
79
|
+
: "The GUI app is using the latest installed aidevops version.",
|
|
80
|
+
},
|
|
81
|
+
machine: readMachineSummary(),
|
|
82
|
+
paths: [
|
|
83
|
+
{
|
|
84
|
+
label: "deployed agents",
|
|
85
|
+
path_ref: agentsPathRef,
|
|
86
|
+
health: existsSync(expandHome("~/.aidevops/agents")) ? "present" : "missing",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: "settings",
|
|
90
|
+
path_ref: settingsPathRef,
|
|
91
|
+
health: existsSync(settingsPath)
|
|
92
|
+
? "present"
|
|
93
|
+
: "missing",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: "repo registry",
|
|
97
|
+
path_ref: reposPathRef,
|
|
98
|
+
health: existsSync(reposPath) ? "present" : "missing",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
helper_availability: [
|
|
102
|
+
{
|
|
103
|
+
name: STATUS_ADAPTER_COMMAND.join(" "),
|
|
104
|
+
status: "unchecked",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
settings: readSettingsSummary(settingsPath, settingsPathRef),
|
|
108
|
+
repos: readRepoRegistrySummary(reposPath, reposPathRef),
|
|
109
|
+
local_repos: readLocalReposSetupSummary(reposPath),
|
|
110
|
+
oauth_pool: readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef),
|
|
111
|
+
setup_targets: setupTargets,
|
|
112
|
+
ai_apps: aiApps,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const envelope = createEnvelope({
|
|
116
|
+
operation_id: "setup.status.read",
|
|
117
|
+
source: {
|
|
118
|
+
surface: "setup",
|
|
119
|
+
authority: "aidevops helpers",
|
|
120
|
+
path_refs: sourcePathRefs,
|
|
121
|
+
},
|
|
122
|
+
data,
|
|
123
|
+
warnings: ["Helper execution is deferred; scaffold reports local path health only."],
|
|
124
|
+
observed_at: options.observedAt,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
assertNoSecretSentinels(envelope);
|
|
128
|
+
return envelope;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const AI_PROVIDER_IDS: GuiAiProviderId[] = ["anthropic", "openai", "cursor", "google"];
|
|
132
|
+
|
|
133
|
+
const SETUP_TARGET_DEFINITIONS: Array<Pick<GuiSetupTargetSummary, "label" | "path_ref" | "purpose">> = [
|
|
134
|
+
{
|
|
135
|
+
label: "Deployed agents",
|
|
136
|
+
path_ref: "~/.aidevops/agents/VERSION",
|
|
137
|
+
purpose: "Canonical aidevops agent, workflow, script, and reference bundle.",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
label: "OpenCode prompt",
|
|
141
|
+
path_ref: "~/.config/opencode/AGENTS.md",
|
|
142
|
+
purpose: "OpenCode system prompt pointer into the deployed aidevops guide.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: "OpenCode config",
|
|
146
|
+
path_ref: "~/.config/opencode/opencode.json",
|
|
147
|
+
purpose: "OpenCode agents, commands, MCPs, and prompt configuration.",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: "Claude Code prompt",
|
|
151
|
+
path_ref: "~/.config/Claude/AGENTS.md",
|
|
152
|
+
purpose: "Claude Code global instructions pointer into the deployed aidevops guide.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: "Claude commands",
|
|
156
|
+
path_ref: "~/.claude/commands",
|
|
157
|
+
purpose: "Claude Code slash commands generated by aidevops.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: "Codex instructions",
|
|
161
|
+
path_ref: "~/.codex/instructions.md",
|
|
162
|
+
purpose: "Codex global instructions generated by aidevops.",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: "Cursor MCP config",
|
|
166
|
+
path_ref: "~/.cursor/mcp.json",
|
|
167
|
+
purpose: "Cursor MCP server configuration managed by aidevops.",
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
interface AiAppDefinition {
|
|
172
|
+
name: string;
|
|
173
|
+
binary: string;
|
|
174
|
+
app_path_refs: string[];
|
|
175
|
+
config_path_ref: string;
|
|
176
|
+
aidevops_target_path_ref: string;
|
|
177
|
+
version_args: string[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const AI_APP_DEFINITIONS: AiAppDefinition[] = [
|
|
181
|
+
{
|
|
182
|
+
name: "OpenCode",
|
|
183
|
+
binary: "opencode",
|
|
184
|
+
app_path_refs: ["~/Applications/OpenCode AIDevOps.app", "/Applications/OpenCode.app", "~/Applications/OpenCode.app"],
|
|
185
|
+
config_path_ref: "~/.config/opencode/opencode.json",
|
|
186
|
+
aidevops_target_path_ref: "~/.config/opencode/AGENTS.md",
|
|
187
|
+
version_args: ["--version"],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "Claude Code",
|
|
191
|
+
binary: "claude",
|
|
192
|
+
app_path_refs: ["/Applications/Claude.app", "~/Applications/Claude.app"],
|
|
193
|
+
config_path_ref: "~/.config/Claude/claude_desktop_config.json",
|
|
194
|
+
aidevops_target_path_ref: "~/.config/Claude/AGENTS.md",
|
|
195
|
+
version_args: ["--version"],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "Codex CLI",
|
|
199
|
+
binary: "codex",
|
|
200
|
+
app_path_refs: [],
|
|
201
|
+
config_path_ref: "~/.codex/config.toml",
|
|
202
|
+
aidevops_target_path_ref: "~/.codex/instructions.md",
|
|
203
|
+
version_args: ["--version"],
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "Cursor",
|
|
207
|
+
binary: "cursor",
|
|
208
|
+
app_path_refs: ["/Applications/Cursor.app", "~/Applications/Cursor.app"],
|
|
209
|
+
config_path_ref: "~/.cursor/mcp.json",
|
|
210
|
+
aidevops_target_path_ref: "~/.cursor/rules",
|
|
211
|
+
version_args: [],
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
function readMachineSummary(): GuiStatusData["machine"] {
|
|
216
|
+
const username = userInfo().username || "local";
|
|
217
|
+
const displayName = readAccountDisplayName() ?? username;
|
|
218
|
+
const host = hostname() || "localhost";
|
|
219
|
+
const localIps = Object.values(networkInterfaces())
|
|
220
|
+
.flatMap((entries) => entries ?? [])
|
|
221
|
+
.filter((entry) => entry.family === "IPv4" && !entry.internal)
|
|
222
|
+
.map((entry) => entry.address)
|
|
223
|
+
.sort();
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
id: "local",
|
|
227
|
+
label: host,
|
|
228
|
+
initials: initialsFromUsername(displayName),
|
|
229
|
+
username: displayName,
|
|
230
|
+
hostname: host,
|
|
231
|
+
local_ips: localIps.length > 0 ? localIps : ["127.0.0.1"],
|
|
232
|
+
public_ip: null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readAccountDisplayName(): string | null {
|
|
237
|
+
try {
|
|
238
|
+
const displayName = execFileSync("id", ["-F"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
239
|
+
return displayName.length > 0 ? displayName : null;
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function initialsFromUsername(username: string): string {
|
|
246
|
+
const parts = username.split(/[^a-z0-9]+/i).filter(Boolean);
|
|
247
|
+
if (parts.length >= 2) {
|
|
248
|
+
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const letters = username.replace(/[^a-z0-9]/gi, "").slice(0, 2);
|
|
252
|
+
return (letters || "LM").toUpperCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readSettingsSummary(pathName: string, pathRef: string): GuiSettingsSummary {
|
|
256
|
+
if (!existsSync(pathName)) {
|
|
257
|
+
return {
|
|
258
|
+
...statusFixture.settings,
|
|
259
|
+
path_ref: pathRef,
|
|
260
|
+
health: "missing",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const parsed = JSON.parse(readFileSync(pathName, "utf8")) as Record<string, unknown>;
|
|
266
|
+
const keys = Object.keys(parsed).sort();
|
|
267
|
+
return {
|
|
268
|
+
path_ref: pathRef,
|
|
269
|
+
health: "present",
|
|
270
|
+
key_count: keys.length,
|
|
271
|
+
keys,
|
|
272
|
+
value_policy: "keys_only_no_values",
|
|
273
|
+
};
|
|
274
|
+
} catch {
|
|
275
|
+
return {
|
|
276
|
+
...statusFixture.settings,
|
|
277
|
+
path_ref: pathRef,
|
|
278
|
+
health: "invalid",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readRepoRegistrySummary(pathName: string, pathRef: string): GuiRepoRegistrySummary {
|
|
284
|
+
if (!existsSync(pathName)) {
|
|
285
|
+
return {
|
|
286
|
+
...statusFixture.repos,
|
|
287
|
+
path_ref: pathRef,
|
|
288
|
+
health: "missing",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(readFileSync(pathName, "utf8")) as unknown;
|
|
294
|
+
const repos = extractRepoSummaries(parsed).slice(0, 12);
|
|
295
|
+
return {
|
|
296
|
+
path_ref: pathRef,
|
|
297
|
+
health: "present",
|
|
298
|
+
total: extractRepoSummaries(parsed).length,
|
|
299
|
+
repos,
|
|
300
|
+
};
|
|
301
|
+
} catch {
|
|
302
|
+
return {
|
|
303
|
+
...statusFixture.repos,
|
|
304
|
+
path_ref: pathRef,
|
|
305
|
+
health: "invalid",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readOAuthPoolSummary(pathName: string, pathRef: string): GuiOAuthPoolSummary {
|
|
311
|
+
const parsed = readJsonObject(pathName);
|
|
312
|
+
if (parsed.health === "missing") {
|
|
313
|
+
return {
|
|
314
|
+
...statusFixture.oauth_pool,
|
|
315
|
+
path_ref: pathRef,
|
|
316
|
+
health: "missing",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (parsed.health === "invalid") {
|
|
320
|
+
return {
|
|
321
|
+
...statusFixture.oauth_pool,
|
|
322
|
+
path_ref: pathRef,
|
|
323
|
+
health: "invalid",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const pool = parsed.value;
|
|
328
|
+
return {
|
|
329
|
+
path_ref: pathRef,
|
|
330
|
+
health: "present",
|
|
331
|
+
value_policy: "metadata_only_no_tokens",
|
|
332
|
+
providers: AI_PROVIDER_IDS.map((provider) => oauthProviderSummary(pool, provider)),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readSetupTargets(latestVersion: string, installedVersion: string): GuiSetupTargetSummary[] {
|
|
337
|
+
return SETUP_TARGET_DEFINITIONS.map((target) => {
|
|
338
|
+
const health = existsSync(expandHome(target.path_ref)) ? "present" : "missing";
|
|
339
|
+
const targetVersion = health === "present" ? installedVersion : "not installed";
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...target,
|
|
343
|
+
health,
|
|
344
|
+
installed_version: targetVersion,
|
|
345
|
+
latest_version: latestVersion,
|
|
346
|
+
needs_update: targetVersion !== latestVersion,
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readAiApps(latestVersion: string, installedVersion: string): GuiAiAppSummary[] {
|
|
352
|
+
return AI_APP_DEFINITIONS.map((definition) => aiAppSummary(definition, latestVersion, installedVersion));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function aiAppSummary(definition: AiAppDefinition, latestVersion: string, installedVersion: string): GuiAiAppSummary {
|
|
356
|
+
const binaryPath = resolveBinary(definition.binary);
|
|
357
|
+
const appPathRef = firstExistingPathRef(definition.app_path_refs) ?? definition.app_path_refs[0] ?? "not applicable";
|
|
358
|
+
const hasApp = definition.app_path_refs.some((pathRef) => existsSync(expandHome(pathRef)));
|
|
359
|
+
const hasConfig = existsSync(expandHome(definition.config_path_ref));
|
|
360
|
+
const hasAidevopsTarget = existsSync(expandHome(definition.aidevops_target_path_ref));
|
|
361
|
+
const aidevopsTargetVersion = hasAidevopsTarget ? installedVersion : "not installed";
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
name: definition.name,
|
|
365
|
+
status: binaryPath !== null || hasApp || hasConfig ? "found" : "missing",
|
|
366
|
+
app_path_ref: appPathRef,
|
|
367
|
+
binary_path_ref: binaryPath === null ? "not found" : collapseHome(binaryPath),
|
|
368
|
+
config_path_ref: definition.config_path_ref,
|
|
369
|
+
aidevops_target_path_ref: definition.aidevops_target_path_ref,
|
|
370
|
+
app_version: binaryPath === null ? "unknown" : readBinaryVersion(binaryPath, definition.version_args),
|
|
371
|
+
aidevops_version: aidevopsTargetVersion,
|
|
372
|
+
latest_version: latestVersion,
|
|
373
|
+
needs_update: aidevopsTargetVersion !== latestVersion,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function firstExistingPathRef(pathRefs: string[]): string | null {
|
|
378
|
+
return pathRefs.find((pathRef) => existsSync(expandHome(pathRef))) ?? null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveBinary(binary: string): string | null {
|
|
382
|
+
try {
|
|
383
|
+
const output = execFileSync("/usr/bin/which", [binary], {
|
|
384
|
+
encoding: "utf8",
|
|
385
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
386
|
+
timeout: 1_000,
|
|
387
|
+
}).trim();
|
|
388
|
+
return output.split("\n").find((line) => line.length > 0) ?? null;
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readBinaryVersion(binaryPath: string, args: string[]): string {
|
|
395
|
+
if (args.length === 0) {
|
|
396
|
+
return "unknown";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const output = execFileSync(binaryPath, args, {
|
|
401
|
+
encoding: "utf8",
|
|
402
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
403
|
+
timeout: 1_500,
|
|
404
|
+
}).trim();
|
|
405
|
+
return output.split("\n").find((line) => line.length > 0) ?? "unknown";
|
|
406
|
+
} catch {
|
|
407
|
+
return "unknown";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isSourcePathRef(pathRef: string): boolean {
|
|
412
|
+
return pathRef === "VERSION" || pathRef.startsWith("~/") || pathRef.startsWith("/");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function oauthProviderSummary(pool: Record<string, unknown>, provider: GuiAiProviderId): GuiOAuthProviderSummary {
|
|
416
|
+
const accounts = Array.isArray(pool[provider]) ? pool[provider].filter(isRecord) : [];
|
|
417
|
+
const now = Date.now();
|
|
418
|
+
const accountSummaries = accounts.map((account) => oauthAccountSummary(account));
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
provider,
|
|
422
|
+
configured: accountSummaries.length > 0,
|
|
423
|
+
total: accountSummaries.length,
|
|
424
|
+
available: accountSummaries.filter((account) => cooldownReady(account.cooldown_until, now)).length,
|
|
425
|
+
active_or_idle: accountSummaries.filter((account) => account.status === "active" || account.status === "idle").length,
|
|
426
|
+
rate_limited: accountSummaries.filter((account) => account.status === "rate-limited" && !cooldownReady(account.cooldown_until, now)).length,
|
|
427
|
+
auth_errors: accountSummaries.filter((account) => account.status === "auth-error").length,
|
|
428
|
+
pending_token: isRecord(pool[`_pending_${provider}`]),
|
|
429
|
+
accounts: accountSummaries,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function oauthAccountSummary(account: Record<string, unknown>): GuiOAuthProviderSummary["accounts"][number] {
|
|
434
|
+
return {
|
|
435
|
+
email_ref: stringField(account, "email") ?? "unknown",
|
|
436
|
+
status: stringField(account, "status") ?? "unknown",
|
|
437
|
+
priority: numberField(account, "priority"),
|
|
438
|
+
last_used: stringField(account, "lastUsed") ?? stringField(account, "added") ?? "unknown",
|
|
439
|
+
expires_at: formatEpochField(account.expires),
|
|
440
|
+
cooldown_until: formatNullableEpochField(account.cooldownUntil),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function extractRepoSummaries(value: unknown): GuiRepoSummary[] {
|
|
445
|
+
if (Array.isArray(value)) {
|
|
446
|
+
return value.map((entry, index) => repoSummaryFromUnknown(entry, `repo-${index + 1}`));
|
|
447
|
+
}
|
|
448
|
+
if (isRecord(value) && Array.isArray(value.initialized_repos)) {
|
|
449
|
+
return value.initialized_repos.map((entry, index) => repoSummaryFromUnknown(entry, `repo-${index + 1}`));
|
|
450
|
+
}
|
|
451
|
+
if (isRecord(value) && Array.isArray(value.repos)) {
|
|
452
|
+
return value.repos.map((entry, index) => repoSummaryFromUnknown(entry, `repo-${index + 1}`));
|
|
453
|
+
}
|
|
454
|
+
if (isRecord(value)) {
|
|
455
|
+
return Object.entries(value)
|
|
456
|
+
.filter(([, entry]) => isRecord(entry))
|
|
457
|
+
.map(([name, entry]) => repoSummaryFromUnknown(entry, name));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function repoSummaryFromUnknown(value: unknown, fallbackName: string): GuiRepoSummary {
|
|
464
|
+
if (!isRecord(value)) {
|
|
465
|
+
return {
|
|
466
|
+
name: fallbackName,
|
|
467
|
+
platform: "unknown",
|
|
468
|
+
slug: fallbackName,
|
|
469
|
+
local_path_status: "not_provided",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const name = stringField(value, "name") ?? stringField(value, "slug") ?? fallbackName;
|
|
474
|
+
const slug = stringField(value, "slug") ?? stringField(value, "repo") ?? name;
|
|
475
|
+
const platform = stringField(value, "platform") ?? stringField(value, "provider") ?? "unknown";
|
|
476
|
+
const localPath = stringField(value, "path") ?? stringField(value, "local_path");
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
name,
|
|
480
|
+
platform,
|
|
481
|
+
slug,
|
|
482
|
+
local_path_status: localPath === undefined ? "not_provided" : existsSync(expandHome(localPath)) ? "present" : "missing",
|
|
483
|
+
};
|
|
484
|
+
}
|