opencode-usage 0.4.8 → 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 +1341 -3
- package/dist/index.js.map +12 -5
- 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.
|
|
@@ -29386,6 +30141,573 @@ async function showConfig() {
|
|
|
29386
30141
|
}
|
|
29387
30142
|
var CODEX_AUTH_PATH2 = join6(homedir4(), ".codex", "auth.json");
|
|
29388
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");
|
|
29389
30711
|
// src/index.ts
|
|
29390
30712
|
function clearScreen() {
|
|
29391
30713
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
@@ -29435,7 +30757,23 @@ async function renderUsage(options, allMessages) {
|
|
|
29435
30757
|
}
|
|
29436
30758
|
}
|
|
29437
30759
|
async function main2() {
|
|
29438
|
-
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
|
+
}
|
|
29439
30777
|
if (config === "show") {
|
|
29440
30778
|
await showConfig();
|
|
29441
30779
|
return;
|
|
@@ -29480,4 +30818,4 @@ async function main2() {
|
|
|
29480
30818
|
var WATCH_INTERVAL_MS = 5 * 60 * 1000;
|
|
29481
30819
|
main2().catch(console.error);
|
|
29482
30820
|
|
|
29483
|
-
//# debugId=
|
|
30821
|
+
//# debugId=FEF96600978E6E5764756E2164756E21
|