opencode-usage 0.4.7 → 0.5.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/dist/__tests__/commander-app-init.test.d.ts +1 -0
- package/dist/__tests__/commander-command-runner.test.d.ts +1 -0
- package/dist/__tests__/commander-config-service.test.d.ts +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/commander/index.d.ts +1 -0
- package/dist/commander/server.d.ts +7 -0
- package/dist/commander/services/action-service.d.ts +11 -0
- package/dist/commander/services/app-init-service.d.ts +17 -0
- package/dist/commander/services/command-runner.d.ts +12 -0
- package/dist/commander/services/config-service.d.ts +38 -0
- package/dist/commander/services/index.d.ts +2 -0
- package/dist/commander/services/plugin-adapters.d.ts +7 -0
- package/dist/commander/services/quota-service.d.ts +10 -0
- package/dist/commander/services/types.d.ts +33 -0
- package/dist/commander/services/usage-service.d.ts +32 -0
- package/dist/commander/services/usage-worker.d.ts +7 -0
- package/dist/index.js +1366 -27
- package/dist/index.js.map +13 -6
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,754 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
4
|
+
|
|
5
|
+
// src/commander/services/command-runner.ts
|
|
6
|
+
function registerCommand(spec) {
|
|
7
|
+
if (registry.has(spec.id)) {
|
|
8
|
+
throw new Error(`Command "${spec.id}" is already registered`);
|
|
9
|
+
}
|
|
10
|
+
registry.set(spec.id, spec);
|
|
11
|
+
}
|
|
12
|
+
function getJob(jobId) {
|
|
13
|
+
return jobs.get(jobId);
|
|
14
|
+
}
|
|
15
|
+
function runCommand(commandId, payload) {
|
|
16
|
+
const spec = registry.get(commandId);
|
|
17
|
+
if (!spec) {
|
|
18
|
+
throw new Error(`Unknown command: "${commandId}"`);
|
|
19
|
+
}
|
|
20
|
+
const input = spec.validateInput(payload);
|
|
21
|
+
const jobId = crypto.randomUUID();
|
|
22
|
+
const job = {
|
|
23
|
+
id: jobId,
|
|
24
|
+
commandId,
|
|
25
|
+
status: "queued",
|
|
26
|
+
logs: []
|
|
27
|
+
};
|
|
28
|
+
jobs.set(jobId, job);
|
|
29
|
+
const ctx = {
|
|
30
|
+
jobId,
|
|
31
|
+
log(level, message) {
|
|
32
|
+
job.logs.push({ ts: new Date().toISOString(), level, message });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
executeJob(job, ctx, spec, input);
|
|
36
|
+
return jobId;
|
|
37
|
+
}
|
|
38
|
+
async function executeJob(job, ctx, spec, input) {
|
|
39
|
+
job.status = "running";
|
|
40
|
+
job.startedAt = new Date().toISOString();
|
|
41
|
+
ctx.log("info", `Running command "${job.commandId}"`);
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true;
|
|
45
|
+
if (job.status === "running") {
|
|
46
|
+
job.status = "failed";
|
|
47
|
+
job.finishedAt = new Date().toISOString();
|
|
48
|
+
job.error = {
|
|
49
|
+
code: "TIMEOUT",
|
|
50
|
+
message: `Command "${job.commandId}" timed out after ${spec.timeoutMs}ms`
|
|
51
|
+
};
|
|
52
|
+
ctx.log("error", `Timeout after ${spec.timeoutMs}ms`);
|
|
53
|
+
}
|
|
54
|
+
}, spec.timeoutMs);
|
|
55
|
+
try {
|
|
56
|
+
const result = await spec.run(ctx, input);
|
|
57
|
+
if (timedOut)
|
|
58
|
+
return;
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
job.status = "success";
|
|
61
|
+
job.finishedAt = new Date().toISOString();
|
|
62
|
+
job.result = result;
|
|
63
|
+
ctx.log("info", "Command completed successfully");
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (timedOut)
|
|
66
|
+
return;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
const message = err instanceof Error ? err.message : "Unknown runtime error";
|
|
69
|
+
job.status = "failed";
|
|
70
|
+
job.finishedAt = new Date().toISOString();
|
|
71
|
+
job.error = { code: "RUNTIME_ERROR", message };
|
|
72
|
+
ctx.log("error", message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
var registry, jobs;
|
|
76
|
+
var init_command_runner = __esm(() => {
|
|
77
|
+
registry = new Map;
|
|
78
|
+
jobs = new Map;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/commander/services/config-service.ts
|
|
82
|
+
import { homedir as homedir5 } from "os";
|
|
83
|
+
import { join as join7, basename as basename2 } from "path";
|
|
84
|
+
import { rename, mkdir, copyFile, readdir, stat } from "fs/promises";
|
|
85
|
+
import { readFileSync } from "fs";
|
|
86
|
+
function isValidSource(s) {
|
|
87
|
+
return VALID_SOURCES.has(s);
|
|
88
|
+
}
|
|
89
|
+
function configPath(source) {
|
|
90
|
+
return join7(CONFIG_DIR, SOURCE_FILENAMES[source]);
|
|
91
|
+
}
|
|
92
|
+
async function fileExists(path2) {
|
|
93
|
+
try {
|
|
94
|
+
await stat(path2);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function readFileTextSync(path2) {
|
|
101
|
+
return readFileSync(path2, "utf-8");
|
|
102
|
+
}
|
|
103
|
+
async function writeFileText(path2, content) {
|
|
104
|
+
if (isBun3) {
|
|
105
|
+
await Bun.write(path2, content);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const { writeFile } = await import("fs/promises");
|
|
109
|
+
await writeFile(path2, content, "utf-8");
|
|
110
|
+
}
|
|
111
|
+
function getCached(path2) {
|
|
112
|
+
const entry = configCache.get(path2);
|
|
113
|
+
if (entry && Date.now() < entry.expiry)
|
|
114
|
+
return entry.data;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
function setCache(path2, data) {
|
|
118
|
+
configCache.set(path2, { data, expiry: Date.now() + CONFIG_CACHE_TTL });
|
|
119
|
+
}
|
|
120
|
+
function invalidateCache(source) {
|
|
121
|
+
configCache.delete(configPath(source));
|
|
122
|
+
}
|
|
123
|
+
async function listConfigFiles() {
|
|
124
|
+
const sources = Object.keys(SOURCE_FILENAMES);
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const source of sources) {
|
|
127
|
+
const path2 = configPath(source);
|
|
128
|
+
let exists = false;
|
|
129
|
+
let parseOk = false;
|
|
130
|
+
let sizeBytes = 0;
|
|
131
|
+
try {
|
|
132
|
+
const s = await stat(path2);
|
|
133
|
+
exists = true;
|
|
134
|
+
sizeBytes = s.size;
|
|
135
|
+
const raw = readFileTextSync(path2);
|
|
136
|
+
JSON.parse(raw);
|
|
137
|
+
parseOk = true;
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
results.push({ source, path: path2, exists, parseOk, sizeBytes });
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
function readConfig(source) {
|
|
145
|
+
const path2 = configPath(source);
|
|
146
|
+
const cached = getCached(path2);
|
|
147
|
+
if (cached !== undefined)
|
|
148
|
+
return cached;
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = readFileTextSync(path2);
|
|
152
|
+
} catch {
|
|
153
|
+
throw new ConfigError(`Config file not found: ${path2}`, 404);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = JSON.parse(raw);
|
|
157
|
+
setCache(path2, result);
|
|
158
|
+
return result;
|
|
159
|
+
} catch {
|
|
160
|
+
throw new ConfigError(`Config file is not valid JSON: ${basename2(path2)}`, 422);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function writeConfig(source, data) {
|
|
164
|
+
const path2 = configPath(source);
|
|
165
|
+
const backupPath = await createBackup(source);
|
|
166
|
+
const tmpPath = `${path2}.tmp`;
|
|
167
|
+
const content = JSON.stringify(data, null, 2) + "\n";
|
|
168
|
+
await writeFileText(tmpPath, content);
|
|
169
|
+
await rename(tmpPath, path2);
|
|
170
|
+
invalidateCache(source);
|
|
171
|
+
return { backupPath };
|
|
172
|
+
}
|
|
173
|
+
async function rollbackConfig(source) {
|
|
174
|
+
const latest = await findLatestBackup(source);
|
|
175
|
+
if (!latest) {
|
|
176
|
+
throw new ConfigError(`No backup found for source: ${source}`, 404);
|
|
177
|
+
}
|
|
178
|
+
const path2 = configPath(source);
|
|
179
|
+
const tmpPath = `${path2}.tmp`;
|
|
180
|
+
await copyFile(latest, tmpPath);
|
|
181
|
+
await rename(tmpPath, path2);
|
|
182
|
+
return { restoredFrom: latest };
|
|
183
|
+
}
|
|
184
|
+
async function createBackup(source) {
|
|
185
|
+
const path2 = configPath(source);
|
|
186
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
187
|
+
const backupDir = join7(BACKUP_ROOT, timestamp);
|
|
188
|
+
await mkdir(backupDir, { recursive: true });
|
|
189
|
+
const backupFile = join7(backupDir, SOURCE_FILENAMES[source]);
|
|
190
|
+
if (await fileExists(path2)) {
|
|
191
|
+
await copyFile(path2, backupFile);
|
|
192
|
+
} else {
|
|
193
|
+
await writeFileText(backupFile, "null\n");
|
|
194
|
+
}
|
|
195
|
+
return backupFile;
|
|
196
|
+
}
|
|
197
|
+
async function findLatestBackup(source) {
|
|
198
|
+
if (!await fileExists(BACKUP_ROOT))
|
|
199
|
+
return null;
|
|
200
|
+
const entries = await readdir(BACKUP_ROOT);
|
|
201
|
+
const sorted = entries.sort();
|
|
202
|
+
for (let i = sorted.length - 1;i >= 0; i--) {
|
|
203
|
+
const candidate = join7(BACKUP_ROOT, sorted[i], SOURCE_FILENAMES[source]);
|
|
204
|
+
if (await fileExists(candidate)) {
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
var isBun3, CONFIG_DIR, BACKUP_ROOT, SOURCE_FILENAMES, VALID_SOURCES, CONFIG_CACHE_TTL = 2000, configCache, ConfigError;
|
|
211
|
+
var init_config_service = __esm(() => {
|
|
212
|
+
isBun3 = typeof globalThis.Bun !== "undefined";
|
|
213
|
+
CONFIG_DIR = join7(homedir5(), ".config", "opencode");
|
|
214
|
+
BACKUP_ROOT = join7(CONFIG_DIR, "commander-backups");
|
|
215
|
+
SOURCE_FILENAMES = {
|
|
216
|
+
"codex-multi-account-accounts": "codex-multi-account-accounts.json",
|
|
217
|
+
"anthropic-multi-account-state": "anthropic-multi-account-state.json",
|
|
218
|
+
"antigravity-accounts": "antigravity-accounts.json",
|
|
219
|
+
opencode: "opencode.json"
|
|
220
|
+
};
|
|
221
|
+
VALID_SOURCES = new Set(Object.keys(SOURCE_FILENAMES));
|
|
222
|
+
configCache = new Map;
|
|
223
|
+
ConfigError = class ConfigError extends Error {
|
|
224
|
+
status;
|
|
225
|
+
constructor(message, status) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.name = "ConfigError";
|
|
228
|
+
this.status = status;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// src/commander/services/plugin-adapters.ts
|
|
234
|
+
var exports_plugin_adapters = {};
|
|
235
|
+
import { homedir as homedir6, tmpdir } from "os";
|
|
236
|
+
import { join as join8 } from "path";
|
|
237
|
+
function resolveSource2(provider) {
|
|
238
|
+
const source = PROVIDER_SOURCE[provider];
|
|
239
|
+
if (!source) {
|
|
240
|
+
throw new Error(`Unknown provider "${provider}". Valid: ${Object.keys(PROVIDER_SOURCE).join(", ")}`);
|
|
241
|
+
}
|
|
242
|
+
return source;
|
|
243
|
+
}
|
|
244
|
+
async function readCredentialFile(filename) {
|
|
245
|
+
const filePath = join8(homedir6(), ".config", "opencode", filename);
|
|
246
|
+
const text = await Bun.file(filePath).text();
|
|
247
|
+
return JSON.parse(text);
|
|
248
|
+
}
|
|
249
|
+
async function spawnPluginCli(command, args, timeoutMs = 15000) {
|
|
250
|
+
const localBin = `./node_modules/${command}/dist/cli.js`;
|
|
251
|
+
const useLocal = await Bun.file(localBin).exists();
|
|
252
|
+
if (!useLocal) {
|
|
253
|
+
const existing = bunxBarrier.get(command);
|
|
254
|
+
if (existing) {
|
|
255
|
+
await existing.catch(() => {
|
|
256
|
+
});
|
|
257
|
+
return runPluginCli(command, args, timeoutMs, false);
|
|
258
|
+
}
|
|
259
|
+
const warmup = runPluginCli(command, args, timeoutMs, false);
|
|
260
|
+
const barrier = warmup.then(() => {
|
|
261
|
+
}, () => {
|
|
262
|
+
});
|
|
263
|
+
bunxBarrier.set(command, barrier);
|
|
264
|
+
barrier.finally(() => bunxBarrier.delete(command));
|
|
265
|
+
return warmup;
|
|
266
|
+
}
|
|
267
|
+
return runPluginCli(command, args, timeoutMs, true);
|
|
268
|
+
}
|
|
269
|
+
async function runPluginCli(command, args, timeoutMs, useLocal) {
|
|
270
|
+
const t0 = Date.now();
|
|
271
|
+
const localBin = `./node_modules/${command}/dist/cli.js`;
|
|
272
|
+
const cmd = useLocal ? ["bun", localBin, ...args] : ["bunx", `${command}@latest`, ...args];
|
|
273
|
+
const cwd = useLocal ? undefined : join8(tmpdir(), `bunx-${crypto.randomUUID()}`);
|
|
274
|
+
if (cwd)
|
|
275
|
+
await Bun.$`mkdir -p ${cwd}`.quiet();
|
|
276
|
+
console.log(`[spawnPluginCli] starting: ${cmd.join(" ")}`);
|
|
277
|
+
const proc = Bun.spawn(cmd, {
|
|
278
|
+
stdout: "pipe",
|
|
279
|
+
stderr: "pipe",
|
|
280
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
281
|
+
cwd
|
|
282
|
+
});
|
|
283
|
+
const timer = setTimeout(() => {
|
|
284
|
+
console.log(`[spawnPluginCli] killing after ${timeoutMs}ms`);
|
|
285
|
+
proc.kill();
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
try {
|
|
288
|
+
const [stdout, exitCode] = await Promise.all([
|
|
289
|
+
new Response(proc.stdout).text(),
|
|
290
|
+
proc.exited
|
|
291
|
+
]);
|
|
292
|
+
const elapsed = Date.now() - t0;
|
|
293
|
+
console.log(`[spawnPluginCli] exited ${exitCode} in ${elapsed}ms, stdout=${stdout.length}b`);
|
|
294
|
+
if (exitCode !== 0) {
|
|
295
|
+
const stderr = await new Response(proc.stderr).text();
|
|
296
|
+
console.log(`[spawnPluginCli] stderr: ${stderr.slice(0, 300)}`);
|
|
297
|
+
throw new Error(`${command} exited with code ${exitCode}: ${(stderr || stdout).slice(0, 300)}`);
|
|
298
|
+
}
|
|
299
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
300
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(lines[i]);
|
|
303
|
+
console.log(`[spawnPluginCli] parsed:`, parsed);
|
|
304
|
+
return parsed;
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`No JSON output from ${command}: ${stdout.slice(0, 200)}`);
|
|
310
|
+
} finally {
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
if (cwd)
|
|
313
|
+
Bun.$`rm -rf ${cwd}`.quiet().catch(() => {
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function clearStaleMetrics(provider, alias) {
|
|
318
|
+
console.log(`[clearStaleMetrics] checking ${provider}/${alias}`);
|
|
319
|
+
try {
|
|
320
|
+
const source = resolveSource2(provider);
|
|
321
|
+
const data = await readConfig(source);
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
let changed = false;
|
|
324
|
+
switch (provider) {
|
|
325
|
+
case "anthropic": {
|
|
326
|
+
const usage = data.usage ?? {};
|
|
327
|
+
const acct = usage[alias];
|
|
328
|
+
if (acct) {
|
|
329
|
+
for (const key of ["session5h", "weekly7d", "weekly7dSonnet"]) {
|
|
330
|
+
const m2 = acct[key];
|
|
331
|
+
if (m2 && typeof m2.reset === "number" && m2.reset < now / 1000) {
|
|
332
|
+
m2.utilization = 0;
|
|
333
|
+
m2.status = "active";
|
|
334
|
+
delete m2.reset;
|
|
335
|
+
changed = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case "codex": {
|
|
342
|
+
const accounts = data.accounts ?? {};
|
|
343
|
+
const acct = accounts[alias];
|
|
344
|
+
if (acct?.rateLimits) {
|
|
345
|
+
const rl = acct.rateLimits;
|
|
346
|
+
for (const key of ["fiveHour", "weekly"]) {
|
|
347
|
+
const w2 = rl[key];
|
|
348
|
+
if (w2?.resetAt && typeof w2.resetAt === "string") {
|
|
349
|
+
if (new Date(w2.resetAt).getTime() < now) {
|
|
350
|
+
w2.remaining = w2.limit;
|
|
351
|
+
delete w2.resetAt;
|
|
352
|
+
changed = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "antigravity": {
|
|
360
|
+
const accounts = Array.isArray(data.accounts) ? data.accounts : [];
|
|
361
|
+
const acct = accounts.find((a) => a.email === alias);
|
|
362
|
+
if (acct?.cachedQuota) {
|
|
363
|
+
const quota = acct.cachedQuota;
|
|
364
|
+
for (const group of Object.keys(quota)) {
|
|
365
|
+
const q2 = quota[group];
|
|
366
|
+
if (q2.resetTime && typeof q2.resetTime === "string") {
|
|
367
|
+
if (new Date(q2.resetTime).getTime() < now) {
|
|
368
|
+
q2.remainingFraction = 1;
|
|
369
|
+
delete q2.resetTime;
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (changed) {
|
|
379
|
+
console.log(`[clearStaleMetrics] cleared stale data for ${provider}/${alias}`);
|
|
380
|
+
await writeConfig(source, data);
|
|
381
|
+
} else {
|
|
382
|
+
console.log(`[clearStaleMetrics] no stale data found for ${provider}/${alias}`);
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.log(`[clearStaleMetrics] error for ${provider}/${alias}:`, err);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function reauthCliCommand(provider) {
|
|
389
|
+
const cmd = REAUTH_PROVIDERS[provider];
|
|
390
|
+
if (!cmd)
|
|
391
|
+
throw new Error(`Re-auth not supported for provider: ${provider}`);
|
|
392
|
+
return cmd;
|
|
393
|
+
}
|
|
394
|
+
var isBun4, PROVIDER_SOURCE, bunxBarrier, REAUTH_PROVIDERS;
|
|
395
|
+
var init_plugin_adapters = __esm(() => {
|
|
396
|
+
init_command_runner();
|
|
397
|
+
init_config_service();
|
|
398
|
+
isBun4 = typeof globalThis.Bun !== "undefined";
|
|
399
|
+
PROVIDER_SOURCE = {
|
|
400
|
+
anthropic: "anthropic-multi-account-state",
|
|
401
|
+
codex: "codex-multi-account-accounts",
|
|
402
|
+
antigravity: "antigravity-accounts"
|
|
403
|
+
};
|
|
404
|
+
registerCommand({
|
|
405
|
+
id: "accounts.add",
|
|
406
|
+
timeoutMs: 30000,
|
|
407
|
+
allowInUi: true,
|
|
408
|
+
validateInput(payload) {
|
|
409
|
+
const p = payload;
|
|
410
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
411
|
+
throw new Error("accounts.add requires a non-empty 'provider' string");
|
|
412
|
+
}
|
|
413
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
414
|
+
throw new Error("accounts.add requires a non-empty 'alias' string");
|
|
415
|
+
}
|
|
416
|
+
return { provider: p.provider, alias: p.alias };
|
|
417
|
+
},
|
|
418
|
+
async run(ctx, input) {
|
|
419
|
+
ctx.log("info", `Adding account "${input.alias}" for provider "${input.provider}"`);
|
|
420
|
+
ctx.log("info", "OAuth authentication requires terminal interaction \u2014 run this command from the CLI.");
|
|
421
|
+
return {
|
|
422
|
+
message: `Account "${input.alias}" for provider "${input.provider}" requires terminal-based OAuth. Please run: opencode-usage accounts add --provider ${input.provider} --alias ${input.alias}`,
|
|
423
|
+
requiresTerminal: true
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
registerCommand({
|
|
428
|
+
id: "accounts.remove",
|
|
429
|
+
timeoutMs: 30000,
|
|
430
|
+
allowInUi: true,
|
|
431
|
+
validateInput(payload) {
|
|
432
|
+
const p = payload;
|
|
433
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
434
|
+
throw new Error("accounts.remove requires a non-empty 'provider' string");
|
|
435
|
+
}
|
|
436
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
437
|
+
throw new Error("accounts.remove requires a non-empty 'alias' string");
|
|
438
|
+
}
|
|
439
|
+
return { provider: p.provider, alias: p.alias };
|
|
440
|
+
},
|
|
441
|
+
async run(ctx, input) {
|
|
442
|
+
const source = resolveSource2(input.provider);
|
|
443
|
+
ctx.log("info", `Removing account "${input.alias}" from ${source}`);
|
|
444
|
+
const data = await readConfig(source);
|
|
445
|
+
const accounts = data.accounts ?? {};
|
|
446
|
+
delete accounts[input.alias];
|
|
447
|
+
data.accounts = accounts;
|
|
448
|
+
await writeConfig(source, data);
|
|
449
|
+
ctx.log("info", `Account "${input.alias}" removed successfully`);
|
|
450
|
+
return { ok: true };
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
registerCommand({
|
|
454
|
+
id: "accounts.switch",
|
|
455
|
+
timeoutMs: 30000,
|
|
456
|
+
allowInUi: true,
|
|
457
|
+
validateInput(payload) {
|
|
458
|
+
const p = payload;
|
|
459
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
460
|
+
throw new Error("accounts.switch requires a non-empty 'provider' string");
|
|
461
|
+
}
|
|
462
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
463
|
+
throw new Error("accounts.switch requires a non-empty 'alias' string");
|
|
464
|
+
}
|
|
465
|
+
return { provider: p.provider, alias: p.alias };
|
|
466
|
+
},
|
|
467
|
+
async run(ctx, input) {
|
|
468
|
+
const source = resolveSource2(input.provider);
|
|
469
|
+
ctx.log("info", `Switching active account to "${input.alias}" in ${source}`);
|
|
470
|
+
const data = await readConfig(source);
|
|
471
|
+
switch (input.provider) {
|
|
472
|
+
case "anthropic":
|
|
473
|
+
data.currentAccount = input.alias;
|
|
474
|
+
break;
|
|
475
|
+
case "codex":
|
|
476
|
+
data.activeAlias = input.alias;
|
|
477
|
+
break;
|
|
478
|
+
case "antigravity": {
|
|
479
|
+
const accounts = Array.isArray(data.accounts) ? data.accounts : [];
|
|
480
|
+
const idx = accounts.findIndex((a) => typeof a.email === "string" && a.email === input.alias);
|
|
481
|
+
if (idx === -1) {
|
|
482
|
+
throw new Error(`Account "${input.alias}" not found in antigravity`);
|
|
483
|
+
}
|
|
484
|
+
data.activeIndex = idx;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
default:
|
|
488
|
+
throw new Error(`Unknown provider: ${input.provider}`);
|
|
489
|
+
}
|
|
490
|
+
await writeConfig(source, data);
|
|
491
|
+
ctx.log("info", `Active account set to "${input.alias}"`);
|
|
492
|
+
return { ok: true };
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
bunxBarrier = new Map;
|
|
496
|
+
registerCommand({
|
|
497
|
+
id: "accounts.ping",
|
|
498
|
+
timeoutMs: 30000,
|
|
499
|
+
allowInUi: true,
|
|
500
|
+
validateInput(payload) {
|
|
501
|
+
const p = payload;
|
|
502
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
503
|
+
throw new Error("accounts.ping requires a non-empty 'provider' string");
|
|
504
|
+
}
|
|
505
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
506
|
+
throw new Error("accounts.ping requires a non-empty 'alias' string");
|
|
507
|
+
}
|
|
508
|
+
return { provider: p.provider, alias: p.alias };
|
|
509
|
+
},
|
|
510
|
+
async run(ctx, input) {
|
|
511
|
+
ctx.log("info", `Pinging ${input.provider} account "${input.alias}"\u2026`);
|
|
512
|
+
switch (input.provider) {
|
|
513
|
+
case "anthropic": {
|
|
514
|
+
ctx.log("info", "Calling oc-anthropic-multi-account ping\u2026");
|
|
515
|
+
const result = await spawnPluginCli("oc-anthropic-multi-account", [
|
|
516
|
+
"ping",
|
|
517
|
+
input.alias
|
|
518
|
+
]);
|
|
519
|
+
const status = String(result.status ?? "error");
|
|
520
|
+
ctx.log("info", `Result: ${status}`);
|
|
521
|
+
if (status === "ok") {
|
|
522
|
+
await clearStaleMetrics(input.provider, input.alias);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
status,
|
|
526
|
+
message: status === "ok" ? "pong" : String(result.error ?? "unknown error")
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
case "codex": {
|
|
530
|
+
ctx.log("info", "Calling oc-codex-multi-account ping\u2026");
|
|
531
|
+
const result = await spawnPluginCli("oc-codex-multi-account", [
|
|
532
|
+
"ping",
|
|
533
|
+
input.alias
|
|
534
|
+
]);
|
|
535
|
+
const status = String(result.status ?? "error");
|
|
536
|
+
ctx.log("info", `Result: ${status}`);
|
|
537
|
+
if (status === "ok") {
|
|
538
|
+
await clearStaleMetrics(input.provider, input.alias);
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
status,
|
|
542
|
+
message: status === "ok" ? "pong" : String(result.error ?? "unknown error")
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
case "antigravity": {
|
|
546
|
+
const creds = await readCredentialFile("antigravity-accounts.json");
|
|
547
|
+
const accounts = Array.isArray(creds.accounts) ? creds.accounts : [];
|
|
548
|
+
const account = accounts.find((a) => a.email === input.alias);
|
|
549
|
+
if (!account) {
|
|
550
|
+
throw new Error(`Account "${input.alias}" not found`);
|
|
551
|
+
}
|
|
552
|
+
if (!account.refreshToken) {
|
|
553
|
+
throw new Error(`No refresh token for "${input.alias}"`);
|
|
554
|
+
}
|
|
555
|
+
ctx.log("info", "Refresh token present");
|
|
556
|
+
await clearStaleMetrics(input.provider, input.alias);
|
|
557
|
+
return {
|
|
558
|
+
status: "ok",
|
|
559
|
+
message: "pong (credentials present)"
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
default:
|
|
563
|
+
throw new Error(`Unknown provider: ${input.provider}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
REAUTH_PROVIDERS = {
|
|
568
|
+
anthropic: "oc-anthropic-multi-account",
|
|
569
|
+
codex: "oc-codex-multi-account"
|
|
570
|
+
};
|
|
571
|
+
registerCommand({
|
|
572
|
+
id: "accounts.reauth-start",
|
|
573
|
+
timeoutMs: 15000,
|
|
574
|
+
allowInUi: true,
|
|
575
|
+
validateInput(payload) {
|
|
576
|
+
const p = payload;
|
|
577
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
578
|
+
throw new Error("accounts.reauth-start requires a non-empty 'provider' string");
|
|
579
|
+
}
|
|
580
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
581
|
+
throw new Error("accounts.reauth-start requires a non-empty 'alias' string");
|
|
582
|
+
}
|
|
583
|
+
return { provider: p.provider, alias: p.alias };
|
|
584
|
+
},
|
|
585
|
+
async run(ctx, input) {
|
|
586
|
+
const cliCmd = reauthCliCommand(input.provider);
|
|
587
|
+
ctx.log("info", `Generating auth URL for ${input.alias}\u2026`);
|
|
588
|
+
const result = await spawnPluginCli(cliCmd, ["reauth", input.alias]);
|
|
589
|
+
const url = String(result.url ?? "");
|
|
590
|
+
const verifier = String(result.verifier ?? "");
|
|
591
|
+
if (!url || !verifier) {
|
|
592
|
+
throw new Error("CLI did not return url/verifier");
|
|
593
|
+
}
|
|
594
|
+
ctx.log("info", "Auth URL generated");
|
|
595
|
+
return { url, verifier };
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
registerCommand({
|
|
599
|
+
id: "accounts.reauth-complete",
|
|
600
|
+
timeoutMs: 30000,
|
|
601
|
+
allowInUi: true,
|
|
602
|
+
validateInput(payload) {
|
|
603
|
+
const p = payload;
|
|
604
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
605
|
+
throw new Error("accounts.reauth-complete requires 'provider'");
|
|
606
|
+
}
|
|
607
|
+
if (typeof p.alias !== "string" || !p.alias) {
|
|
608
|
+
throw new Error("accounts.reauth-complete requires 'alias'");
|
|
609
|
+
}
|
|
610
|
+
if (typeof p.callbackUrl !== "string" || !p.callbackUrl) {
|
|
611
|
+
throw new Error("accounts.reauth-complete requires 'callbackUrl'");
|
|
612
|
+
}
|
|
613
|
+
if (typeof p.verifier !== "string" || !p.verifier) {
|
|
614
|
+
throw new Error("accounts.reauth-complete requires 'verifier'");
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
provider: p.provider,
|
|
618
|
+
alias: p.alias,
|
|
619
|
+
callbackUrl: p.callbackUrl,
|
|
620
|
+
verifier: p.verifier
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
async run(ctx, input) {
|
|
624
|
+
const cliCmd = reauthCliCommand(input.provider);
|
|
625
|
+
ctx.log("info", `Completing re-auth for ${input.alias}\u2026`);
|
|
626
|
+
const result = await spawnPluginCli(cliCmd, [
|
|
627
|
+
"reauth",
|
|
628
|
+
"--callback",
|
|
629
|
+
input.callbackUrl,
|
|
630
|
+
"--verifier",
|
|
631
|
+
input.verifier,
|
|
632
|
+
input.alias
|
|
633
|
+
]);
|
|
634
|
+
const status = String(result.status ?? "error");
|
|
635
|
+
ctx.log("info", `Result: ${status}`);
|
|
636
|
+
return {
|
|
637
|
+
status,
|
|
638
|
+
message: status === "ok" ? "Re-authenticated successfully" : String(result.error ?? "unknown error")
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
registerCommand({
|
|
643
|
+
id: "actions.thresholds",
|
|
644
|
+
timeoutMs: 30000,
|
|
645
|
+
allowInUi: true,
|
|
646
|
+
validateInput(payload) {
|
|
647
|
+
const p = payload;
|
|
648
|
+
if (!p || typeof p.provider !== "string" || !p.provider) {
|
|
649
|
+
throw new Error("actions.thresholds requires a non-empty 'provider' string");
|
|
650
|
+
}
|
|
651
|
+
if (typeof p.warning !== "number") {
|
|
652
|
+
throw new Error("actions.thresholds requires a numeric 'warning' value");
|
|
653
|
+
}
|
|
654
|
+
if (typeof p.critical !== "number") {
|
|
655
|
+
throw new Error("actions.thresholds requires a numeric 'critical' value");
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
provider: p.provider,
|
|
659
|
+
warning: p.warning,
|
|
660
|
+
critical: p.critical
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
async run(ctx, input) {
|
|
664
|
+
const source = resolveSource2(input.provider);
|
|
665
|
+
ctx.log("info", `Updating thresholds for "${input.provider}" \u2014 warning: ${input.warning}, critical: ${input.critical}`);
|
|
666
|
+
const data = await readConfig(source);
|
|
667
|
+
data.thresholds = {
|
|
668
|
+
warning: input.warning,
|
|
669
|
+
critical: input.critical
|
|
670
|
+
};
|
|
671
|
+
await writeConfig(source, data);
|
|
672
|
+
ctx.log("info", "Thresholds updated successfully");
|
|
673
|
+
return { ok: true };
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
registerCommand({
|
|
677
|
+
id: "actions.import",
|
|
678
|
+
timeoutMs: 30000,
|
|
679
|
+
allowInUi: true,
|
|
680
|
+
validateInput(payload) {
|
|
681
|
+
const p = payload;
|
|
682
|
+
if (!p || typeof p.source !== "string" || !p.source) {
|
|
683
|
+
throw new Error("actions.import requires a non-empty 'source' string");
|
|
684
|
+
}
|
|
685
|
+
if (p.data === undefined) {
|
|
686
|
+
throw new Error("actions.import requires a 'data' field");
|
|
687
|
+
}
|
|
688
|
+
return { source: p.source, data: p.data };
|
|
689
|
+
},
|
|
690
|
+
async run(ctx, input) {
|
|
691
|
+
ctx.log("info", `Importing config into "${input.source}"`);
|
|
692
|
+
const { backupPath } = await writeConfig(input.source, input.data);
|
|
693
|
+
ctx.log("info", `Config imported, backup at ${backupPath}`);
|
|
694
|
+
return { ok: true, backupPath };
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
registerCommand({
|
|
698
|
+
id: "actions.export",
|
|
699
|
+
timeoutMs: 30000,
|
|
700
|
+
allowInUi: true,
|
|
701
|
+
validateInput(payload) {
|
|
702
|
+
const p = payload;
|
|
703
|
+
if (!p || typeof p.source !== "string" || !p.source) {
|
|
704
|
+
throw new Error("actions.export requires a non-empty 'source' string");
|
|
705
|
+
}
|
|
706
|
+
return { source: p.source };
|
|
707
|
+
},
|
|
708
|
+
async run(ctx, input) {
|
|
709
|
+
ctx.log("info", `Exporting config from "${input.source}"`);
|
|
710
|
+
const data = await readConfig(input.source);
|
|
711
|
+
ctx.log("info", "Config exported successfully");
|
|
712
|
+
return { source: input.source, data };
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
registerCommand({
|
|
716
|
+
id: "actions.reset",
|
|
717
|
+
timeoutMs: 30000,
|
|
718
|
+
allowInUi: true,
|
|
719
|
+
validateInput(payload) {
|
|
720
|
+
const p = payload;
|
|
721
|
+
if (!p || typeof p.source !== "string" || !p.source) {
|
|
722
|
+
throw new Error("actions.reset requires a non-empty 'source' string");
|
|
723
|
+
}
|
|
724
|
+
return { source: p.source };
|
|
725
|
+
},
|
|
726
|
+
async run(ctx, input) {
|
|
727
|
+
ctx.log("info", `Resetting config "${input.source}" to empty object`);
|
|
728
|
+
const { backupPath } = await writeConfig(input.source, {});
|
|
729
|
+
ctx.log("info", `Config reset, backup at ${backupPath}`);
|
|
730
|
+
return { ok: true, backupPath };
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
registerCommand({
|
|
734
|
+
id: "actions.rollback",
|
|
735
|
+
timeoutMs: 30000,
|
|
736
|
+
allowInUi: true,
|
|
737
|
+
validateInput(payload) {
|
|
738
|
+
const p = payload;
|
|
739
|
+
if (!p || typeof p.source !== "string" || !p.source) {
|
|
740
|
+
throw new Error("actions.rollback requires a non-empty 'source' string");
|
|
741
|
+
}
|
|
742
|
+
return { source: p.source };
|
|
743
|
+
},
|
|
744
|
+
async run(ctx, input) {
|
|
745
|
+
ctx.log("info", `Rolling back config "${input.source}" to latest backup`);
|
|
746
|
+
const { restoredFrom } = await rollbackConfig(input.source);
|
|
747
|
+
ctx.log("info", `Config restored from ${restoredFrom}`);
|
|
748
|
+
return { ok: true, restoredFrom };
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
3
752
|
|
|
4
753
|
// src/cli.ts
|
|
5
754
|
import { parseArgs as nodeParseArgs } from "util";
|
|
@@ -48,6 +797,8 @@ function parseArgs() {
|
|
|
48
797
|
watch: { type: "boolean", short: "w" },
|
|
49
798
|
stats: { type: "boolean", short: "S" },
|
|
50
799
|
config: { type: "string" },
|
|
800
|
+
commander: { type: "boolean" },
|
|
801
|
+
"commander-port": { type: "string" },
|
|
51
802
|
help: { type: "boolean", short: "h" }
|
|
52
803
|
},
|
|
53
804
|
strict: true
|
|
@@ -65,7 +816,9 @@ function parseArgs() {
|
|
|
65
816
|
monthly: values.monthly,
|
|
66
817
|
watch: values.watch,
|
|
67
818
|
stats: values.stats,
|
|
68
|
-
config: values.config
|
|
819
|
+
config: values.config,
|
|
820
|
+
commander: values.commander,
|
|
821
|
+
commanderPort: values["commander-port"] ? parseInt(values["commander-port"], 10) : undefined
|
|
69
822
|
};
|
|
70
823
|
} catch (error) {
|
|
71
824
|
if (error instanceof Error && error.message.includes("Unknown option")) {
|
|
@@ -86,6 +839,7 @@ Usage:
|
|
|
86
839
|
Modes:
|
|
87
840
|
(default) Interactive dashboard (Bun only)
|
|
88
841
|
-S, --stats Stats table mode (works with Node.js too)
|
|
842
|
+
--commander Start Commander web server (Bun only)
|
|
89
843
|
|
|
90
844
|
Options:
|
|
91
845
|
-p, --provider <name> Filter by provider (anthropic, openai, google, opencode)
|
|
@@ -97,6 +851,7 @@ Options:
|
|
|
97
851
|
-w, --watch Watch mode - refresh every 5 minutes (stats mode only)
|
|
98
852
|
--config show Show current configuration
|
|
99
853
|
-h, --help Show this help message
|
|
854
|
+
--commander-port <n> Commander server port (default: 3000)
|
|
100
855
|
|
|
101
856
|
Codex Quota:
|
|
102
857
|
Dashboard auto-reads Codex auth from ~/.codex/auth.json.
|
|
@@ -28566,8 +29321,8 @@ function UsageTable(props) {
|
|
|
28566
29321
|
})();
|
|
28567
29322
|
})(), null);
|
|
28568
29323
|
insertNode(_el$7, _el$8);
|
|
28569
|
-
setProp(_el$7, "
|
|
28570
|
-
setProp(_el$7, "
|
|
29324
|
+
setProp(_el$7, "paddingTop", 1);
|
|
29325
|
+
setProp(_el$7, "paddingBottom", 1);
|
|
28571
29326
|
setProp(_el$7, "border-bottom", true);
|
|
28572
29327
|
insertNode(_el$8, _el$9);
|
|
28573
29328
|
setProp(_el$8, "wrapMode", "none");
|
|
@@ -28585,7 +29340,7 @@ function UsageTable(props) {
|
|
|
28585
29340
|
return [(() => {
|
|
28586
29341
|
var _el$26 = createElement("box"), _el$27 = createElement("text"), _el$28 = createElement("span"), _el$29 = createElement("span"), _el$30 = createElement("span");
|
|
28587
29342
|
insertNode(_el$26, _el$27);
|
|
28588
|
-
setProp(_el$26, "
|
|
29343
|
+
setProp(_el$26, "paddingTop", 0.5);
|
|
28589
29344
|
insertNode(_el$27, _el$28);
|
|
28590
29345
|
insertNode(_el$27, _el$29);
|
|
28591
29346
|
insertNode(_el$27, _el$30);
|
|
@@ -28605,8 +29360,8 @@ function UsageTable(props) {
|
|
|
28605
29360
|
fg: COLORS.accent.amber,
|
|
28606
29361
|
bold: true
|
|
28607
29362
|
};
|
|
28608
|
-
_v$1 !== _p$.e && (_p$.e = setProp(_el$26, "
|
|
28609
|
-
_v$10 !== _p$.t && (_p$.t = setProp(_el$26, "
|
|
29363
|
+
_v$1 !== _p$.e && (_p$.e = setProp(_el$26, "paddingBottom", _v$1, _p$.e));
|
|
29364
|
+
_v$10 !== _p$.t && (_p$.t = setProp(_el$26, "backgroundColor", _v$10, _p$.t));
|
|
28610
29365
|
_v$11 !== _p$.a && (_p$.a = setProp(_el$28, "style", _v$11, _p$.a));
|
|
28611
29366
|
_v$12 !== _p$.o && (_p$.o = setProp(_el$29, "style", _v$12, _p$.o));
|
|
28612
29367
|
_v$13 !== _p$.i && (_p$.i = setProp(_el$30, "style", _v$13, _p$.i));
|
|
@@ -28626,9 +29381,9 @@ function UsageTable(props) {
|
|
|
28626
29381
|
return (() => {
|
|
28627
29382
|
var _el$31 = createElement("box"), _el$32 = createElement("text"), _el$33 = createElement("span"), _el$34 = createElement("span"), _el$35 = createElement("span");
|
|
28628
29383
|
insertNode(_el$31, _el$32);
|
|
28629
|
-
setProp(_el$31, "
|
|
28630
|
-
setProp(_el$31, "
|
|
28631
|
-
setProp(_el$31, "
|
|
29384
|
+
setProp(_el$31, "paddingTop", 0.25);
|
|
29385
|
+
setProp(_el$31, "paddingBottom", isLastProvider ? 0.5 : 0.25);
|
|
29386
|
+
setProp(_el$31, "paddingLeft", 2);
|
|
28632
29387
|
insertNode(_el$32, _el$33);
|
|
28633
29388
|
insertNode(_el$32, _el$34);
|
|
28634
29389
|
insertNode(_el$32, _el$35);
|
|
@@ -28645,7 +29400,7 @@ function UsageTable(props) {
|
|
|
28645
29400
|
}, _v$17 = {
|
|
28646
29401
|
fg: COLORS.accent.amber
|
|
28647
29402
|
};
|
|
28648
|
-
_v$14 !== _p$.e && (_p$.e = setProp(_el$31, "
|
|
29403
|
+
_v$14 !== _p$.e && (_p$.e = setProp(_el$31, "backgroundColor", _v$14, _p$.e));
|
|
28649
29404
|
_v$15 !== _p$.t && (_p$.t = setProp(_el$33, "style", _v$15, _p$.t));
|
|
28650
29405
|
_v$16 !== _p$.a && (_p$.a = setProp(_el$34, "style", _v$16, _p$.a));
|
|
28651
29406
|
_v$17 !== _p$.o && (_p$.o = setProp(_el$35, "style", _v$17, _p$.o));
|
|
@@ -28663,14 +29418,14 @@ function UsageTable(props) {
|
|
|
28663
29418
|
var _el$36 = createElement("box");
|
|
28664
29419
|
setProp(_el$36, "height", 1);
|
|
28665
29420
|
setProp(_el$36, "border-bottom", true);
|
|
28666
|
-
effect((_$p) => setProp(_el$36, "
|
|
29421
|
+
effect((_$p) => setProp(_el$36, "borderColor", COLORS.border, _$p));
|
|
28667
29422
|
return _el$36;
|
|
28668
29423
|
})()];
|
|
28669
29424
|
}
|
|
28670
29425
|
}), _el$0);
|
|
28671
29426
|
insertNode(_el$0, _el$1);
|
|
28672
|
-
setProp(_el$0, "
|
|
28673
|
-
setProp(_el$0, "
|
|
29427
|
+
setProp(_el$0, "paddingTop", 1);
|
|
29428
|
+
setProp(_el$0, "paddingBottom", 0.5);
|
|
28674
29429
|
setProp(_el$0, "border-top", true);
|
|
28675
29430
|
insertNode(_el$1, _el$10);
|
|
28676
29431
|
insertNode(_el$1, _el$11);
|
|
@@ -28694,9 +29449,9 @@ function UsageTable(props) {
|
|
|
28694
29449
|
_v$2 !== _p$.t && (_p$.t = setProp(_el$, "width", _v$2, _p$.t));
|
|
28695
29450
|
_v$3 !== _p$.a && (_p$.a = setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
|
28696
29451
|
_v$4 !== _p$.o && (_p$.o = setProp(_el$4, "fg", _v$4, _p$.o));
|
|
28697
|
-
_v$5 !== _p$.i && (_p$.i = setProp(_el$7, "
|
|
29452
|
+
_v$5 !== _p$.i && (_p$.i = setProp(_el$7, "borderColor", _v$5, _p$.i));
|
|
28698
29453
|
_v$6 !== _p$.n && (_p$.n = setProp(_el$8, "fg", _v$6, _p$.n));
|
|
28699
|
-
_v$7 !== _p$.s && (_p$.s = setProp(_el$0, "
|
|
29454
|
+
_v$7 !== _p$.s && (_p$.s = setProp(_el$0, "borderColor", _v$7, _p$.s));
|
|
28700
29455
|
_v$8 !== _p$.h && (_p$.h = setProp(_el$10, "style", _v$8, _p$.h));
|
|
28701
29456
|
_v$9 !== _p$.r && (_p$.r = setProp(_el$11, "style", _v$9, _p$.r));
|
|
28702
29457
|
_v$0 !== _p$.d && (_p$.d = setProp(_el$12, "style", _v$0, _p$.d));
|
|
@@ -28872,6 +29627,7 @@ function QuotaPanel(props) {
|
|
|
28872
29627
|
setProp(_el$52, "border", true);
|
|
28873
29628
|
setProp(_el$52, "borderStyle", "rounded");
|
|
28874
29629
|
setProp(_el$52, "borderColor", isActive ? "#14b8a6" : "#334155");
|
|
29630
|
+
setProp(_el$52, "flexGrow", 1);
|
|
28875
29631
|
insertNode(_el$53, _el$54);
|
|
28876
29632
|
setProp(_el$53, "paddingBottom", 0);
|
|
28877
29633
|
setProp(_el$53, "flexShrink", 0);
|
|
@@ -28912,7 +29668,7 @@ function QuotaPanel(props) {
|
|
|
28912
29668
|
const barWidth = compact ? 10 : 20;
|
|
28913
29669
|
const displayLabelText = compact ? truncateText(displayLabel, labelWidth) : displayLabel;
|
|
28914
29670
|
const resetText = formatResetTime(quota.resetAt, compact);
|
|
28915
|
-
const compactResetText =
|
|
29671
|
+
const compactResetText = resetText;
|
|
28916
29672
|
const errorText = compact ? truncateText(`${displayLabel}: ${quota.error ?? "Error"}`, 28) : `${displayLabel}: ${quota.error}`;
|
|
28917
29673
|
return createComponent2(Show, {
|
|
28918
29674
|
get when() {
|
|
@@ -28972,7 +29728,6 @@ function QuotaPanel(props) {
|
|
|
28972
29728
|
});
|
|
28973
29729
|
}
|
|
28974
29730
|
}), null);
|
|
28975
|
-
effect((_$p) => setProp(_el$52, "width", props.twoColumns ? "49%" : undefined, _$p));
|
|
28976
29731
|
return _el$52;
|
|
28977
29732
|
})();
|
|
28978
29733
|
}
|
|
@@ -29034,8 +29789,8 @@ function StatusBar(props) {
|
|
|
29034
29789
|
insertNode(_el$72, _el$73);
|
|
29035
29790
|
setProp(_el$72, "height", 1);
|
|
29036
29791
|
setProp(_el$72, "border-top", true);
|
|
29037
|
-
setProp(_el$72, "
|
|
29038
|
-
setProp(_el$72, "
|
|
29792
|
+
setProp(_el$72, "paddingLeft", 1);
|
|
29793
|
+
setProp(_el$72, "paddingRight", 1);
|
|
29039
29794
|
insertNode(_el$73, _el$74);
|
|
29040
29795
|
insertNode(_el$73, _el$76);
|
|
29041
29796
|
insertNode(_el$73, _el$78);
|
|
@@ -29054,8 +29809,8 @@ function StatusBar(props) {
|
|
|
29054
29809
|
}, _v$33 = {
|
|
29055
29810
|
fg: COLORS.text.muted
|
|
29056
29811
|
};
|
|
29057
|
-
_v$28 !== _p$.e && (_p$.e = setProp(_el$72, "
|
|
29058
|
-
_v$29 !== _p$.t && (_p$.t = setProp(_el$72, "
|
|
29812
|
+
_v$28 !== _p$.e && (_p$.e = setProp(_el$72, "borderColor", _v$28, _p$.e));
|
|
29813
|
+
_v$29 !== _p$.t && (_p$.t = setProp(_el$72, "backgroundColor", _v$29, _p$.t));
|
|
29059
29814
|
_v$30 !== _p$.a && (_p$.a = setProp(_el$73, "fg", _v$30, _p$.a));
|
|
29060
29815
|
_v$31 !== _p$.o && (_p$.o = setProp(_el$74, "style", _v$31, _p$.o));
|
|
29061
29816
|
_v$32 !== _p$.i && (_p$.i = setProp(_el$78, "style", _v$32, _p$.i));
|
|
@@ -29239,8 +29994,9 @@ function Dashboard(props) {
|
|
|
29239
29994
|
insertNode(_el$82, _el$83);
|
|
29240
29995
|
setProp(_el$82, "width", "100%");
|
|
29241
29996
|
setProp(_el$82, "height", "100%");
|
|
29242
|
-
setProp(_el$82, "
|
|
29243
|
-
setProp(_el$83, "
|
|
29997
|
+
setProp(_el$82, "flexDirection", "column");
|
|
29998
|
+
setProp(_el$83, "flexGrow", 1);
|
|
29999
|
+
setProp(_el$83, "width", "100%");
|
|
29244
30000
|
setProp(_el$83, "gap", 0);
|
|
29245
30001
|
setProp(_el$83, "padding", 0);
|
|
29246
30002
|
insert(_el$83, createComponent2(UsageTable, {
|
|
@@ -29299,8 +30055,8 @@ function Dashboard(props) {
|
|
|
29299
30055
|
}), null);
|
|
29300
30056
|
effect((_p$) => {
|
|
29301
30057
|
var _v$34 = COLORS.bg.primary, _v$35 = sideBySide() ? "row" : "column";
|
|
29302
|
-
_v$34 !== _p$.e && (_p$.e = setProp(_el$82, "
|
|
29303
|
-
_v$35 !== _p$.t && (_p$.t = setProp(_el$83, "
|
|
30058
|
+
_v$34 !== _p$.e && (_p$.e = setProp(_el$82, "backgroundColor", _v$34, _p$.e));
|
|
30059
|
+
_v$35 !== _p$.t && (_p$.t = setProp(_el$83, "flexDirection", _v$35, _p$.t));
|
|
29304
30060
|
return _p$;
|
|
29305
30061
|
}, {
|
|
29306
30062
|
e: undefined,
|
|
@@ -29385,6 +30141,573 @@ async function showConfig() {
|
|
|
29385
30141
|
}
|
|
29386
30142
|
var CODEX_AUTH_PATH2 = join6(homedir4(), ".codex", "auth.json");
|
|
29387
30143
|
|
|
30144
|
+
// src/commander/server.ts
|
|
30145
|
+
import { join as join10, dirname as dirname2 } from "path";
|
|
30146
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
30147
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
30148
|
+
|
|
30149
|
+
// src/commander/services/quota-service.ts
|
|
30150
|
+
async function getQuotaData() {
|
|
30151
|
+
const [anthropic, antigravity, codex] = await Promise.all([
|
|
30152
|
+
loadMultiAccountQuota(),
|
|
30153
|
+
loadAntigravityQuota(),
|
|
30154
|
+
loadCodexQuota()
|
|
30155
|
+
]);
|
|
30156
|
+
return { anthropic, antigravity, codex };
|
|
30157
|
+
}
|
|
30158
|
+
|
|
30159
|
+
// src/commander/server.ts
|
|
30160
|
+
init_command_runner();
|
|
30161
|
+
|
|
30162
|
+
// src/commander/services/action-service.ts
|
|
30163
|
+
function ensureActionsRegistered() {
|
|
30164
|
+
if (registered)
|
|
30165
|
+
return;
|
|
30166
|
+
registered = true;
|
|
30167
|
+
Promise.resolve().then(() => init_plugin_adapters());
|
|
30168
|
+
}
|
|
30169
|
+
var isBun5 = typeof globalThis.Bun !== "undefined";
|
|
30170
|
+
var registered = false;
|
|
30171
|
+
|
|
30172
|
+
// src/commander/services/app-init-service.ts
|
|
30173
|
+
init_command_runner();
|
|
30174
|
+
import { homedir as homedir7 } from "os";
|
|
30175
|
+
import { join as join9 } from "path";
|
|
30176
|
+
async function fileExists2(path2) {
|
|
30177
|
+
try {
|
|
30178
|
+
const file = Bun.file(path2);
|
|
30179
|
+
return await file.exists();
|
|
30180
|
+
} catch {
|
|
30181
|
+
return false;
|
|
30182
|
+
}
|
|
30183
|
+
}
|
|
30184
|
+
async function readOpencodeJson() {
|
|
30185
|
+
try {
|
|
30186
|
+
const file = Bun.file(OPENCODE_JSON_PATH);
|
|
30187
|
+
if (!await file.exists())
|
|
30188
|
+
return {};
|
|
30189
|
+
const text = await file.text();
|
|
30190
|
+
return JSON.parse(text);
|
|
30191
|
+
} catch {
|
|
30192
|
+
return {};
|
|
30193
|
+
}
|
|
30194
|
+
}
|
|
30195
|
+
function pluginListContains(config, pluginName) {
|
|
30196
|
+
const plugins = config.plugins;
|
|
30197
|
+
if (!Array.isArray(plugins))
|
|
30198
|
+
return false;
|
|
30199
|
+
return plugins.some((p) => typeof p === "string" && p.includes(pluginName));
|
|
30200
|
+
}
|
|
30201
|
+
async function patchPluginList(pluginEntry) {
|
|
30202
|
+
const config = await readOpencodeJson();
|
|
30203
|
+
if (!Array.isArray(config.plugins)) {
|
|
30204
|
+
config.plugins = [];
|
|
30205
|
+
}
|
|
30206
|
+
const plugins = config.plugins;
|
|
30207
|
+
if (plugins.includes(pluginEntry))
|
|
30208
|
+
return;
|
|
30209
|
+
plugins.push(pluginEntry);
|
|
30210
|
+
await Bun.write(OPENCODE_JSON_PATH, JSON.stringify(config, null, 2));
|
|
30211
|
+
}
|
|
30212
|
+
function spawnSyncCheck(cmd) {
|
|
30213
|
+
try {
|
|
30214
|
+
const result = Bun.spawnSync(cmd, {
|
|
30215
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
30216
|
+
});
|
|
30217
|
+
return result.exitCode === 0;
|
|
30218
|
+
} catch {
|
|
30219
|
+
return false;
|
|
30220
|
+
}
|
|
30221
|
+
}
|
|
30222
|
+
async function spawnAndLog(ctx, cmd, options) {
|
|
30223
|
+
ctx.log("info", `Running: ${cmd.join(" ")}`);
|
|
30224
|
+
try {
|
|
30225
|
+
const proc = Bun.spawn(cmd, {
|
|
30226
|
+
cwd: options?.cwd,
|
|
30227
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
30228
|
+
});
|
|
30229
|
+
const stdout = await new Response(proc.stdout).text();
|
|
30230
|
+
const stderr = await new Response(proc.stderr).text();
|
|
30231
|
+
await proc.exited;
|
|
30232
|
+
if (stdout.trim())
|
|
30233
|
+
ctx.log("info", stdout.trim());
|
|
30234
|
+
if (stderr.trim())
|
|
30235
|
+
ctx.log("warn", stderr.trim());
|
|
30236
|
+
return proc.exitCode === 0;
|
|
30237
|
+
} catch (err) {
|
|
30238
|
+
const message = err instanceof Error ? err.message : "Spawn failed";
|
|
30239
|
+
ctx.log("error", message);
|
|
30240
|
+
return false;
|
|
30241
|
+
}
|
|
30242
|
+
}
|
|
30243
|
+
async function detectOcCodexMultiAccount() {
|
|
30244
|
+
const binaryPath = join9(OPENCODE_CONFIG_DIR, "node_modules", ".bin", "oc-codex-multi-account");
|
|
30245
|
+
const binaryExists = await fileExists2(binaryPath);
|
|
30246
|
+
const config = await readOpencodeJson();
|
|
30247
|
+
const pluginConfigured = pluginListContains(config, "oc-codex-multi-account");
|
|
30248
|
+
let state;
|
|
30249
|
+
const details = [];
|
|
30250
|
+
if (binaryExists && pluginConfigured) {
|
|
30251
|
+
state = "ready";
|
|
30252
|
+
details.push("Binary installed", "Plugin configured");
|
|
30253
|
+
} else if (binaryExists || pluginConfigured) {
|
|
30254
|
+
state = "partial";
|
|
30255
|
+
if (!binaryExists)
|
|
30256
|
+
details.push("Binary missing");
|
|
30257
|
+
if (!pluginConfigured)
|
|
30258
|
+
details.push("Plugin not configured");
|
|
30259
|
+
} else {
|
|
30260
|
+
state = "not-installed";
|
|
30261
|
+
details.push("Binary not found", "Plugin not configured");
|
|
30262
|
+
}
|
|
30263
|
+
return {
|
|
30264
|
+
id: "oc-codex-multi-account",
|
|
30265
|
+
name: "OC Codex Multi-Account",
|
|
30266
|
+
description: "Multi-account support for OpenAI Codex via OpenCode plugin",
|
|
30267
|
+
state,
|
|
30268
|
+
details
|
|
30269
|
+
};
|
|
30270
|
+
}
|
|
30271
|
+
async function detectOcAnthropicMultiAccount() {
|
|
30272
|
+
const dirPath = "/Users/gabrielecegi/oc/oc-anthropic-multi-account";
|
|
30273
|
+
const statePath = join9(OPENCODE_CONFIG_DIR, "anthropic-multi-account-state.json");
|
|
30274
|
+
const dirExists = await fileExists2(dirPath);
|
|
30275
|
+
const stateExists = await fileExists2(statePath);
|
|
30276
|
+
let state;
|
|
30277
|
+
const details = [];
|
|
30278
|
+
if (dirExists && stateExists) {
|
|
30279
|
+
state = "ready";
|
|
30280
|
+
details.push("Project directory found", "State file present");
|
|
30281
|
+
} else if (dirExists) {
|
|
30282
|
+
state = "partial";
|
|
30283
|
+
details.push("Project directory found", "State file missing");
|
|
30284
|
+
} else {
|
|
30285
|
+
state = "not-installed";
|
|
30286
|
+
details.push("Project directory not found");
|
|
30287
|
+
if (!stateExists)
|
|
30288
|
+
details.push("State file missing");
|
|
30289
|
+
}
|
|
30290
|
+
return {
|
|
30291
|
+
id: "oc-anthropic-multi-account",
|
|
30292
|
+
name: "OC Anthropic Multi-Account",
|
|
30293
|
+
description: "Multi-account support for Anthropic via OpenCode plugin",
|
|
30294
|
+
state,
|
|
30295
|
+
details
|
|
30296
|
+
};
|
|
30297
|
+
}
|
|
30298
|
+
async function detectOpencodeGitbutler() {
|
|
30299
|
+
const butAvailable = spawnSyncCheck(["but", "--version"]);
|
|
30300
|
+
const config = await readOpencodeJson();
|
|
30301
|
+
const pluginConfigured = pluginListContains(config, "opencode-gitbutler");
|
|
30302
|
+
let state;
|
|
30303
|
+
const details = [];
|
|
30304
|
+
if (butAvailable && pluginConfigured) {
|
|
30305
|
+
state = "ready";
|
|
30306
|
+
details.push("GitButler CLI available", "Plugin configured");
|
|
30307
|
+
} else if (!butAvailable) {
|
|
30308
|
+
state = "missing-deps";
|
|
30309
|
+
details.push("GitButler CLI (but) not found");
|
|
30310
|
+
if (!pluginConfigured)
|
|
30311
|
+
details.push("Plugin not configured");
|
|
30312
|
+
} else {
|
|
30313
|
+
state = "partial";
|
|
30314
|
+
details.push("GitButler CLI available", "Plugin not configured");
|
|
30315
|
+
}
|
|
30316
|
+
return {
|
|
30317
|
+
id: "opencode-gitbutler",
|
|
30318
|
+
name: "OpenCode GitButler",
|
|
30319
|
+
description: "GitButler integration for OpenCode",
|
|
30320
|
+
state,
|
|
30321
|
+
details
|
|
30322
|
+
};
|
|
30323
|
+
}
|
|
30324
|
+
async function detectOpencodeUsage() {
|
|
30325
|
+
const available = spawnSyncCheck(["bunx", "opencode-usage", "--help"]);
|
|
30326
|
+
let state;
|
|
30327
|
+
const details = [];
|
|
30328
|
+
if (available) {
|
|
30329
|
+
state = "ready";
|
|
30330
|
+
details.push("opencode-usage available via bunx");
|
|
30331
|
+
} else {
|
|
30332
|
+
state = "not-installed";
|
|
30333
|
+
details.push("opencode-usage not available");
|
|
30334
|
+
}
|
|
30335
|
+
return {
|
|
30336
|
+
id: "opencode-usage",
|
|
30337
|
+
name: "OpenCode Usage",
|
|
30338
|
+
description: "CLI tool for tracking OpenCode AI usage and costs",
|
|
30339
|
+
state,
|
|
30340
|
+
details
|
|
30341
|
+
};
|
|
30342
|
+
}
|
|
30343
|
+
async function getAppCatalog() {
|
|
30344
|
+
const results = await Promise.all([
|
|
30345
|
+
detectOcCodexMultiAccount(),
|
|
30346
|
+
detectOcAnthropicMultiAccount(),
|
|
30347
|
+
detectOpencodeGitbutler(),
|
|
30348
|
+
detectOpencodeUsage()
|
|
30349
|
+
]);
|
|
30350
|
+
return results;
|
|
30351
|
+
}
|
|
30352
|
+
function validateAppInput(payload) {
|
|
30353
|
+
if (typeof payload !== "object" || payload === null || !("appId" in payload)) {
|
|
30354
|
+
throw new Error('Missing "appId" in payload');
|
|
30355
|
+
}
|
|
30356
|
+
const { appId } = payload;
|
|
30357
|
+
if (typeof appId !== "string" || !VALID_APP_IDS.has(appId)) {
|
|
30358
|
+
throw new Error(`Invalid appId: "${String(appId)}"`);
|
|
30359
|
+
}
|
|
30360
|
+
return { appId };
|
|
30361
|
+
}
|
|
30362
|
+
async function initOcCodexMultiAccount(ctx) {
|
|
30363
|
+
await spawnAndLog(ctx, [
|
|
30364
|
+
"bun",
|
|
30365
|
+
"add",
|
|
30366
|
+
"oc-codex-multi-account",
|
|
30367
|
+
"--cwd",
|
|
30368
|
+
OPENCODE_CONFIG_DIR
|
|
30369
|
+
]);
|
|
30370
|
+
await patchPluginList("oc-codex-multi-account@latest");
|
|
30371
|
+
ctx.log("info", "Plugin configured in opencode.json");
|
|
30372
|
+
ctx.log("info", `To add accounts, run: ${join9(OPENCODE_CONFIG_DIR, "node_modules", ".bin", "oc-codex-multi-account")} add <alias>`);
|
|
30373
|
+
}
|
|
30374
|
+
async function initOcAnthropicMultiAccount(ctx) {
|
|
30375
|
+
const dirPath = "/Users/gabrielecegi/oc/oc-anthropic-multi-account";
|
|
30376
|
+
const dirExists = await fileExists2(dirPath);
|
|
30377
|
+
if (!dirExists) {
|
|
30378
|
+
ctx.log("error", `Project directory not found: ${dirPath}. Clone the repo first.`);
|
|
30379
|
+
return;
|
|
30380
|
+
}
|
|
30381
|
+
await spawnAndLog(ctx, ["bun", "install"], { cwd: dirPath });
|
|
30382
|
+
await patchPluginList("oc-anthropic-multi-account@latest");
|
|
30383
|
+
ctx.log("info", "Plugin configured in opencode.json");
|
|
30384
|
+
ctx.log("info", `To add accounts, run: cd ${dirPath} && bun src/cli.ts add primary`);
|
|
30385
|
+
}
|
|
30386
|
+
async function initOpencodeGitbutler(ctx) {
|
|
30387
|
+
await patchPluginList("opencode-gitbutler@latest");
|
|
30388
|
+
ctx.log("info", "Plugin configured in opencode.json");
|
|
30389
|
+
ctx.log("info", "To install GitButler CLI, run: brew install gitbutler");
|
|
30390
|
+
}
|
|
30391
|
+
async function initOpencodeUsage(ctx) {
|
|
30392
|
+
const ok = await spawnAndLog(ctx, ["bunx", "opencode-usage", "--help"]);
|
|
30393
|
+
if (ok) {
|
|
30394
|
+
ctx.log("info", "opencode-usage is working correctly");
|
|
30395
|
+
}
|
|
30396
|
+
ctx.log("info", "opencode-usage is available via bunx. For global install: bun add -g opencode-usage");
|
|
30397
|
+
}
|
|
30398
|
+
async function runInit(ctx, input) {
|
|
30399
|
+
ctx.log("info", `Initializing app: ${input.appId}`);
|
|
30400
|
+
switch (input.appId) {
|
|
30401
|
+
case "oc-codex-multi-account":
|
|
30402
|
+
await initOcCodexMultiAccount(ctx);
|
|
30403
|
+
break;
|
|
30404
|
+
case "oc-anthropic-multi-account":
|
|
30405
|
+
await initOcAnthropicMultiAccount(ctx);
|
|
30406
|
+
break;
|
|
30407
|
+
case "opencode-gitbutler":
|
|
30408
|
+
await initOpencodeGitbutler(ctx);
|
|
30409
|
+
break;
|
|
30410
|
+
case "opencode-usage":
|
|
30411
|
+
await initOpencodeUsage(ctx);
|
|
30412
|
+
break;
|
|
30413
|
+
}
|
|
30414
|
+
return { ok: true };
|
|
30415
|
+
}
|
|
30416
|
+
async function runRepair(ctx, input) {
|
|
30417
|
+
ctx.log("info", `Repairing app: ${input.appId}`);
|
|
30418
|
+
const catalog = await getAppCatalog();
|
|
30419
|
+
const app = catalog.find((a) => a.id === input.appId);
|
|
30420
|
+
if (!app) {
|
|
30421
|
+
ctx.log("error", `App not found: ${input.appId}`);
|
|
30422
|
+
return { ok: false, state: "not-installed" };
|
|
30423
|
+
}
|
|
30424
|
+
ctx.log("info", `Current state: ${app.state} \u2014 ${app.details.join(", ")}`);
|
|
30425
|
+
if (app.state === "ready") {
|
|
30426
|
+
ctx.log("info", "App is already in ready state, no repair needed");
|
|
30427
|
+
return { ok: true, state: "ready" };
|
|
30428
|
+
}
|
|
30429
|
+
ctx.log("info", "Attempting repair via init workflow...");
|
|
30430
|
+
await runInit(ctx, input);
|
|
30431
|
+
const afterCatalog = await getAppCatalog();
|
|
30432
|
+
const afterApp = afterCatalog.find((a) => a.id === input.appId);
|
|
30433
|
+
const finalState = afterApp?.state ?? "not-installed";
|
|
30434
|
+
ctx.log("info", `Post-repair state: ${finalState}`);
|
|
30435
|
+
return { ok: finalState === "ready", state: finalState };
|
|
30436
|
+
}
|
|
30437
|
+
function ensureAppCommandsRegistered() {
|
|
30438
|
+
if (registered2)
|
|
30439
|
+
return;
|
|
30440
|
+
registered2 = true;
|
|
30441
|
+
registerCommand({
|
|
30442
|
+
id: "apps.init",
|
|
30443
|
+
validateInput: validateAppInput,
|
|
30444
|
+
run: runInit,
|
|
30445
|
+
timeoutMs: 120000,
|
|
30446
|
+
allowInUi: true
|
|
30447
|
+
});
|
|
30448
|
+
registerCommand({
|
|
30449
|
+
id: "apps.repair",
|
|
30450
|
+
validateInput: validateAppInput,
|
|
30451
|
+
run: runRepair,
|
|
30452
|
+
timeoutMs: 120000,
|
|
30453
|
+
allowInUi: true
|
|
30454
|
+
});
|
|
30455
|
+
}
|
|
30456
|
+
var isBun6 = typeof globalThis.Bun !== "undefined";
|
|
30457
|
+
var OPENCODE_CONFIG_DIR = join9(homedir7(), ".config", "opencode");
|
|
30458
|
+
var OPENCODE_JSON_PATH = join9(OPENCODE_CONFIG_DIR, "opencode.json");
|
|
30459
|
+
var VALID_APP_IDS = new Set([
|
|
30460
|
+
"oc-codex-multi-account",
|
|
30461
|
+
"oc-anthropic-multi-account",
|
|
30462
|
+
"opencode-gitbutler",
|
|
30463
|
+
"opencode-usage"
|
|
30464
|
+
]);
|
|
30465
|
+
var registered2 = false;
|
|
30466
|
+
|
|
30467
|
+
// src/commander/server.ts
|
|
30468
|
+
init_config_service();
|
|
30469
|
+
function queryUsageInWorker(opts) {
|
|
30470
|
+
return new Promise((resolve3, reject) => {
|
|
30471
|
+
const worker = new Worker(usageWorkerPath);
|
|
30472
|
+
worker.onmessage = (event) => {
|
|
30473
|
+
worker.terminate();
|
|
30474
|
+
const msg = event.data;
|
|
30475
|
+
if (msg.ok)
|
|
30476
|
+
resolve3(msg.data);
|
|
30477
|
+
else
|
|
30478
|
+
reject(new Error(msg.error));
|
|
30479
|
+
};
|
|
30480
|
+
worker.onerror = (err) => {
|
|
30481
|
+
worker.terminate();
|
|
30482
|
+
reject(err);
|
|
30483
|
+
};
|
|
30484
|
+
worker.postMessage(opts);
|
|
30485
|
+
});
|
|
30486
|
+
}
|
|
30487
|
+
async function runCommanderServer(args) {
|
|
30488
|
+
if (!isBun7) {
|
|
30489
|
+
console.error("Commander mode requires Bun runtime.");
|
|
30490
|
+
process.exit(1);
|
|
30491
|
+
}
|
|
30492
|
+
ensureActionsRegistered();
|
|
30493
|
+
ensureAppCommandsRegistered();
|
|
30494
|
+
await listConfigFiles();
|
|
30495
|
+
const port = args.commanderPort ?? DEFAULT_PORT;
|
|
30496
|
+
const hostname = "127.0.0.1";
|
|
30497
|
+
Bun.serve({
|
|
30498
|
+
hostname,
|
|
30499
|
+
port,
|
|
30500
|
+
async fetch(req) {
|
|
30501
|
+
const url = new URL(req.url);
|
|
30502
|
+
if (req.method === "GET" && url.pathname === "/api/health") {
|
|
30503
|
+
return Response.json({
|
|
30504
|
+
status: "ok",
|
|
30505
|
+
version: PKG_VERSION,
|
|
30506
|
+
timestamp: new Date().toISOString()
|
|
30507
|
+
});
|
|
30508
|
+
}
|
|
30509
|
+
if (req.method === "GET" && url.pathname === "/api/usage") {
|
|
30510
|
+
try {
|
|
30511
|
+
const provider = url.searchParams.get("provider") ?? undefined;
|
|
30512
|
+
const daysParam = url.searchParams.get("days");
|
|
30513
|
+
const days = daysParam !== null ? Number(daysParam) : undefined;
|
|
30514
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
30515
|
+
const until = url.searchParams.get("until") ?? undefined;
|
|
30516
|
+
const monthly = url.searchParams.get("monthly") === "true";
|
|
30517
|
+
const data = await queryUsageInWorker({
|
|
30518
|
+
provider,
|
|
30519
|
+
days,
|
|
30520
|
+
since,
|
|
30521
|
+
until,
|
|
30522
|
+
monthly
|
|
30523
|
+
});
|
|
30524
|
+
return Response.json(data);
|
|
30525
|
+
} catch (err) {
|
|
30526
|
+
return Response.json({
|
|
30527
|
+
error: err instanceof Error ? err.message : "Internal server error"
|
|
30528
|
+
}, { status: 500 });
|
|
30529
|
+
}
|
|
30530
|
+
}
|
|
30531
|
+
if (req.method === "GET" && url.pathname === "/api/quota") {
|
|
30532
|
+
try {
|
|
30533
|
+
const data = await getQuotaData();
|
|
30534
|
+
return Response.json(data);
|
|
30535
|
+
} catch (err) {
|
|
30536
|
+
return Response.json({
|
|
30537
|
+
error: err instanceof Error ? err.message : "Internal server error"
|
|
30538
|
+
}, { status: 500 });
|
|
30539
|
+
}
|
|
30540
|
+
}
|
|
30541
|
+
if (req.method === "POST" && url.pathname === "/api/commands/run") {
|
|
30542
|
+
try {
|
|
30543
|
+
const body = await req.json();
|
|
30544
|
+
if (typeof body.commandId !== "string" || !body.commandId) {
|
|
30545
|
+
return Response.json({ error: "Missing or invalid commandId" }, { status: 400 });
|
|
30546
|
+
}
|
|
30547
|
+
const jobId = runCommand(body.commandId, body.payload);
|
|
30548
|
+
return Response.json({ jobId }, { status: 202 });
|
|
30549
|
+
} catch (err) {
|
|
30550
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30551
|
+
return Response.json({ error: message }, { status: 400 });
|
|
30552
|
+
}
|
|
30553
|
+
}
|
|
30554
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/jobs/")) {
|
|
30555
|
+
const jobId = url.pathname.slice("/api/jobs/".length);
|
|
30556
|
+
const job = getJob(jobId);
|
|
30557
|
+
if (!job) {
|
|
30558
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
30559
|
+
}
|
|
30560
|
+
return Response.json(job);
|
|
30561
|
+
}
|
|
30562
|
+
if (req.method === "GET" && url.pathname === "/api/config/files") {
|
|
30563
|
+
try {
|
|
30564
|
+
const files = await listConfigFiles();
|
|
30565
|
+
return Response.json(files);
|
|
30566
|
+
} catch (err) {
|
|
30567
|
+
return Response.json({
|
|
30568
|
+
error: err instanceof Error ? err.message : "Internal server error"
|
|
30569
|
+
}, { status: 500 });
|
|
30570
|
+
}
|
|
30571
|
+
}
|
|
30572
|
+
if (req.method === "GET" && url.pathname.startsWith("/api/config/") && url.pathname !== "/api/config/files") {
|
|
30573
|
+
const source = url.pathname.slice("/api/config/".length);
|
|
30574
|
+
if (!isValidSource(source)) {
|
|
30575
|
+
return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
|
|
30576
|
+
}
|
|
30577
|
+
try {
|
|
30578
|
+
const data = readConfig(source);
|
|
30579
|
+
return Response.json(data);
|
|
30580
|
+
} catch (err) {
|
|
30581
|
+
const status = err instanceof ConfigError ? err.status : 500;
|
|
30582
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30583
|
+
return Response.json({ error: message }, { status });
|
|
30584
|
+
}
|
|
30585
|
+
}
|
|
30586
|
+
if (req.method === "PUT" && url.pathname.startsWith("/api/config/")) {
|
|
30587
|
+
const source = url.pathname.slice("/api/config/".length);
|
|
30588
|
+
if (!isValidSource(source)) {
|
|
30589
|
+
return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
|
|
30590
|
+
}
|
|
30591
|
+
try {
|
|
30592
|
+
const body = await req.json();
|
|
30593
|
+
const { backupPath } = await writeConfig(source, body);
|
|
30594
|
+
return Response.json({ ok: true, backupPath });
|
|
30595
|
+
} catch (err) {
|
|
30596
|
+
if (err instanceof SyntaxError) {
|
|
30597
|
+
return Response.json({ error: "Request body is not valid JSON" }, { status: 400 });
|
|
30598
|
+
}
|
|
30599
|
+
const status = err instanceof ConfigError ? err.status : 500;
|
|
30600
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30601
|
+
return Response.json({ error: message }, { status });
|
|
30602
|
+
}
|
|
30603
|
+
}
|
|
30604
|
+
if (req.method === "POST" && url.pathname.endsWith("/rollback") && url.pathname.startsWith("/api/config/")) {
|
|
30605
|
+
const source = url.pathname.slice("/api/config/".length).replace(/\/rollback$/, "");
|
|
30606
|
+
if (!isValidSource(source)) {
|
|
30607
|
+
return Response.json({ error: `Unknown config source: ${source}` }, { status: 404 });
|
|
30608
|
+
}
|
|
30609
|
+
try {
|
|
30610
|
+
const result = await rollbackConfig(source);
|
|
30611
|
+
return Response.json({ ok: true, ...result });
|
|
30612
|
+
} catch (err) {
|
|
30613
|
+
const status = err instanceof ConfigError ? err.status : 500;
|
|
30614
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30615
|
+
return Response.json({ error: message }, { status });
|
|
30616
|
+
}
|
|
30617
|
+
}
|
|
30618
|
+
if (req.method === "POST" && url.pathname.startsWith("/api/accounts/")) {
|
|
30619
|
+
const parts = url.pathname.split("/");
|
|
30620
|
+
const provider = parts[3];
|
|
30621
|
+
const action = parts[4];
|
|
30622
|
+
if (!provider || !action) {
|
|
30623
|
+
return Response.json({ error: "Invalid account route" }, { status: 400 });
|
|
30624
|
+
}
|
|
30625
|
+
try {
|
|
30626
|
+
const body = await req.json();
|
|
30627
|
+
const jobId = runCommand(`accounts.${action}`, {
|
|
30628
|
+
provider,
|
|
30629
|
+
...body
|
|
30630
|
+
});
|
|
30631
|
+
return Response.json({ jobId }, { status: 202 });
|
|
30632
|
+
} catch (err) {
|
|
30633
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30634
|
+
return Response.json({ error: message }, { status: 400 });
|
|
30635
|
+
}
|
|
30636
|
+
}
|
|
30637
|
+
if (req.method === "POST" && url.pathname.startsWith("/api/actions/")) {
|
|
30638
|
+
const action = url.pathname.slice("/api/actions/".length);
|
|
30639
|
+
if (!action) {
|
|
30640
|
+
return Response.json({ error: "Invalid action route" }, { status: 400 });
|
|
30641
|
+
}
|
|
30642
|
+
try {
|
|
30643
|
+
const body = await req.json();
|
|
30644
|
+
const jobId = runCommand(`actions.${action}`, body);
|
|
30645
|
+
return Response.json({ jobId }, { status: 202 });
|
|
30646
|
+
} catch (err) {
|
|
30647
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30648
|
+
return Response.json({ error: message }, { status: 400 });
|
|
30649
|
+
}
|
|
30650
|
+
}
|
|
30651
|
+
if (req.method === "GET" && url.pathname === "/api/apps") {
|
|
30652
|
+
try {
|
|
30653
|
+
const catalog = await getAppCatalog();
|
|
30654
|
+
return Response.json(catalog);
|
|
30655
|
+
} catch (err) {
|
|
30656
|
+
return Response.json({
|
|
30657
|
+
error: err instanceof Error ? err.message : "Internal server error"
|
|
30658
|
+
}, { status: 500 });
|
|
30659
|
+
}
|
|
30660
|
+
}
|
|
30661
|
+
if (req.method === "POST" && url.pathname.startsWith("/api/apps/") && url.pathname.endsWith("/init")) {
|
|
30662
|
+
const appId = url.pathname.slice("/api/apps/".length).replace(/\/init$/, "");
|
|
30663
|
+
try {
|
|
30664
|
+
const jobId = runCommand("apps.init", { appId });
|
|
30665
|
+
return Response.json({ jobId }, { status: 202 });
|
|
30666
|
+
} catch (err) {
|
|
30667
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30668
|
+
return Response.json({ error: message }, { status: 400 });
|
|
30669
|
+
}
|
|
30670
|
+
}
|
|
30671
|
+
if (req.method === "POST" && url.pathname.startsWith("/api/apps/") && url.pathname.endsWith("/repair")) {
|
|
30672
|
+
const appId = url.pathname.slice("/api/apps/".length).replace(/\/repair$/, "");
|
|
30673
|
+
try {
|
|
30674
|
+
const jobId = runCommand("apps.repair", { appId });
|
|
30675
|
+
return Response.json({ jobId }, { status: 202 });
|
|
30676
|
+
} catch (err) {
|
|
30677
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
30678
|
+
return Response.json({ error: message }, { status: 400 });
|
|
30679
|
+
}
|
|
30680
|
+
}
|
|
30681
|
+
if (!url.pathname.startsWith("/api/")) {
|
|
30682
|
+
const UI_DIST = join10(new URL(".", import.meta.url).pathname, "../commander-ui/dist");
|
|
30683
|
+
const filePath = url.pathname === "/" ? join10(UI_DIST, "index.html") : join10(UI_DIST, url.pathname);
|
|
30684
|
+
const file = Bun.file(filePath);
|
|
30685
|
+
if (await file.exists())
|
|
30686
|
+
return new Response(file);
|
|
30687
|
+
return new Response(Bun.file(join10(UI_DIST, "index.html")));
|
|
30688
|
+
}
|
|
30689
|
+
return Response.json({ error: "Not found" }, { status: 404 });
|
|
30690
|
+
}
|
|
30691
|
+
});
|
|
30692
|
+
const serverUrl = `http://${hostname}:${port}`;
|
|
30693
|
+
console.log(`Commander ready at ${serverUrl}`);
|
|
30694
|
+
if (isBun7 && !process.env.NO_OPEN) {
|
|
30695
|
+
const cmd = process.platform === "win32" ? ["cmd", "/c", "start", serverUrl] : process.platform === "darwin" ? ["open", serverUrl] : ["xdg-open", serverUrl];
|
|
30696
|
+
Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
|
|
30697
|
+
}
|
|
30698
|
+
}
|
|
30699
|
+
var isBun7 = typeof globalThis.Bun !== "undefined";
|
|
30700
|
+
var DEFAULT_PORT = 4466;
|
|
30701
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
30702
|
+
var PKG_VERSION = (() => {
|
|
30703
|
+
try {
|
|
30704
|
+
const pkg = JSON.parse(readFileSync2(join10(__dirname2, "..", "..", "package.json"), "utf-8"));
|
|
30705
|
+
return String(pkg.version ?? "0.0.0");
|
|
30706
|
+
} catch {
|
|
30707
|
+
return "0.0.0";
|
|
30708
|
+
}
|
|
30709
|
+
})();
|
|
30710
|
+
var usageWorkerPath = join10(__dirname2, "services", "usage-worker.ts");
|
|
29388
30711
|
// src/index.ts
|
|
29389
30712
|
function clearScreen() {
|
|
29390
30713
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
@@ -29434,7 +30757,23 @@ async function renderUsage(options, allMessages) {
|
|
|
29434
30757
|
}
|
|
29435
30758
|
}
|
|
29436
30759
|
async function main2() {
|
|
29437
|
-
const
|
|
30760
|
+
const args = parseArgs();
|
|
30761
|
+
const {
|
|
30762
|
+
provider,
|
|
30763
|
+
days,
|
|
30764
|
+
since,
|
|
30765
|
+
until,
|
|
30766
|
+
json,
|
|
30767
|
+
monthly,
|
|
30768
|
+
watch,
|
|
30769
|
+
stats,
|
|
30770
|
+
config,
|
|
30771
|
+
commander
|
|
30772
|
+
} = args;
|
|
30773
|
+
if (commander) {
|
|
30774
|
+
await runCommanderServer(args);
|
|
30775
|
+
return;
|
|
30776
|
+
}
|
|
29438
30777
|
if (config === "show") {
|
|
29439
30778
|
await showConfig();
|
|
29440
30779
|
return;
|
|
@@ -29479,4 +30818,4 @@ async function main2() {
|
|
|
29479
30818
|
var WATCH_INTERVAL_MS = 5 * 60 * 1000;
|
|
29480
30819
|
main2().catch(console.error);
|
|
29481
30820
|
|
|
29482
|
-
//# debugId=
|
|
30821
|
+
//# debugId=FEF96600978E6E5764756E2164756E21
|