@dylanrussell/agent-router 1.0.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/LICENSE +21 -0
- package/README.md +211 -0
- package/assets/logo.svg +45 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +1259 -0
- package/dist/cli.js.map +1 -0
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.js +787 -0
- package/dist/plugin.js.map +1 -0
- package/dist/tui.d.ts +99 -0
- package/dist/tui.js +1048 -0
- package/dist/tui.js.map +1 -0
- package/package.json +95 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/core/errors.ts
|
|
13
|
+
var RouterError, StackNotFoundError, AgentFileError, ValidationError, ModelValidationError, IOError, UserError;
|
|
14
|
+
var init_errors = __esm({
|
|
15
|
+
"src/core/errors.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
RouterError = class extends Error {
|
|
18
|
+
/** CLI exit code for this error class. Subclasses override. */
|
|
19
|
+
static exitCode = 1;
|
|
20
|
+
name = "RouterError";
|
|
21
|
+
};
|
|
22
|
+
StackNotFoundError = class extends RouterError {
|
|
23
|
+
constructor(stackName, available) {
|
|
24
|
+
super(
|
|
25
|
+
`Stack "${stackName}" not found. Available: ${available.length ? available.join(", ") : "(none)"}.`
|
|
26
|
+
);
|
|
27
|
+
this.stackName = stackName;
|
|
28
|
+
this.available = available;
|
|
29
|
+
}
|
|
30
|
+
stackName;
|
|
31
|
+
available;
|
|
32
|
+
static exitCode = 2;
|
|
33
|
+
name = "StackNotFoundError";
|
|
34
|
+
};
|
|
35
|
+
AgentFileError = class extends RouterError {
|
|
36
|
+
constructor(agentName, filePath, reason) {
|
|
37
|
+
super(`Agent "${agentName}" (${filePath}): ${reason}`);
|
|
38
|
+
this.agentName = agentName;
|
|
39
|
+
this.filePath = filePath;
|
|
40
|
+
}
|
|
41
|
+
agentName;
|
|
42
|
+
filePath;
|
|
43
|
+
static exitCode = 2;
|
|
44
|
+
name = "AgentFileError";
|
|
45
|
+
};
|
|
46
|
+
ValidationError = class extends RouterError {
|
|
47
|
+
constructor(message, path9, issues) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.path = path9;
|
|
50
|
+
this.issues = issues;
|
|
51
|
+
}
|
|
52
|
+
path;
|
|
53
|
+
issues;
|
|
54
|
+
static exitCode = 4;
|
|
55
|
+
name = "ValidationError";
|
|
56
|
+
};
|
|
57
|
+
ModelValidationError = class extends RouterError {
|
|
58
|
+
constructor(stackName, missing) {
|
|
59
|
+
super(
|
|
60
|
+
`Stack "${stackName}" references ${missing.length} unreachable model ID${missing.length === 1 ? "" : "s"}.`
|
|
61
|
+
);
|
|
62
|
+
this.stackName = stackName;
|
|
63
|
+
this.missing = missing;
|
|
64
|
+
}
|
|
65
|
+
stackName;
|
|
66
|
+
missing;
|
|
67
|
+
static exitCode = 4;
|
|
68
|
+
name = "ModelValidationError";
|
|
69
|
+
};
|
|
70
|
+
IOError = class extends RouterError {
|
|
71
|
+
static exitCode = 3;
|
|
72
|
+
name = "IOError";
|
|
73
|
+
causedBy;
|
|
74
|
+
constructor(message, causedBy) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.causedBy = causedBy;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
UserError = class extends RouterError {
|
|
80
|
+
static exitCode = 1;
|
|
81
|
+
name = "UserError";
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/core/schema.ts
|
|
87
|
+
import { z } from "zod";
|
|
88
|
+
var StateFileSchema, AgentEntrySchema, StackFileSchema, ConfigFileSchema, OpencodeJsonSchema;
|
|
89
|
+
var init_schema = __esm({
|
|
90
|
+
"src/core/schema.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
StateFileSchema = z.object({
|
|
93
|
+
version: z.literal(1),
|
|
94
|
+
active: z.string().min(1),
|
|
95
|
+
previousActive: z.string().min(1).nullable(),
|
|
96
|
+
lastSwitchedAt: z.string().min(1)
|
|
97
|
+
}).strict();
|
|
98
|
+
AgentEntrySchema = z.object({
|
|
99
|
+
model: z.string().min(1)
|
|
100
|
+
}).passthrough();
|
|
101
|
+
StackFileSchema = z.object({
|
|
102
|
+
agents: z.record(z.string(), AgentEntrySchema)
|
|
103
|
+
}).passthrough().refine((s) => Object.keys(s.agents).length > 0, {
|
|
104
|
+
message: "Stack file must map at least one agent in `agents`."
|
|
105
|
+
});
|
|
106
|
+
ConfigFileSchema = z.object({
|
|
107
|
+
/** Where the agent .md files live. Default: `${opencodeConfigDir}/agents`. */
|
|
108
|
+
agentsDir: z.string().min(1).optional(),
|
|
109
|
+
/** Where named stacks live. Default: `${routerHome}/stacks`. */
|
|
110
|
+
stacksDir: z.string().min(1).optional()
|
|
111
|
+
}).strict();
|
|
112
|
+
OpencodeJsonSchema = z.object({
|
|
113
|
+
plugin: z.array(z.string()).optional()
|
|
114
|
+
}).passthrough();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// src/core/atomic-write.ts
|
|
119
|
+
import { randomBytes } from "crypto";
|
|
120
|
+
import { mkdir as mkdir2, realpath, rename, unlink, writeFile } from "fs/promises";
|
|
121
|
+
import path5 from "path";
|
|
122
|
+
async function resolveDestination(destPath) {
|
|
123
|
+
try {
|
|
124
|
+
return await realpath(destPath);
|
|
125
|
+
} catch {
|
|
126
|
+
try {
|
|
127
|
+
const dir = await realpath(path5.dirname(destPath));
|
|
128
|
+
return path5.join(dir, path5.basename(destPath));
|
|
129
|
+
} catch {
|
|
130
|
+
return destPath;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function atomicWriteFile(destPath, contents) {
|
|
135
|
+
await mkdir2(path5.dirname(destPath), { recursive: true });
|
|
136
|
+
const dest = await resolveDestination(destPath);
|
|
137
|
+
const dir = path5.dirname(dest);
|
|
138
|
+
const tmp = path5.join(
|
|
139
|
+
dir,
|
|
140
|
+
`.${path5.basename(dest)}.artmp-${process.pid}-${randomBytes(4).toString("hex")}`
|
|
141
|
+
);
|
|
142
|
+
try {
|
|
143
|
+
await writeFile(tmp, contents, { encoding: "utf8", mode: 420 });
|
|
144
|
+
await rename(tmp, dest);
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
await unlink(tmp).catch(() => {
|
|
147
|
+
});
|
|
148
|
+
throw new IOError(`Atomic write failed for ${destPath}: ${cause.message}`, cause);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function atomicWriteJson(destPath, value) {
|
|
152
|
+
const text = `${JSON.stringify(value, null, 2)}
|
|
153
|
+
`;
|
|
154
|
+
await atomicWriteFile(destPath, text);
|
|
155
|
+
}
|
|
156
|
+
var init_atomic_write = __esm({
|
|
157
|
+
"src/core/atomic-write.ts"() {
|
|
158
|
+
"use strict";
|
|
159
|
+
init_errors();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// src/core/state.ts
|
|
164
|
+
var state_exports = {};
|
|
165
|
+
__export(state_exports, {
|
|
166
|
+
readState: () => readState,
|
|
167
|
+
writeState: () => writeState
|
|
168
|
+
});
|
|
169
|
+
import { existsSync as existsSync5 } from "fs";
|
|
170
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
171
|
+
async function readState(statePath) {
|
|
172
|
+
if (!existsSync5(statePath)) return null;
|
|
173
|
+
let raw;
|
|
174
|
+
try {
|
|
175
|
+
raw = await readFile4(statePath, "utf8");
|
|
176
|
+
} catch (cause) {
|
|
177
|
+
throw new IOError(`Failed to read ${statePath}: ${cause.message}`, cause);
|
|
178
|
+
}
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(raw);
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
throw new ValidationError(
|
|
184
|
+
`state.json is not valid JSON: ${cause.message}`,
|
|
185
|
+
statePath
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const result = StateFileSchema.safeParse(parsed);
|
|
189
|
+
if (!result.success) {
|
|
190
|
+
throw new ValidationError(
|
|
191
|
+
`state.json failed validation: ${result.error.issues.map((i) => i.message).join("; ")}`,
|
|
192
|
+
statePath,
|
|
193
|
+
result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return result.data;
|
|
197
|
+
}
|
|
198
|
+
async function writeState(statePath, state) {
|
|
199
|
+
const result = StateFileSchema.safeParse(state);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
throw new ValidationError(
|
|
202
|
+
`Refusing to write invalid state.json: ${result.error.issues.map((i) => i.message).join("; ")}`,
|
|
203
|
+
statePath
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
await atomicWriteJson(statePath, result.data);
|
|
207
|
+
}
|
|
208
|
+
var init_state = __esm({
|
|
209
|
+
"src/core/state.ts"() {
|
|
210
|
+
"use strict";
|
|
211
|
+
init_atomic_write();
|
|
212
|
+
init_errors();
|
|
213
|
+
init_schema();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/cli.ts
|
|
218
|
+
import { spawnSync } from "child_process";
|
|
219
|
+
import { existsSync as existsSync7, realpathSync } from "fs";
|
|
220
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
221
|
+
import path8 from "path";
|
|
222
|
+
import { fileURLToPath } from "url";
|
|
223
|
+
import { cac } from "cac";
|
|
224
|
+
|
|
225
|
+
// src/core/backups.ts
|
|
226
|
+
init_errors();
|
|
227
|
+
import { existsSync } from "fs";
|
|
228
|
+
import { copyFile, mkdir } from "fs/promises";
|
|
229
|
+
import path from "path";
|
|
230
|
+
function timestampStamp(date = /* @__PURE__ */ new Date()) {
|
|
231
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
232
|
+
}
|
|
233
|
+
async function backupFiles(backupsRoot, files) {
|
|
234
|
+
const stamp = timestampStamp();
|
|
235
|
+
const dir = path.join(backupsRoot, stamp);
|
|
236
|
+
await mkdir(dir, { recursive: true });
|
|
237
|
+
for (const f of files) {
|
|
238
|
+
if (!existsSync(f)) continue;
|
|
239
|
+
try {
|
|
240
|
+
await copyFile(f, path.join(dir, path.basename(f)));
|
|
241
|
+
} catch (cause) {
|
|
242
|
+
throw new IOError(`Backup copy failed for ${f}: ${cause.message}`, cause);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return dir;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/core/config.ts
|
|
249
|
+
init_errors();
|
|
250
|
+
import { existsSync as existsSync2 } from "fs";
|
|
251
|
+
import { readFile } from "fs/promises";
|
|
252
|
+
import { homedir as homedir2 } from "os";
|
|
253
|
+
import path3 from "path";
|
|
254
|
+
|
|
255
|
+
// src/core/paths.ts
|
|
256
|
+
import { homedir } from "os";
|
|
257
|
+
import path2 from "path";
|
|
258
|
+
function resolvePaths(options = {}) {
|
|
259
|
+
const env = options.env ?? process.env;
|
|
260
|
+
const opencodeConfigDir = options.opencodeConfigDir ?? path2.join(env.XDG_CONFIG_HOME ?? path2.join(homedir(), ".config"), "opencode");
|
|
261
|
+
const routerHome = options.routerHome ?? env.AGENT_ROUTER_HOME ?? env.OMO_ROUTER_HOME ?? path2.join(opencodeConfigDir, "agent-router");
|
|
262
|
+
const agentsDir = options.agentsDir ?? env.AGENT_ROUTER_AGENTS_DIR ?? path2.join(opencodeConfigDir, "agents");
|
|
263
|
+
const stacksDir = options.stacksDir ?? env.AGENT_ROUTER_STACKS_DIR ?? path2.join(routerHome, "stacks");
|
|
264
|
+
return {
|
|
265
|
+
opencodeConfigDir,
|
|
266
|
+
opencodeJsonPath: path2.join(opencodeConfigDir, "opencode.json"),
|
|
267
|
+
tuiJsonPath: path2.join(opencodeConfigDir, "tui.json"),
|
|
268
|
+
agentsDir,
|
|
269
|
+
opencodeBackupsDir: path2.join(opencodeConfigDir, ".backups"),
|
|
270
|
+
routerHome,
|
|
271
|
+
statePath: path2.join(routerHome, "state.json"),
|
|
272
|
+
stacksDir,
|
|
273
|
+
historyDir: path2.join(routerHome, "history")
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/core/config.ts
|
|
278
|
+
init_schema();
|
|
279
|
+
var CONFIG_FILE_NAME = "config.json";
|
|
280
|
+
function expandHome(p) {
|
|
281
|
+
if (p === "~") return homedir2();
|
|
282
|
+
if (p.startsWith("~/")) return path3.join(homedir2(), p.slice(2));
|
|
283
|
+
return p;
|
|
284
|
+
}
|
|
285
|
+
async function readConfigFile(routerHome) {
|
|
286
|
+
const configPath = path3.join(routerHome, CONFIG_FILE_NAME);
|
|
287
|
+
if (!existsSync2(configPath)) return null;
|
|
288
|
+
let raw;
|
|
289
|
+
try {
|
|
290
|
+
raw = await readFile(configPath, "utf8");
|
|
291
|
+
} catch (cause) {
|
|
292
|
+
throw new IOError(`Failed to read ${configPath}: ${cause.message}`, cause);
|
|
293
|
+
}
|
|
294
|
+
let parsed;
|
|
295
|
+
try {
|
|
296
|
+
parsed = JSON.parse(raw);
|
|
297
|
+
} catch (cause) {
|
|
298
|
+
throw new ValidationError(
|
|
299
|
+
`agent-router config.json is not valid JSON: ${cause.message}`,
|
|
300
|
+
configPath
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const result = ConfigFileSchema.safeParse(parsed);
|
|
304
|
+
if (!result.success) {
|
|
305
|
+
throw new ValidationError(
|
|
306
|
+
`agent-router config.json failed validation: ${result.error.issues.map((i) => i.message).join("; ")}`,
|
|
307
|
+
configPath,
|
|
308
|
+
result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return result.data;
|
|
312
|
+
}
|
|
313
|
+
function normalizeConfigPath(routerHome, p) {
|
|
314
|
+
const expanded = expandHome(p);
|
|
315
|
+
return path3.isAbsolute(expanded) ? expanded : path3.resolve(routerHome, expanded);
|
|
316
|
+
}
|
|
317
|
+
async function resolvePathsWithConfig(options = {}) {
|
|
318
|
+
const base = resolvePaths(options);
|
|
319
|
+
const config = await readConfigFile(base.routerHome);
|
|
320
|
+
if (!config) return base;
|
|
321
|
+
const next = { ...options };
|
|
322
|
+
if (options.agentsDir === void 0 && config.agentsDir) {
|
|
323
|
+
next.agentsDir = normalizeConfigPath(
|
|
324
|
+
base.routerHome,
|
|
325
|
+
config.agentsDir
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (options.stacksDir === void 0 && config.stacksDir) {
|
|
329
|
+
next.stacksDir = normalizeConfigPath(
|
|
330
|
+
base.routerHome,
|
|
331
|
+
config.stacksDir
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return resolvePaths(next);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/cli.ts
|
|
338
|
+
init_errors();
|
|
339
|
+
|
|
340
|
+
// src/core/frontmatter.ts
|
|
341
|
+
init_errors();
|
|
342
|
+
import { existsSync as existsSync3 } from "fs";
|
|
343
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
344
|
+
import path4 from "path";
|
|
345
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
346
|
+
var MODEL_LINE_RE = /^model:[ \t]*(.*)$/m;
|
|
347
|
+
function cleanModelValue(raw) {
|
|
348
|
+
let v = raw;
|
|
349
|
+
const hash = v.search(/[ \t]#/);
|
|
350
|
+
if (hash >= 0) v = v.slice(0, hash);
|
|
351
|
+
v = v.trim();
|
|
352
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2 || v.startsWith("'") && v.endsWith("'") && v.length >= 2) {
|
|
353
|
+
v = v.slice(1, -1);
|
|
354
|
+
}
|
|
355
|
+
return v;
|
|
356
|
+
}
|
|
357
|
+
function getFrontmatterModel(content) {
|
|
358
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
359
|
+
if (!fm?.[1]) return null;
|
|
360
|
+
const line = MODEL_LINE_RE.exec(fm[1]);
|
|
361
|
+
if (!line) return null;
|
|
362
|
+
const value = cleanModelValue(line[1] ?? "");
|
|
363
|
+
return value.length > 0 ? value : null;
|
|
364
|
+
}
|
|
365
|
+
function setFrontmatterModel(content, model) {
|
|
366
|
+
const fm = FRONTMATTER_RE.exec(content);
|
|
367
|
+
if (!fm?.[1]) throw new Error("no frontmatter block");
|
|
368
|
+
const block = fm[1];
|
|
369
|
+
if (!MODEL_LINE_RE.test(block)) throw new Error("no `model:` line in frontmatter");
|
|
370
|
+
const nextBlock = block.replace(MODEL_LINE_RE, () => `model: ${model}`);
|
|
371
|
+
const blockStart = fm[0].indexOf(block);
|
|
372
|
+
const nextFm = fm[0].slice(0, blockStart) + nextBlock + fm[0].slice(blockStart + block.length);
|
|
373
|
+
return nextFm + content.slice(fm[0].length);
|
|
374
|
+
}
|
|
375
|
+
function agentFilePath(agentsDir, name) {
|
|
376
|
+
return path4.join(agentsDir, `${name}.md`);
|
|
377
|
+
}
|
|
378
|
+
async function listAgentFiles(agentsDir) {
|
|
379
|
+
if (!existsSync3(agentsDir)) return [];
|
|
380
|
+
let names;
|
|
381
|
+
try {
|
|
382
|
+
names = await readdir(agentsDir);
|
|
383
|
+
} catch (cause) {
|
|
384
|
+
throw new IOError(`Failed to read agents dir: ${cause.message}`, cause);
|
|
385
|
+
}
|
|
386
|
+
return names.filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -".md".length)).sort();
|
|
387
|
+
}
|
|
388
|
+
async function readAgentModels(agentsDir) {
|
|
389
|
+
const out = {};
|
|
390
|
+
for (const name of await listAgentFiles(agentsDir)) {
|
|
391
|
+
const filePath = agentFilePath(agentsDir, name);
|
|
392
|
+
let content;
|
|
393
|
+
try {
|
|
394
|
+
content = await readFile2(filePath, "utf8");
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
397
|
+
}
|
|
398
|
+
const model = getFrontmatterModel(content);
|
|
399
|
+
if (model !== null) out[name] = model;
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
async function readAgentFileStrict(agentsDir, name) {
|
|
404
|
+
const filePath = agentFilePath(agentsDir, name);
|
|
405
|
+
if (!existsSync3(filePath)) {
|
|
406
|
+
throw new AgentFileError(name, filePath, "file does not exist");
|
|
407
|
+
}
|
|
408
|
+
let content;
|
|
409
|
+
try {
|
|
410
|
+
content = await readFile2(filePath, "utf8");
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
413
|
+
}
|
|
414
|
+
const model = getFrontmatterModel(content);
|
|
415
|
+
if (model === null) {
|
|
416
|
+
throw new AgentFileError(name, filePath, "no frontmatter `model:` line to rewrite");
|
|
417
|
+
}
|
|
418
|
+
return { filePath, content, model };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/core/history.ts
|
|
422
|
+
init_atomic_write();
|
|
423
|
+
import { mkdir as mkdir3, readdir as readdir2, unlink as unlink2 } from "fs/promises";
|
|
424
|
+
import path6 from "path";
|
|
425
|
+
init_errors();
|
|
426
|
+
function filename(fromStack, toStack, when = /* @__PURE__ */ new Date()) {
|
|
427
|
+
const stamp = timestampStamp(when);
|
|
428
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9._()-]/g, "_");
|
|
429
|
+
return `${stamp}__${safe(fromStack)}-to-${safe(toStack)}.json`;
|
|
430
|
+
}
|
|
431
|
+
function parseHistoryFilename(name) {
|
|
432
|
+
if (!name.endsWith(".json")) return null;
|
|
433
|
+
const stem = name.slice(0, -".json".length);
|
|
434
|
+
const sep = stem.indexOf("__");
|
|
435
|
+
if (sep < 0) return null;
|
|
436
|
+
const stamp = stem.slice(0, sep);
|
|
437
|
+
const rest = stem.slice(sep + 2);
|
|
438
|
+
const toIdx = rest.lastIndexOf("-to-");
|
|
439
|
+
if (toIdx < 0) return null;
|
|
440
|
+
return {
|
|
441
|
+
id: stem,
|
|
442
|
+
timestamp: stamp,
|
|
443
|
+
fromStack: rest.slice(0, toIdx),
|
|
444
|
+
toStack: rest.slice(toIdx + "-to-".length)
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function appendHistory(historyDir, fromStack, toStack, displacedContent) {
|
|
448
|
+
await mkdir3(historyDir, { recursive: true });
|
|
449
|
+
const name = filename(fromStack, toStack);
|
|
450
|
+
await atomicWriteFile(path6.join(historyDir, name), displacedContent);
|
|
451
|
+
return name.slice(0, -".json".length);
|
|
452
|
+
}
|
|
453
|
+
async function listHistory(historyDir) {
|
|
454
|
+
let names;
|
|
455
|
+
try {
|
|
456
|
+
names = await readdir2(historyDir);
|
|
457
|
+
} catch (cause) {
|
|
458
|
+
if (cause.code === "ENOENT") return [];
|
|
459
|
+
throw new IOError(`Failed to read history dir: ${cause.message}`, cause);
|
|
460
|
+
}
|
|
461
|
+
const entries = [];
|
|
462
|
+
for (const n of names) {
|
|
463
|
+
const parsed = parseHistoryFilename(n);
|
|
464
|
+
if (parsed) entries.push({ ...parsed, path: path6.join(historyDir, n) });
|
|
465
|
+
}
|
|
466
|
+
entries.sort((a, b) => a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : 0);
|
|
467
|
+
return entries;
|
|
468
|
+
}
|
|
469
|
+
async function trimHistory(historyDir, keep = 20) {
|
|
470
|
+
const all = await listHistory(historyDir);
|
|
471
|
+
if (all.length <= keep) return [];
|
|
472
|
+
const toDelete = all.slice(keep);
|
|
473
|
+
const deleted = [];
|
|
474
|
+
for (const e of toDelete) {
|
|
475
|
+
try {
|
|
476
|
+
await unlink2(e.path);
|
|
477
|
+
deleted.push(e.id);
|
|
478
|
+
} catch (cause) {
|
|
479
|
+
void cause;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return deleted;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/core/opencode-config.ts
|
|
486
|
+
init_atomic_write();
|
|
487
|
+
init_errors();
|
|
488
|
+
init_schema();
|
|
489
|
+
import { existsSync as existsSync4 } from "fs";
|
|
490
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
491
|
+
var PLUGIN_NPM_NAME = "@dylanrussell/agent-router";
|
|
492
|
+
var PLUGIN_REGISTRY_ENTRY = `${PLUGIN_NPM_NAME}@latest`;
|
|
493
|
+
var LEGACY_PLUGIN_NPM_NAME = "@dylanrussell/omo-router";
|
|
494
|
+
async function readOpencodeJson(opencodeJsonPath) {
|
|
495
|
+
return readPluginConfigFile(opencodeJsonPath, "opencode.json");
|
|
496
|
+
}
|
|
497
|
+
async function readTuiJson(tuiJsonPath) {
|
|
498
|
+
return readPluginConfigFile(tuiJsonPath, "tui.json");
|
|
499
|
+
}
|
|
500
|
+
async function readPluginConfigFile(filePath, label) {
|
|
501
|
+
if (!existsSync4(filePath)) return null;
|
|
502
|
+
let raw;
|
|
503
|
+
try {
|
|
504
|
+
raw = await readFile3(filePath, "utf8");
|
|
505
|
+
} catch (cause) {
|
|
506
|
+
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
507
|
+
}
|
|
508
|
+
let parsed;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(raw);
|
|
511
|
+
} catch (cause) {
|
|
512
|
+
throw new ValidationError(`${label} is not valid JSON: ${cause.message}`, filePath);
|
|
513
|
+
}
|
|
514
|
+
const result = OpencodeJsonSchema.safeParse(parsed);
|
|
515
|
+
if (!result.success) {
|
|
516
|
+
throw new ValidationError(
|
|
517
|
+
`${label} failed validation: ${result.error.issues.map((i) => i.message).join("; ")}`,
|
|
518
|
+
filePath,
|
|
519
|
+
result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return result.data;
|
|
523
|
+
}
|
|
524
|
+
function ensurePluginEntry(config, entry = PLUGIN_REGISTRY_ENTRY) {
|
|
525
|
+
const existing = config.plugin ?? [];
|
|
526
|
+
const already = existing.some(
|
|
527
|
+
(p) => p === entry || stripVersionTag(p) === stripVersionTag(entry)
|
|
528
|
+
);
|
|
529
|
+
if (already) {
|
|
530
|
+
return { config, result: { added: false, plugin: existing } };
|
|
531
|
+
}
|
|
532
|
+
const next = [...existing, entry];
|
|
533
|
+
return {
|
|
534
|
+
config: { ...config, plugin: next },
|
|
535
|
+
result: { added: true, plugin: next }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function removePluginEntry(config, npmName = LEGACY_PLUGIN_NPM_NAME) {
|
|
539
|
+
const existing = config.plugin ?? [];
|
|
540
|
+
const removed = existing.filter((p) => stripVersionTag(p) === npmName);
|
|
541
|
+
if (removed.length === 0) {
|
|
542
|
+
return { config, result: { removed: [], plugin: existing } };
|
|
543
|
+
}
|
|
544
|
+
const next = existing.filter((p) => stripVersionTag(p) !== npmName);
|
|
545
|
+
return {
|
|
546
|
+
config: { ...config, plugin: next },
|
|
547
|
+
result: { removed, plugin: next }
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function stripVersionTag(entry) {
|
|
551
|
+
const lastAt = entry.lastIndexOf("@");
|
|
552
|
+
if (lastAt <= 0) return entry;
|
|
553
|
+
return entry.slice(0, lastAt);
|
|
554
|
+
}
|
|
555
|
+
async function writeOpencodeJson(opencodeJsonPath, config) {
|
|
556
|
+
await atomicWriteJson(opencodeJsonPath, config);
|
|
557
|
+
}
|
|
558
|
+
var TUI_JSON_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
559
|
+
async function ensureTuiJsonPluginEntry(tuiJsonPath, entry = PLUGIN_REGISTRY_ENTRY) {
|
|
560
|
+
const existing = await readTuiJson(tuiJsonPath) ?? { $schema: TUI_JSON_SCHEMA_URL };
|
|
561
|
+
const removed = removePluginEntry(existing);
|
|
562
|
+
const { config, result } = ensurePluginEntry(removed.config, entry);
|
|
563
|
+
if (result.added || removed.result.removed.length > 0) {
|
|
564
|
+
await atomicWriteJson(tuiJsonPath, config);
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/cli.ts
|
|
570
|
+
init_schema();
|
|
571
|
+
|
|
572
|
+
// src/core/stack-manager.ts
|
|
573
|
+
init_atomic_write();
|
|
574
|
+
init_errors();
|
|
575
|
+
import { existsSync as existsSync6 } from "fs";
|
|
576
|
+
import { copyFile as copyFile2, mkdir as mkdir4, readFile as readFile5, readdir as readdir3, unlink as unlink3 } from "fs/promises";
|
|
577
|
+
import path7 from "path";
|
|
578
|
+
init_schema();
|
|
579
|
+
init_state();
|
|
580
|
+
|
|
581
|
+
// src/core/validator.ts
|
|
582
|
+
init_errors();
|
|
583
|
+
import { execFile } from "child_process";
|
|
584
|
+
import { promisify } from "util";
|
|
585
|
+
var execFileAsync = promisify(execFile);
|
|
586
|
+
var DEFAULT_RUNNER = async () => {
|
|
587
|
+
const { stdout } = await execFileAsync("opencode", ["models"], {
|
|
588
|
+
encoding: "utf8",
|
|
589
|
+
timeout: 3e4,
|
|
590
|
+
maxBuffer: 4 * 1024 * 1024
|
|
591
|
+
});
|
|
592
|
+
return stdout;
|
|
593
|
+
};
|
|
594
|
+
function parseModelList(stdout) {
|
|
595
|
+
const out = /* @__PURE__ */ new Set();
|
|
596
|
+
for (const raw of stdout.split(/\r?\n/)) {
|
|
597
|
+
const line = raw.trim();
|
|
598
|
+
if (!line) continue;
|
|
599
|
+
if (line.startsWith("#")) continue;
|
|
600
|
+
out.add(line);
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
function collectModelRefs(stack) {
|
|
605
|
+
const refs = [];
|
|
606
|
+
for (const [k, v] of Object.entries(stack.agents)) {
|
|
607
|
+
refs.push({ path: `agents.${k}.model`, modelId: v.model });
|
|
608
|
+
}
|
|
609
|
+
return refs;
|
|
610
|
+
}
|
|
611
|
+
async function validateStack(stack, options = {}) {
|
|
612
|
+
const runner = options.runOpencodeModels ?? DEFAULT_RUNNER;
|
|
613
|
+
const stdout = await runner();
|
|
614
|
+
const available = parseModelList(stdout);
|
|
615
|
+
const refs = collectModelRefs(stack);
|
|
616
|
+
const missing = refs.filter((r) => !available.has(r.modelId));
|
|
617
|
+
return { ok: missing.length === 0, checked: refs.length, missing, available };
|
|
618
|
+
}
|
|
619
|
+
async function validateStackOrThrow(stackName, stack, options = {}) {
|
|
620
|
+
const result = await validateStack(stack, options);
|
|
621
|
+
if (!result.ok) throw new ModelValidationError(stackName, result.missing);
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/core/stack-manager.ts
|
|
626
|
+
async function listStacks(paths) {
|
|
627
|
+
if (!existsSync6(paths.stacksDir)) return [];
|
|
628
|
+
let names;
|
|
629
|
+
try {
|
|
630
|
+
names = await readdir3(paths.stacksDir);
|
|
631
|
+
} catch (cause) {
|
|
632
|
+
throw new IOError(`Failed to read stacks dir: ${cause.message}`, cause);
|
|
633
|
+
}
|
|
634
|
+
return names.filter((n) => n.endsWith(".json")).map((n) => n.slice(0, -".json".length)).sort();
|
|
635
|
+
}
|
|
636
|
+
function stackPath(paths, name) {
|
|
637
|
+
return path7.join(paths.stacksDir, `${name}.json`);
|
|
638
|
+
}
|
|
639
|
+
async function readStack(paths, name) {
|
|
640
|
+
const filePath = stackPath(paths, name);
|
|
641
|
+
if (!existsSync6(filePath)) {
|
|
642
|
+
throw new StackNotFoundError(name, await listStacks(paths));
|
|
643
|
+
}
|
|
644
|
+
const raw = await readFile5(filePath, "utf8").catch((cause) => {
|
|
645
|
+
throw new IOError(`Failed to read ${filePath}: ${cause.message}`, cause);
|
|
646
|
+
});
|
|
647
|
+
let parsed;
|
|
648
|
+
try {
|
|
649
|
+
parsed = JSON.parse(raw);
|
|
650
|
+
} catch (cause) {
|
|
651
|
+
throw new ValidationError(
|
|
652
|
+
`Stack "${name}" is not valid JSON: ${cause.message}`,
|
|
653
|
+
filePath
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const result = StackFileSchema.safeParse(parsed);
|
|
657
|
+
if (!result.success) {
|
|
658
|
+
throw new ValidationError(
|
|
659
|
+
`Stack "${name}" failed validation: ${result.error.issues.map((i) => i.message).join("; ")}`,
|
|
660
|
+
filePath,
|
|
661
|
+
result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return result.data;
|
|
665
|
+
}
|
|
666
|
+
async function readStackRaw(paths, name) {
|
|
667
|
+
const filePath = stackPath(paths, name);
|
|
668
|
+
if (!existsSync6(filePath)) {
|
|
669
|
+
throw new StackNotFoundError(name, await listStacks(paths));
|
|
670
|
+
}
|
|
671
|
+
return readFile5(filePath, "utf8");
|
|
672
|
+
}
|
|
673
|
+
async function getActiveStackName(paths) {
|
|
674
|
+
const state = await readState(paths.statePath);
|
|
675
|
+
return state?.active ?? null;
|
|
676
|
+
}
|
|
677
|
+
async function applyStack(paths, name, options = {}) {
|
|
678
|
+
const validate = options.validate ?? true;
|
|
679
|
+
const forceInvalid = options.forceInvalid ?? false;
|
|
680
|
+
const target = await readStack(paths, name);
|
|
681
|
+
if (validate && !forceInvalid) {
|
|
682
|
+
await validateStackOrThrow(name, target, options.validateOptions);
|
|
683
|
+
}
|
|
684
|
+
const pending = [];
|
|
685
|
+
for (const [agent, entry] of Object.entries(target.agents)) {
|
|
686
|
+
const { filePath, content, model } = await readAgentFileStrict(paths.agentsDir, agent);
|
|
687
|
+
pending.push({
|
|
688
|
+
agent,
|
|
689
|
+
filePath,
|
|
690
|
+
next: model === entry.model ? null : setFrontmatterModel(content, entry.model)
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const prevState = await readState(paths.statePath);
|
|
694
|
+
const prevActive = prevState?.active ?? null;
|
|
695
|
+
const displaced = { agents: modelsToStackAgents(await readAgentModels(paths.agentsDir)) };
|
|
696
|
+
const historyId = await appendHistory(
|
|
697
|
+
paths.historyDir,
|
|
698
|
+
prevActive ?? "(none)",
|
|
699
|
+
name,
|
|
700
|
+
`${JSON.stringify(displaced, null, 2)}
|
|
701
|
+
`
|
|
702
|
+
);
|
|
703
|
+
const changed = [];
|
|
704
|
+
for (const p of pending) {
|
|
705
|
+
if (p.next === null) continue;
|
|
706
|
+
await atomicWriteFile(p.filePath, p.next);
|
|
707
|
+
changed.push(p.agent);
|
|
708
|
+
}
|
|
709
|
+
await writeState(paths.statePath, {
|
|
710
|
+
version: 1,
|
|
711
|
+
active: name,
|
|
712
|
+
previousActive: prevActive,
|
|
713
|
+
lastSwitchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
714
|
+
});
|
|
715
|
+
await trimHistory(paths.historyDir).catch(() => {
|
|
716
|
+
});
|
|
717
|
+
return {
|
|
718
|
+
previous: prevActive,
|
|
719
|
+
current: name,
|
|
720
|
+
changed,
|
|
721
|
+
historyId,
|
|
722
|
+
restartRequired: true
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function modelsToStackAgents(models) {
|
|
726
|
+
const out = {};
|
|
727
|
+
for (const k of Object.keys(models).sort()) {
|
|
728
|
+
const model = models[k];
|
|
729
|
+
if (model !== void 0) out[k] = { model };
|
|
730
|
+
}
|
|
731
|
+
return out;
|
|
732
|
+
}
|
|
733
|
+
async function back(paths, n = 1, options = {}) {
|
|
734
|
+
if (n < 1) throw new UserError("`back -n` must be at least 1.");
|
|
735
|
+
const state = await readState(paths.statePath);
|
|
736
|
+
if (!state || !state.previousActive) {
|
|
737
|
+
throw new UserError("No previous stack to revert to.");
|
|
738
|
+
}
|
|
739
|
+
if (n === 1) return applyStack(paths, state.previousActive, options);
|
|
740
|
+
const entries = await listHistory(paths.historyDir);
|
|
741
|
+
if (entries.length < n) {
|
|
742
|
+
throw new UserError(
|
|
743
|
+
`Cannot go back ${n} steps; only ${entries.length} switch${entries.length === 1 ? "" : "es"} in history.`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const target = entries[n - 1]?.fromStack;
|
|
747
|
+
if (!target || target === "(none)") {
|
|
748
|
+
throw new UserError(`Cannot go back ${n} steps to a non-stack target ("${target}").`);
|
|
749
|
+
}
|
|
750
|
+
return applyStack(paths, target, options);
|
|
751
|
+
}
|
|
752
|
+
var STACK_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
753
|
+
function assertValidStackName(name) {
|
|
754
|
+
if (!STACK_NAME_RE.test(name)) {
|
|
755
|
+
throw new UserError(
|
|
756
|
+
`Stack name "${name}" contains invalid characters. Allowed: A-Z a-z 0-9 . _ -`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function captureStack(paths, name, options = {}) {
|
|
761
|
+
assertValidStackName(name);
|
|
762
|
+
const dest = stackPath(paths, name);
|
|
763
|
+
if (existsSync6(dest) && !options.force) {
|
|
764
|
+
throw new UserError(`Stack "${name}" already exists. Use --force to overwrite.`);
|
|
765
|
+
}
|
|
766
|
+
const models = await readAgentModels(paths.agentsDir);
|
|
767
|
+
const agents = modelsToStackAgents(models);
|
|
768
|
+
if (Object.keys(agents).length === 0) {
|
|
769
|
+
throw new UserError(
|
|
770
|
+
`No agent .md files with a frontmatter \`model:\` line found in ${paths.agentsDir}.`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
await mkdir4(paths.stacksDir, { recursive: true });
|
|
774
|
+
await atomicWriteJson(dest, { agents });
|
|
775
|
+
return { name, path: dest, agents: Object.keys(agents).length };
|
|
776
|
+
}
|
|
777
|
+
async function removeStack(paths, name, options = {}) {
|
|
778
|
+
const filePath = stackPath(paths, name);
|
|
779
|
+
if (!existsSync6(filePath)) {
|
|
780
|
+
throw new StackNotFoundError(name, await listStacks(paths));
|
|
781
|
+
}
|
|
782
|
+
const active = await getActiveStackName(paths);
|
|
783
|
+
if (active === name && !options.force) {
|
|
784
|
+
throw new UserError(`"${name}" is the active stack. Use --force to remove anyway.`);
|
|
785
|
+
}
|
|
786
|
+
await unlink3(filePath);
|
|
787
|
+
}
|
|
788
|
+
async function importStack(paths, name, fromFile, options = {}) {
|
|
789
|
+
assertValidStackName(name);
|
|
790
|
+
const dest = stackPath(paths, name);
|
|
791
|
+
if (existsSync6(dest) && !options.force) {
|
|
792
|
+
throw new UserError(`Stack "${name}" already exists. Use --force to overwrite.`);
|
|
793
|
+
}
|
|
794
|
+
if (!existsSync6(fromFile)) {
|
|
795
|
+
throw new UserError(`Import file does not exist: ${fromFile}`);
|
|
796
|
+
}
|
|
797
|
+
await mkdir4(paths.stacksDir, { recursive: true });
|
|
798
|
+
await copyFile2(fromFile, dest);
|
|
799
|
+
}
|
|
800
|
+
async function exportStack(paths, name, toFile) {
|
|
801
|
+
const filePath = stackPath(paths, name);
|
|
802
|
+
if (!existsSync6(filePath)) {
|
|
803
|
+
throw new StackNotFoundError(name, await listStacks(paths));
|
|
804
|
+
}
|
|
805
|
+
await mkdir4(path7.dirname(toFile), { recursive: true });
|
|
806
|
+
await copyFile2(filePath, toFile);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/version.ts
|
|
810
|
+
var VERSION = "1.0.0";
|
|
811
|
+
|
|
812
|
+
// src/cli.ts
|
|
813
|
+
var log = (...args) => console.log(...args);
|
|
814
|
+
var err = (...args) => console.error(...args);
|
|
815
|
+
function formatStackListLine(name, isActive) {
|
|
816
|
+
return `${isActive ? "*" : " "} ${name}`;
|
|
817
|
+
}
|
|
818
|
+
var INIT_CAPTURE_NAME = "default";
|
|
819
|
+
async function cmdInit(paths, opts) {
|
|
820
|
+
await mkdir5(paths.stacksDir, { recursive: true });
|
|
821
|
+
await mkdir5(paths.historyDir, { recursive: true });
|
|
822
|
+
const { readState: readState2, writeState: writeState2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
823
|
+
const existing = await readState2(paths.statePath);
|
|
824
|
+
const stacks = await listStacks(paths);
|
|
825
|
+
if ((!existing || opts.force) && stacks.length === 0) {
|
|
826
|
+
try {
|
|
827
|
+
const captured = await captureStack(paths, INIT_CAPTURE_NAME, { force: !!opts.force });
|
|
828
|
+
await writeState2(paths.statePath, {
|
|
829
|
+
version: 1,
|
|
830
|
+
active: INIT_CAPTURE_NAME,
|
|
831
|
+
previousActive: null,
|
|
832
|
+
lastSwitchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
833
|
+
});
|
|
834
|
+
log(
|
|
835
|
+
`agent-router: captured ${captured.agents} agent${captured.agents === 1 ? "" : "s"} from ${paths.agentsDir} \u2192 stacks/${INIT_CAPTURE_NAME}.json (active)`
|
|
836
|
+
);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
if (e instanceof UserError) {
|
|
839
|
+
log(`agent-router: no agents captured (${e.message})`);
|
|
840
|
+
log(
|
|
841
|
+
" create agent .md files with a frontmatter `model:` line, then run `agent-router capture <name>`."
|
|
842
|
+
);
|
|
843
|
+
} else {
|
|
844
|
+
throw e;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} else if (existing) {
|
|
848
|
+
log(
|
|
849
|
+
`agent-router: state already initialized (active="${existing.active}"); use --force to reset`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (opts.noEditOpencodeJson) {
|
|
853
|
+
log("agent-router: --no-edit-opencode-json passed; not editing opencode.json or tui.json");
|
|
854
|
+
log(` add this to opencode.json plugin[]: "${PLUGIN_REGISTRY_ENTRY}"`);
|
|
855
|
+
log(` add this to tui.json plugin[] (sidebar): "${PLUGIN_REGISTRY_ENTRY}"`);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
await editOpencodeJson(paths);
|
|
859
|
+
await editTuiJson(paths);
|
|
860
|
+
}
|
|
861
|
+
async function editTuiJson(paths) {
|
|
862
|
+
const exists = existsSync7(paths.tuiJsonPath);
|
|
863
|
+
if (exists) {
|
|
864
|
+
await backupFiles(paths.opencodeBackupsDir, [paths.tuiJsonPath]);
|
|
865
|
+
}
|
|
866
|
+
const result = await ensureTuiJsonPluginEntry(paths.tuiJsonPath);
|
|
867
|
+
if (result.added) {
|
|
868
|
+
log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to tui.json plugin[] (sidebar support)`);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
log("agent-router: tui.json already up to date");
|
|
872
|
+
}
|
|
873
|
+
async function editOpencodeJson(paths) {
|
|
874
|
+
const cfg = await readOpencodeJson(paths.opencodeJsonPath);
|
|
875
|
+
if (!cfg) {
|
|
876
|
+
log(`agent-router: ${paths.opencodeJsonPath} missing \u2014 skipping plugin edits.`);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const removed = removePluginEntry(cfg);
|
|
880
|
+
const ensured = ensurePluginEntry(removed.config);
|
|
881
|
+
if (!ensured.result.added && removed.result.removed.length === 0) {
|
|
882
|
+
log("agent-router: opencode.json already up to date");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
await backupFiles(paths.opencodeBackupsDir, [paths.opencodeJsonPath]);
|
|
886
|
+
await writeOpencodeJson(paths.opencodeJsonPath, ensured.config);
|
|
887
|
+
if (removed.result.removed.length) {
|
|
888
|
+
log(
|
|
889
|
+
`agent-router: removed legacy plugin entr${removed.result.removed.length === 1 ? "y" : "ies"}: ${removed.result.removed.join(", ")}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (ensured.result.added) {
|
|
893
|
+
log(`agent-router: added "${PLUGIN_REGISTRY_ENTRY}" to opencode.json plugin[]`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async function cmdList(paths) {
|
|
897
|
+
const stacks = await listStacks(paths);
|
|
898
|
+
if (stacks.length === 0) {
|
|
899
|
+
log("(no stacks; run `agent-router init` or `agent-router capture <name>`)");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const active = await getActiveStackName(paths);
|
|
903
|
+
for (const s of stacks) log(formatStackListLine(s, s === active));
|
|
904
|
+
}
|
|
905
|
+
async function cmdStatus(paths) {
|
|
906
|
+
const active = await getActiveStackName(paths);
|
|
907
|
+
log(active ?? "(none)");
|
|
908
|
+
}
|
|
909
|
+
async function cmdShow(paths, name) {
|
|
910
|
+
const raw = await readStackRaw(paths, name);
|
|
911
|
+
try {
|
|
912
|
+
log(JSON.stringify(JSON.parse(raw), null, 2));
|
|
913
|
+
} catch {
|
|
914
|
+
process.stdout.write(raw);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function cmdCurrent(paths) {
|
|
918
|
+
const models = await readAgentModels(paths.agentsDir);
|
|
919
|
+
const names = Object.keys(models).sort();
|
|
920
|
+
if (names.length === 0) {
|
|
921
|
+
log(`(no agent .md files with a \`model:\` line in ${paths.agentsDir})`);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const width = Math.max(...names.map((n) => n.length));
|
|
925
|
+
for (const n of names) log(`${n.padEnd(width)} ${models[n]}`);
|
|
926
|
+
}
|
|
927
|
+
async function cmdUse(paths, name, opts) {
|
|
928
|
+
const r = await applyStack(paths, name, {
|
|
929
|
+
validate: !opts.noValidate,
|
|
930
|
+
forceInvalid: opts.forceInvalid ?? false
|
|
931
|
+
});
|
|
932
|
+
log(
|
|
933
|
+
`Switched: ${r.previous ?? "(none)"} \u2192 ${r.current} (${r.changed.length} agent${r.changed.length === 1 ? "" : "s"} updated). Restart opencode for change to take effect.`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
async function cmdBack(paths, n, opts) {
|
|
937
|
+
const r = await back(paths, n, {
|
|
938
|
+
validate: !opts.noValidate,
|
|
939
|
+
forceInvalid: opts.forceInvalid ?? false
|
|
940
|
+
});
|
|
941
|
+
log(
|
|
942
|
+
`Switched: ${r.previous ?? "(none)"} \u2192 ${r.current} (${r.changed.length} agent${r.changed.length === 1 ? "" : "s"} updated). Restart opencode for change to take effect.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
async function cmdCapture(paths, name, force) {
|
|
946
|
+
const r = await captureStack(paths, name, { force });
|
|
947
|
+
log(`Captured ${r.agents} agent${r.agents === 1 ? "" : "s"} \u2192 ${r.path}`);
|
|
948
|
+
}
|
|
949
|
+
async function cmdHistory(paths, limit) {
|
|
950
|
+
const entries = await listHistory(paths.historyDir);
|
|
951
|
+
if (entries.length === 0) {
|
|
952
|
+
log("(no history)");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
for (const e of entries.slice(0, limit)) {
|
|
956
|
+
log(`${e.id} ${e.fromStack} \u2192 ${e.toStack}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async function cmdValidate(paths, name, opts) {
|
|
960
|
+
const targets = [];
|
|
961
|
+
if (opts.active) {
|
|
962
|
+
targets.push({
|
|
963
|
+
name: "(current frontmatter)",
|
|
964
|
+
load: async () => {
|
|
965
|
+
const models = await readAgentModels(paths.agentsDir);
|
|
966
|
+
const agents = Object.fromEntries(
|
|
967
|
+
Object.entries(models).map(([k, model]) => [k, { model }])
|
|
968
|
+
);
|
|
969
|
+
return { agents };
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
} else if (opts.all) {
|
|
973
|
+
for (const n of await listStacks(paths)) {
|
|
974
|
+
targets.push({ name: n, load: () => readStack(paths, n) });
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
if (!name) throw new UserError("Specify a stack name, or pass --all or --active.");
|
|
978
|
+
targets.push({ name, load: () => readStack(paths, name) });
|
|
979
|
+
}
|
|
980
|
+
let anyMissing = false;
|
|
981
|
+
for (const t of targets) {
|
|
982
|
+
const parsed = StackFileSchema.parse(await t.load());
|
|
983
|
+
const r = await validateStack(parsed);
|
|
984
|
+
if (r.ok) {
|
|
985
|
+
log(`${t.name}: OK (${r.checked} model id${r.checked === 1 ? "" : "s"} checked)`);
|
|
986
|
+
} else {
|
|
987
|
+
anyMissing = true;
|
|
988
|
+
err(`${t.name}: MISSING ${r.missing.length} model id${r.missing.length === 1 ? "" : "s"}:`);
|
|
989
|
+
for (const m of r.missing) err(` ${m.path.padEnd(48)} ${m.modelId}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (anyMissing) {
|
|
993
|
+
err("");
|
|
994
|
+
err(
|
|
995
|
+
"Run `opencode auth list` to confirm provider auth, or `opencode models` to see the full reachable list."
|
|
996
|
+
);
|
|
997
|
+
throw new ModelValidationError("(validation gate)", []);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function cmdRm(paths, name, force) {
|
|
1001
|
+
await removeStack(paths, name, { force });
|
|
1002
|
+
log(`Removed stack "${name}"`);
|
|
1003
|
+
}
|
|
1004
|
+
async function cmdEdit(paths, name) {
|
|
1005
|
+
const filePath = stackPath(paths, name);
|
|
1006
|
+
if (!existsSync7(filePath)) {
|
|
1007
|
+
throw new StackNotFoundError(name, await listStacks(paths));
|
|
1008
|
+
}
|
|
1009
|
+
const editor = process.env.EDITOR && process.env.EDITOR.length > 0 ? process.env.EDITOR : "vi";
|
|
1010
|
+
const result = spawnSync(editor, [filePath], { stdio: "inherit" });
|
|
1011
|
+
if (result.status !== 0) {
|
|
1012
|
+
throw new UserError(`Editor exited with status ${result.status}.`);
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
await readStack(paths, name);
|
|
1016
|
+
} catch (e) {
|
|
1017
|
+
err(`Warning: ${e.message}`);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function cmdImport(paths, name, file, force) {
|
|
1021
|
+
await importStack(paths, name, path8.resolve(file), { force });
|
|
1022
|
+
log(`Imported "${file}" \u2192 stacks/${name}.json`);
|
|
1023
|
+
}
|
|
1024
|
+
async function cmdExport(paths, name, file) {
|
|
1025
|
+
await exportStack(paths, name, path8.resolve(file));
|
|
1026
|
+
log(`Exported stacks/${name}.json \u2192 "${file}"`);
|
|
1027
|
+
}
|
|
1028
|
+
function cmdPath(paths) {
|
|
1029
|
+
for (const [k, v] of Object.entries(paths)) log(`${k.padEnd(22)} ${v}`);
|
|
1030
|
+
}
|
|
1031
|
+
async function main(argv) {
|
|
1032
|
+
const cli = cac("agent-router");
|
|
1033
|
+
const paths = await resolvePathsWithConfig();
|
|
1034
|
+
cli.command("init", "create state dirs, capture current models, register the plugin").option("--force", "overwrite existing state").option("--no-edit-opencode-json", "do not modify opencode.json or tui.json").action(async (opts) => {
|
|
1035
|
+
const passthrough = {};
|
|
1036
|
+
if (opts.force !== void 0) passthrough.force = opts.force;
|
|
1037
|
+
if (opts.editOpencodeJson === false) passthrough.noEditOpencodeJson = true;
|
|
1038
|
+
await cmdInit(paths, passthrough);
|
|
1039
|
+
});
|
|
1040
|
+
cli.command("list", "list available stacks (* marks active)").action(() => cmdList(paths));
|
|
1041
|
+
cli.command("status", "print active stack name").action(() => cmdStatus(paths));
|
|
1042
|
+
cli.command("current", "print the agent \u2192 model mapping currently in frontmatter").action(() => cmdCurrent(paths));
|
|
1043
|
+
cli.command("show <name>", "print a stack file as pretty JSON").action((n) => cmdShow(paths, n));
|
|
1044
|
+
cli.command("use <name>", "apply a stack to the agent files (validates first)").alias("apply").option("--no-validate", "skip the pre-apply model validation").option("--force-invalid", "apply even if validation finds missing models").action(async (n, opts) => {
|
|
1045
|
+
const passthrough = {};
|
|
1046
|
+
if (opts.validate === false) passthrough.noValidate = true;
|
|
1047
|
+
if (opts.forceInvalid !== void 0) passthrough.forceInvalid = opts.forceInvalid;
|
|
1048
|
+
await cmdUse(paths, n, passthrough);
|
|
1049
|
+
});
|
|
1050
|
+
cli.command("capture <name>", "snapshot current frontmatter models into a new stack").option("--force", "overwrite an existing stack").action((n, opts) => cmdCapture(paths, n, !!opts.force));
|
|
1051
|
+
cli.command("back", "undo the last N switches").option("-n <n>", "how many switches to undo", { default: 1 }).option("--no-validate", "skip the pre-apply model validation").option("--force-invalid", "apply even if validation finds missing models").action(async (opts) => {
|
|
1052
|
+
const passthrough = {};
|
|
1053
|
+
if (opts.validate === false) passthrough.noValidate = true;
|
|
1054
|
+
if (opts.forceInvalid !== void 0) passthrough.forceInvalid = opts.forceInvalid;
|
|
1055
|
+
await cmdBack(paths, opts.n ?? 1, passthrough);
|
|
1056
|
+
});
|
|
1057
|
+
cli.command("history", "list recent switches (newest first)").option("--limit <n>", "max entries to show", { default: 20 }).action((opts) => cmdHistory(paths, opts.limit ?? 20));
|
|
1058
|
+
cli.command(
|
|
1059
|
+
"validate [name]",
|
|
1060
|
+
"verify model IDs in a stack are reachable via current opencode auth"
|
|
1061
|
+
).option("--all", "validate every stack").option("--active", "validate the models currently in agent frontmatter").action(
|
|
1062
|
+
(name, opts) => cmdValidate(paths, name, opts)
|
|
1063
|
+
);
|
|
1064
|
+
cli.command("rm <name>", "delete a stack").option("--force", "delete even if active").action((n, opts) => cmdRm(paths, n, !!opts.force));
|
|
1065
|
+
cli.command("edit <name>", "open a stack in $EDITOR (fallback `vi`)").action((n) => cmdEdit(paths, n));
|
|
1066
|
+
cli.command("import <name> <file>", "copy <file> into stacks/<name>.json").option("--force", "overwrite an existing stack").action(
|
|
1067
|
+
(n, f, opts) => cmdImport(paths, n, f, !!opts.force)
|
|
1068
|
+
);
|
|
1069
|
+
cli.command("export <name> <file>", "copy stacks/<name>.json to <file>").action((n, f) => cmdExport(paths, n, f));
|
|
1070
|
+
cli.command("path", "print all paths used by agent-router").action(() => cmdPath(paths));
|
|
1071
|
+
cli.command("completion", "print instructions for installing shell autocomplete").action(() => {
|
|
1072
|
+
log("To install autocomplete for agent-router, run one of the following:\n");
|
|
1073
|
+
log(
|
|
1074
|
+
"ZSH:\n agent-router completion-script zsh > ~/.agent-router-completion.zsh\n echo 'source ~/.agent-router-completion.zsh' >> ~/.zshrc\n"
|
|
1075
|
+
);
|
|
1076
|
+
log(
|
|
1077
|
+
"BASH:\n agent-router completion-script bash > ~/.agent-router-completion.bash\n echo 'source ~/.agent-router-completion.bash' >> ~/.bashrc\n"
|
|
1078
|
+
);
|
|
1079
|
+
log(
|
|
1080
|
+
"FISH:\n agent-router completion-script fish > ~/.config/fish/completions/agent-router.fish\n"
|
|
1081
|
+
);
|
|
1082
|
+
});
|
|
1083
|
+
cli.command("completion-script <shell>", "Generate the completion script for your shell").action((shell) => {
|
|
1084
|
+
if (shell === "zsh") {
|
|
1085
|
+
log(
|
|
1086
|
+
`
|
|
1087
|
+
#compdef agent-router
|
|
1088
|
+
|
|
1089
|
+
_agent_router() {
|
|
1090
|
+
local -a commands
|
|
1091
|
+
commands=(
|
|
1092
|
+
'init:create state dirs, capture current models, register the plugin'
|
|
1093
|
+
'list:list available stacks'
|
|
1094
|
+
'status:print active stack name'
|
|
1095
|
+
'current:print the current agent \u2192 model mapping'
|
|
1096
|
+
'show:print a stack file as pretty JSON'
|
|
1097
|
+
'use:apply a stack to the agent files'
|
|
1098
|
+
'capture:snapshot current models into a new stack'
|
|
1099
|
+
'back:undo the last N switches'
|
|
1100
|
+
'history:list recent switches'
|
|
1101
|
+
'validate:verify model IDs'
|
|
1102
|
+
'rm:delete a stack'
|
|
1103
|
+
'edit:open a stack in $EDITOR'
|
|
1104
|
+
'import:copy file into stacks'
|
|
1105
|
+
'export:copy stack to file'
|
|
1106
|
+
'path:print all paths'
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
if (( CURRENT == 2 )); then
|
|
1110
|
+
_describe -t commands 'commands' commands
|
|
1111
|
+
else
|
|
1112
|
+
local cmd=\${words[2]}
|
|
1113
|
+
case $cmd in
|
|
1114
|
+
show|use|validate|rm|edit|export)
|
|
1115
|
+
local -a stacks
|
|
1116
|
+
stacks=($(agent-router completion-resolve))
|
|
1117
|
+
_describe -t stacks 'stacks' stacks
|
|
1118
|
+
;;
|
|
1119
|
+
esac
|
|
1120
|
+
fi
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
compdef _agent_router agent-router
|
|
1124
|
+
`.trim()
|
|
1125
|
+
);
|
|
1126
|
+
} else if (shell === "bash") {
|
|
1127
|
+
log(
|
|
1128
|
+
`
|
|
1129
|
+
_agent_router() {
|
|
1130
|
+
local cur prev commands stacks
|
|
1131
|
+
COMPREPLY=()
|
|
1132
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1133
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1134
|
+
commands="init list status current show use capture back history validate rm edit import export path completion"
|
|
1135
|
+
|
|
1136
|
+
if [[ \${COMP_CWORD} == 1 ]]; then
|
|
1137
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
|
|
1138
|
+
return 0
|
|
1139
|
+
fi
|
|
1140
|
+
|
|
1141
|
+
case "\${prev}" in
|
|
1142
|
+
show|use|validate|rm|edit|export)
|
|
1143
|
+
stacks=$(agent-router completion-resolve 2>/dev/null)
|
|
1144
|
+
COMPREPLY=( $(compgen -W "\${stacks}" -- \${cur}) )
|
|
1145
|
+
;;
|
|
1146
|
+
esac
|
|
1147
|
+
return 0
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
complete -F _agent_router agent-router
|
|
1151
|
+
`.trim()
|
|
1152
|
+
);
|
|
1153
|
+
} else if (shell === "fish") {
|
|
1154
|
+
log(
|
|
1155
|
+
`
|
|
1156
|
+
function _agent_router_needs_command
|
|
1157
|
+
set cmd (commandline -opc)
|
|
1158
|
+
if test (count $cmd) -eq 1
|
|
1159
|
+
return 0
|
|
1160
|
+
end
|
|
1161
|
+
return 1
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
function _agent_router_using_command
|
|
1165
|
+
set cmd (commandline -opc)
|
|
1166
|
+
if test (count $cmd) -gt 1
|
|
1167
|
+
if test $cmd[2] = $argv[1]
|
|
1168
|
+
return 0
|
|
1169
|
+
end
|
|
1170
|
+
end
|
|
1171
|
+
return 1
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a init -d 'create state dirs, capture current models, register the plugin'
|
|
1175
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a list -d 'list available stacks'
|
|
1176
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a status -d 'print active stack name'
|
|
1177
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a current -d 'print the current agent \u2192 model mapping'
|
|
1178
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a show -d 'print a stack file as pretty JSON'
|
|
1179
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a use -d 'apply a stack to the agent files'
|
|
1180
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a capture -d 'snapshot current models into a new stack'
|
|
1181
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a back -d 'undo the last N switches'
|
|
1182
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a history -d 'list recent switches'
|
|
1183
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a validate -d 'verify model IDs'
|
|
1184
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a rm -d 'delete a stack'
|
|
1185
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a edit -d 'open a stack in $EDITOR'
|
|
1186
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a import -d 'copy file into stacks'
|
|
1187
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a export -d 'copy stack to file'
|
|
1188
|
+
complete -f -c agent-router -n '_agent_router_needs_command' -a path -d 'print all paths'
|
|
1189
|
+
|
|
1190
|
+
complete -f -c agent-router -n '_agent_router_using_command show' -a '(agent-router completion-resolve)'
|
|
1191
|
+
complete -f -c agent-router -n '_agent_router_using_command use' -a '(agent-router completion-resolve)'
|
|
1192
|
+
complete -f -c agent-router -n '_agent_router_using_command validate' -a '(agent-router completion-resolve)'
|
|
1193
|
+
complete -f -c agent-router -n '_agent_router_using_command rm' -a '(agent-router completion-resolve)'
|
|
1194
|
+
complete -f -c agent-router -n '_agent_router_using_command edit' -a '(agent-router completion-resolve)'
|
|
1195
|
+
complete -f -c agent-router -n '_agent_router_using_command export' -a '(agent-router completion-resolve)'
|
|
1196
|
+
`.trim()
|
|
1197
|
+
);
|
|
1198
|
+
} else {
|
|
1199
|
+
err("Unsupported shell. Supported: bash, zsh, fish");
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
cli.command("completion-resolve", "Internal command used by shell autocomplete").action(async () => {
|
|
1204
|
+
try {
|
|
1205
|
+
const stacks = await listStacks(paths);
|
|
1206
|
+
log(stacks.join("\n"));
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
cli.help();
|
|
1211
|
+
cli.version(VERSION);
|
|
1212
|
+
try {
|
|
1213
|
+
cli.parse([process.argv[0] ?? "node", process.argv[1] ?? "agent-router", ...argv], {
|
|
1214
|
+
run: false
|
|
1215
|
+
});
|
|
1216
|
+
if (cli.matchedCommand) {
|
|
1217
|
+
await cli.runMatchedCommand();
|
|
1218
|
+
} else if (argv.length > 0 && !argv.includes("--help") && !argv.includes("-h") && !argv.includes("--version") && !argv.includes("-v")) {
|
|
1219
|
+
cli.outputHelp();
|
|
1220
|
+
return 1;
|
|
1221
|
+
}
|
|
1222
|
+
return 0;
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
if (e instanceof RouterError) {
|
|
1225
|
+
err(`error: ${e.message}`);
|
|
1226
|
+
if (e instanceof ModelValidationError && e.missing.length > 0) {
|
|
1227
|
+
err("");
|
|
1228
|
+
for (const m of e.missing) err(` ${m.path.padEnd(48)} ${m.modelId}`);
|
|
1229
|
+
}
|
|
1230
|
+
const ctor = e.constructor;
|
|
1231
|
+
return ctor.exitCode ?? 1;
|
|
1232
|
+
}
|
|
1233
|
+
err(`unexpected error: ${e.message}`);
|
|
1234
|
+
return 1;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function isInvokedAsScript() {
|
|
1238
|
+
if (!import.meta.url.startsWith("file:")) return false;
|
|
1239
|
+
const argv1 = process.argv[1];
|
|
1240
|
+
if (!argv1) return false;
|
|
1241
|
+
try {
|
|
1242
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path8.resolve(argv1));
|
|
1243
|
+
} catch {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (isInvokedAsScript()) {
|
|
1248
|
+
main(process.argv.slice(2)).then(
|
|
1249
|
+
(code) => process.exit(code),
|
|
1250
|
+
(e) => {
|
|
1251
|
+
err(e);
|
|
1252
|
+
process.exit(1);
|
|
1253
|
+
}
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
export {
|
|
1257
|
+
main
|
|
1258
|
+
};
|
|
1259
|
+
//# sourceMappingURL=cli.js.map
|