offrouter-cli 0.2.1 → 0.3.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.
@@ -1,59 +0,0 @@
1
- import type { Command } from "commander";
2
- import { detectEnvironment, V1_HONESTY_LINE } from "./detection.js";
3
- import { writeJson, writeText, type IoStreams } from "./output.js";
4
-
5
- export interface CommandContext {
6
- env: NodeJS.ProcessEnv;
7
- cwd: string;
8
- stdout: IoStreams;
9
- stderr: IoStreams;
10
- /** Read full stdin (injected string in tests, process.stdin otherwise). */
11
- readStdin: () => Promise<string>;
12
- }
13
-
14
- export function registerInitCommand(
15
- program: Command,
16
- ctx: () => CommandContext,
17
- ): void {
18
- program
19
- .command("init")
20
- .description("Detect environment and scaffold OffRouter config")
21
- .option("--dry-run", "Report detection without writing config", false)
22
- .option("--json", "Emit JSON", false)
23
- .action(async (opts: { dryRun?: boolean; json?: boolean }) => {
24
- const { env, cwd, stdout } = ctx();
25
- const detection = await detectEnvironment({ env, cwd });
26
- const dryRun = Boolean(opts.dryRun);
27
- const wroteConfig = false;
28
-
29
- if (opts.json) {
30
- writeJson(stdout, {
31
- dryRun,
32
- wroteConfig,
33
- honesty: V1_HONESTY_LINE,
34
- detection: {
35
- harnessProfiles: detection.harnessProfiles,
36
- authTiers: detection.authTiers,
37
- subscription: detection.subscription,
38
- apiKeyReadiness: detection.apiKeyReadiness,
39
- localRuntime: detection.localRuntime,
40
- limits: detection.limits,
41
- lastProviderError: detection.lastProviderError,
42
- },
43
- });
44
- return;
45
- }
46
-
47
- writeText(stdout, V1_HONESTY_LINE);
48
- writeText(
49
- stdout,
50
- dryRun
51
- ? "Dry run: no config files written."
52
- : "Init writing is reserved for a later milestone; use --dry-run for detection.",
53
- );
54
- writeText(
55
- stdout,
56
- `Subscription: ${detection.subscription.status}; API-key ready: ${detection.apiKeyReadiness.anyReady}; local: ${detection.localRuntime.health}`,
57
- );
58
- });
59
- }
@@ -1,438 +0,0 @@
1
- /**
2
- * CLI install / uninstall for harness plugins (Claude / Codex V1).
3
- */
4
- import type { Command } from "commander";
5
- import { loadConfig } from "offrouter-core";
6
- import {
7
- formatInstallDiffText as formatClaudeDiff,
8
- installClaudeProfile,
9
- listAllowlistedPersonalClaudeProfiles,
10
- rollbackClaudeProfile,
11
- type ClaudeInstallResult,
12
- type ClaudeRollbackResult,
13
- } from "offrouter-adapter-claude";
14
- import {
15
- formatInstallDiffText as formatCodexDiff,
16
- installCodexProfile,
17
- listAllowlistedPersonalCodexProfiles,
18
- rollbackCodexProfile,
19
- type CodexInstallResult,
20
- type CodexRollbackResult,
21
- } from "offrouter-adapter-codex";
22
- import {
23
- formatInstallDiffText as formatGeminiDiff,
24
- installGeminiProfile,
25
- listAllowlistedPersonalGeminiProfiles,
26
- rollbackGeminiProfile,
27
- type GeminiInstallResult,
28
- type GeminiRollbackResult,
29
- } from "offrouter-adapter-gemini";
30
- import {
31
- formatInstallDiffText as formatGrokDiff,
32
- installGrokProfile,
33
- listAllowlistedPersonalGrokProfiles,
34
- rollbackGrokProfile,
35
- type GrokInstallResult,
36
- type GrokRollbackResult,
37
- } from "offrouter-adapter-grok";
38
- import type { CommandContext } from "./init.js";
39
- import { writeJson, writeText } from "./output.js";
40
- import { honestyForHarness } from "./detection.js";
41
- import { formatConfigError } from "./config-error.js";
42
-
43
- type HarnessKind = "claude" | "codex" | "gemini" | "grok";
44
-
45
- type InstallResultUnion =
46
- | ClaudeInstallResult
47
- | CodexInstallResult
48
- | GeminiInstallResult
49
- | GrokInstallResult;
50
- type RollbackResultUnion =
51
- | ClaudeRollbackResult
52
- | CodexRollbackResult
53
- | GeminiRollbackResult
54
- | GrokRollbackResult;
55
-
56
- function assertHarness(harness: string): HarnessKind {
57
- if (
58
- harness !== "claude" &&
59
- harness !== "codex" &&
60
- harness !== "gemini" &&
61
- harness !== "grok"
62
- ) {
63
- throw new Error(
64
- `Unsupported harness "${harness}". V1 install currently supports: claude, codex, gemini, grok`,
65
- );
66
- }
67
- return harness;
68
- }
69
-
70
- function harnessLabel(kind: HarnessKind): string {
71
- if (kind === "codex") return "Codex";
72
- if (kind === "gemini") return "Gemini";
73
- if (kind === "grok") return "Grok";
74
- return "Claude";
75
- }
76
-
77
- function allowlistedPersonalProfiles(
78
- kind: HarnessKind,
79
- config: Parameters<typeof listAllowlistedPersonalClaudeProfiles>[0],
80
- ): string[] {
81
- if (kind === "codex") return listAllowlistedPersonalCodexProfiles(config);
82
- if (kind === "gemini") return listAllowlistedPersonalGeminiProfiles(config);
83
- if (kind === "grok") return listAllowlistedPersonalGrokProfiles(config);
84
- return listAllowlistedPersonalClaudeProfiles(config);
85
- }
86
-
87
- async function installOne(
88
- kind: HarnessKind,
89
- args: {
90
- profile: string;
91
- config: Parameters<typeof installClaudeProfile>[0]["config"];
92
- env: NodeJS.ProcessEnv;
93
- dryRun: boolean;
94
- },
95
- ): Promise<InstallResultUnion> {
96
- if (kind === "codex") {
97
- return installCodexProfile(args);
98
- }
99
- if (kind === "gemini") {
100
- return installGeminiProfile(args);
101
- }
102
- if (kind === "grok") {
103
- return installGrokProfile(args);
104
- }
105
- return installClaudeProfile(args);
106
- }
107
-
108
- async function rollbackOne(
109
- kind: HarnessKind,
110
- args: {
111
- profile: string;
112
- env: NodeJS.ProcessEnv;
113
- dryRun: boolean;
114
- },
115
- ): Promise<RollbackResultUnion> {
116
- if (kind === "codex") {
117
- return rollbackCodexProfile(args);
118
- }
119
- if (kind === "gemini") {
120
- return rollbackGeminiProfile(args);
121
- }
122
- if (kind === "grok") {
123
- return rollbackGrokProfile(args);
124
- }
125
- return rollbackClaudeProfile(args);
126
- }
127
-
128
- function formatDiff(
129
- kind: HarnessKind,
130
- results: InstallResultUnion[],
131
- headline?: string,
132
- ): string {
133
- if (kind === "codex") {
134
- return formatCodexDiff(
135
- results as unknown as CodexInstallResult[],
136
- headline,
137
- );
138
- }
139
- if (kind === "gemini") {
140
- return formatGeminiDiff(
141
- results as unknown as GeminiInstallResult[],
142
- headline,
143
- );
144
- }
145
- if (kind === "grok") {
146
- return formatGrokDiff(results as unknown as GrokInstallResult[], headline);
147
- }
148
- return formatClaudeDiff(
149
- results as unknown as ClaudeInstallResult[],
150
- headline,
151
- );
152
- }
153
-
154
- function resultExitCode(results: Array<{ ok: boolean }>): number {
155
- return results.every((r) => r.ok) ? 0 : 1;
156
- }
157
-
158
- export function registerInstallCommand(
159
- program: Command,
160
- ctx: () => CommandContext,
161
- ): void {
162
- const install = program
163
- .command("install")
164
- .description(
165
- "Install OffRouter into an allowlisted personal harness profile",
166
- )
167
- .option(
168
- "--all-personal",
169
- "Install into every allowlisted personal harness profile (Claude or Codex)",
170
- false,
171
- )
172
- .option("--dry-run", "Show planned file changes without writing", false)
173
- .option("--json", "Emit JSON", false)
174
- .argument("[harness]", "Harness kind (V1: claude, codex)", "claude")
175
- .option(
176
- "--profile <profile>",
177
- "Target profile id (e.g. claude-personal, codex-personal). Required unless --all-personal.",
178
- )
179
- .action(
180
- async (
181
- harness: string,
182
- opts: {
183
- profile?: string;
184
- allPersonal?: boolean;
185
- dryRun?: boolean;
186
- json?: boolean;
187
- },
188
- ) => {
189
- const { env, cwd, stdout, stderr } = ctx();
190
- let kind: HarnessKind = "claude";
191
- try {
192
- kind = assertHarness(harness);
193
- const config = await loadConfig({ env, workspaceDir: cwd });
194
- const dryRun = Boolean(opts.dryRun);
195
- const allPersonal = Boolean(opts.allPersonal);
196
- const profile = opts.profile?.trim();
197
-
198
- if (allPersonal && profile) {
199
- throw new Error(
200
- "Use either --profile <id> or --all-personal, not both.",
201
- );
202
- }
203
-
204
- if (!allPersonal && !profile) {
205
- throw new Error(
206
- "Provide --profile <id> or --all-personal. Example: offrouter install claude --profile claude-personal --dry-run",
207
- );
208
- }
209
-
210
- const targets = allPersonal
211
- ? allowlistedPersonalProfiles(kind, config)
212
- : [profile!];
213
-
214
- const honesty = honestyForHarness(kind);
215
- const label = harnessLabel(kind);
216
-
217
- if (targets.length === 0) {
218
- const empty: InstallResultUnion[] = [];
219
- if (opts.json) {
220
- writeJson(stdout, {
221
- honesty,
222
- dryRun,
223
- harness: kind,
224
- results: empty,
225
- message: `No allowlisted personal ${label} profiles found for install.`,
226
- });
227
- return;
228
- }
229
- writeText(stdout, honesty);
230
- writeText(
231
- stdout,
232
- `No allowlisted personal ${label} profiles found for install.`,
233
- );
234
- return;
235
- }
236
-
237
- const results: InstallResultUnion[] = [];
238
- for (const target of targets) {
239
- results.push(
240
- await installOne(kind, { profile: target, config, env, dryRun }),
241
- );
242
- }
243
-
244
- if (opts.json) {
245
- writeJson(stdout, {
246
- honesty,
247
- dryRun,
248
- harness: kind,
249
- results,
250
- });
251
- } else {
252
- writeText(stdout, honesty);
253
- const headline = allPersonal
254
- ? `Install matrix (allowlisted personal ${label} profiles):`
255
- : undefined;
256
- writeText(stdout, formatDiff(kind, results, headline).trimEnd());
257
- }
258
-
259
- if (resultExitCode(results) !== 0) {
260
- const failed = results.filter((r) => !r.ok);
261
- const msg = failed.map((r) => r.message).join("; ");
262
- // Commander exitOverride path
263
- const err = new Error(msg) as Error & { exitCode?: number };
264
- err.exitCode = 1;
265
- throw err;
266
- }
267
- } catch (err) {
268
- if (
269
- err &&
270
- typeof err === "object" &&
271
- "exitCode" in err &&
272
- typeof (err as { exitCode: unknown }).exitCode === "number"
273
- ) {
274
- throw err;
275
- }
276
- const formatted = formatConfigError(err);
277
- const honesty = honestyForHarness(kind);
278
- if (opts.json) {
279
- writeJson(stdout, {
280
- honesty,
281
- error: formatted,
282
- });
283
- const e = new Error(formatted.message) as Error & {
284
- exitCode?: number;
285
- };
286
- e.exitCode = 1;
287
- throw e;
288
- }
289
- writeText(stderr, formatted.message);
290
- const e = new Error(formatted.message) as Error & {
291
- exitCode?: number;
292
- };
293
- e.exitCode = 1;
294
- throw e;
295
- }
296
- },
297
- );
298
-
299
- // Silence unused local if tree-shaking complains; keep reference stable.
300
- void install;
301
- }
302
-
303
- export function registerUninstallCommand(
304
- program: Command,
305
- ctx: () => CommandContext,
306
- ): void {
307
- program
308
- .command("uninstall")
309
- .description("Uninstall / roll back OffRouter from a harness profile")
310
- .argument("[harness]", "Harness kind (V1: claude, codex)", "claude")
311
- .option(
312
- "--profile <profile>",
313
- "Target profile id (e.g. claude-personal, codex-personal)",
314
- )
315
- .option(
316
- "--all-personal",
317
- "Uninstall from every allowlisted personal harness profile that has install state",
318
- false,
319
- )
320
- .option("--dry-run", "Show planned rollback without writing", false)
321
- .option("--json", "Emit JSON", false)
322
- .action(
323
- async (
324
- harness: string,
325
- opts: {
326
- profile?: string;
327
- allPersonal?: boolean;
328
- dryRun?: boolean;
329
- json?: boolean;
330
- },
331
- ) => {
332
- const { env, cwd, stdout, stderr } = ctx();
333
- let kind: HarnessKind = "claude";
334
- try {
335
- kind = assertHarness(harness);
336
- const config = await loadConfig({ env, workspaceDir: cwd });
337
- const dryRun = Boolean(opts.dryRun);
338
- const allPersonal = Boolean(opts.allPersonal);
339
- const profile = opts.profile?.trim();
340
-
341
- if (allPersonal && profile) {
342
- throw new Error(
343
- "Use either --profile <id> or --all-personal, not both.",
344
- );
345
- }
346
-
347
- if (!allPersonal && !profile) {
348
- throw new Error(
349
- "Provide --profile <id> or --all-personal. Example: offrouter uninstall claude --profile claude-personal",
350
- );
351
- }
352
-
353
- const targets = allPersonal
354
- ? allowlistedPersonalProfiles(kind, config)
355
- : [profile!];
356
-
357
- const results: RollbackResultUnion[] = [];
358
- for (const target of targets) {
359
- results.push(
360
- await rollbackOne(kind, { profile: target, env, dryRun }),
361
- );
362
- }
363
-
364
- // Map rollback results into install-result-like text blocks.
365
- const asInstallish: InstallResultUnion[] = results.map((r) => ({
366
- ok: r.ok,
367
- dryRun: r.dryRun,
368
- profile: r.profile,
369
- profileDir: r.profileDir,
370
- changes: r.changes,
371
- message: r.message,
372
- errorCode: r.ok
373
- ? undefined
374
- : r.errorCode === "no_install_state"
375
- ? "install_error"
376
- : "install_error",
377
- }));
378
-
379
- const honesty = honestyForHarness(kind);
380
-
381
- if (opts.json) {
382
- writeJson(stdout, {
383
- honesty,
384
- dryRun,
385
- harness: kind,
386
- results,
387
- });
388
- } else {
389
- writeText(stdout, honesty);
390
- writeText(
391
- stdout,
392
- formatDiff(kind, asInstallish, "Uninstall / rollback:").trimEnd(),
393
- );
394
- }
395
-
396
- // Missing state on --all-personal is soft; invalid state/errors fail.
397
- const hardFailures = results.filter(
398
- (r) =>
399
- !r.ok && (!allPersonal || r.errorCode !== "no_install_state"),
400
- );
401
- if (hardFailures.length > 0) {
402
- const msg = hardFailures.map((r) => r.message).join("; ");
403
- const err = new Error(msg) as Error & { exitCode?: number };
404
- err.exitCode = 1;
405
- throw err;
406
- }
407
- } catch (err) {
408
- if (
409
- err &&
410
- typeof err === "object" &&
411
- "exitCode" in err &&
412
- typeof (err as { exitCode: unknown }).exitCode === "number"
413
- ) {
414
- throw err;
415
- }
416
- const formatted = formatConfigError(err);
417
- const honesty = honestyForHarness(kind);
418
- if (opts.json) {
419
- writeJson(stdout, {
420
- honesty,
421
- error: formatted,
422
- });
423
- const e = new Error(formatted.message) as Error & {
424
- exitCode?: number;
425
- };
426
- e.exitCode = 1;
427
- throw e;
428
- }
429
- writeText(stderr, formatted.message);
430
- const e = new Error(formatted.message) as Error & {
431
- exitCode?: number;
432
- };
433
- e.exitCode = 1;
434
- throw e;
435
- }
436
- },
437
- );
438
- }