tokenmaxxing 1.9.1 → 1.11.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/src/cli/config.ts CHANGED
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { paths, realClaudeBinFromEnv, realCodexBinFromEnv } from "../lib/paths.ts";
6
6
  import { ConfigFileSchema, loadConfig, mergeConfigFile } from "../lib/state.ts";
7
7
  import { writeFileAtomic } from "../lib/atomic.ts";
8
- import { c } from "./render.ts";
8
+ import { c, emitError, emitJson } from "./render.ts";
9
9
 
10
10
  export const KNOWN_KEYS = [
11
11
  "thresholds.session",
@@ -49,22 +49,53 @@ function unknownFileKeys(raw: Record<string, unknown>): string[] {
49
49
  return dottedKeys(raw).filter((key) => !known.has(key));
50
50
  }
51
51
 
52
+ function isKnownKey(key: string): boolean {
53
+ return KNOWN_KEYS.some((known) => known === key);
54
+ }
55
+
52
56
  function envSourceFor(key: string): string | null {
53
57
  if (key === "claudeBin" && realClaudeBinFromEnv()) return "TOKENMAXXING_CLAUDE_BIN";
54
58
  if (key === "codexBin" && realCodexBinFromEnv()) return "TOKENMAXXING_CODEX_BIN";
55
59
  return null;
56
60
  }
57
61
 
58
- function printEffective(): number {
62
+ function sourceOf(input: { key: string; raw: Record<string, unknown> }): { source: "env" | "file" | "default"; env: string | null } {
63
+ const env = envSourceFor(input.key);
64
+ if (env) return { source: "env", env };
65
+ return { source: get(input.raw, input.key) !== undefined ? "file" : "default", env: null };
66
+ }
67
+
68
+ function unknownKey(input: { key: string; json: boolean }): number {
69
+ emitError({
70
+ json: input.json,
71
+ message: `unknown config key: ${input.key}`,
72
+ notes: [`known keys: ${KNOWN_KEYS.join(", ")}`],
73
+ extra: { knownKeys: KNOWN_KEYS },
74
+ });
75
+ return 1;
76
+ }
77
+
78
+ function printEffective(json: boolean): number {
59
79
  const effective = loadConfig();
60
80
  const raw = readRawFile();
81
+ const unknown = unknownFileKeys(raw);
82
+ if (json) {
83
+ const sources: Record<string, string> = {};
84
+ const envOverrides: Record<string, string> = {};
85
+ for (const key of KNOWN_KEYS) {
86
+ const { source, env } = sourceOf({ key, raw });
87
+ sources[key] = source;
88
+ if (env) envOverrides[key] = env;
89
+ }
90
+ emitJson({ ok: true, path: paths.configJson, config: effective, sources, envOverrides, unknownKeys: unknown });
91
+ return 0;
92
+ }
61
93
  console.log(c.dim(`config.json: ${paths.configJson}`));
62
94
  for (const key of KNOWN_KEYS) {
63
- const env = envSourceFor(key);
64
- const source = env ? c.yellow(`env ${env}`) : get(raw, key) !== undefined ? c.green("file") : c.dim("default");
65
- console.log(` ${key.padEnd(28)} ${JSON.stringify(get(effective, key))} ${source}`);
95
+ const { source, env } = sourceOf({ key, raw });
96
+ const painted = source === "env" ? c.yellow(`env ${env}`) : source === "file" ? c.green("file") : c.dim("default");
97
+ console.log(` ${key.padEnd(28)} ${JSON.stringify(get(effective, key))} ${painted}`);
66
98
  }
67
- const unknown = unknownFileKeys(raw);
68
99
  if (unknown.length > 0) {
69
100
  console.log();
70
101
  console.log(c.yellow(`unknown keys in the file (ignored by the loader): ${unknown.join(", ")}`));
@@ -73,13 +104,14 @@ function printEffective(): number {
73
104
  return 0;
74
105
  }
75
106
 
76
- function cmdGet(key: string): number {
77
- if (!KNOWN_KEYS.some((known) => known === key)) {
78
- console.error(c.red(`unknown config key: ${key}`));
79
- console.error(c.dim(`known keys: ${KNOWN_KEYS.join(", ")}`));
80
- return 1;
107
+ function cmdGet(key: string, json: boolean): number {
108
+ if (!isKnownKey(key)) return unknownKey({ key, json });
109
+ const value = get(loadConfig(), key);
110
+ if (json) {
111
+ emitJson({ ok: true, key, value, source: sourceOf({ key, raw: readRawFile() }).source });
112
+ return 0;
81
113
  }
82
- console.log(JSON.stringify(get(loadConfig(), key)));
114
+ console.log(JSON.stringify(value));
83
115
  return 0;
84
116
  }
85
117
 
@@ -91,31 +123,31 @@ function parseValue(text: string): unknown {
91
123
  }
92
124
  }
93
125
 
94
- function cmdSet(key: string, valueText: string): number {
95
- if (!KNOWN_KEYS.some((known) => known === key)) {
96
- console.error(c.red(`unknown config key: ${key}`));
97
- console.error(c.dim(`known keys: ${KNOWN_KEYS.join(", ")}`));
98
- return 1;
99
- }
126
+ function cmdSet(key: string, valueText: string, json: boolean): number {
127
+ if (!isKnownKey(key)) return unknownKey({ key, json });
100
128
  const raw = readRawFile();
101
129
  const next = structuredClone(raw);
102
130
  set(next, key, parseValue(valueText));
103
131
  const validated = ConfigFileSchema.safeParse(next);
104
132
  if (!validated.success) {
105
- console.error(c.red(`rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`));
133
+ emitError({ json, message: `rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}` });
106
134
  return 1;
107
135
  }
108
136
  const mergedCheck = mergeConfigFile(validated.data);
109
137
  if (!mergedCheck.ok) {
110
- console.error(c.red(`rejected: ${mergedCheck.detail}`));
138
+ emitError({ json, message: `rejected: ${mergedCheck.detail}` });
111
139
  return 1;
112
140
  }
113
141
  writeRawFile({ raw: next });
114
142
  const beforeFile = get(raw, key);
143
+ const env = envSourceFor(key);
144
+ if (json) {
145
+ emitJson({ ok: true, key, before: beforeFile === undefined ? null : beforeFile, after: get(next, key), envOverride: env });
146
+ return 0;
147
+ }
115
148
  console.log(
116
149
  `${key}: ${beforeFile === undefined ? "(default)" : JSON.stringify(beforeFile)} -> ${JSON.stringify(get(next, key))}`,
117
150
  );
118
- const env = envSourceFor(key);
119
151
  if (env) console.log(c.yellow(`note: ${env} is set and overrides the file value in this environment`));
120
152
  return 0;
121
153
  }
@@ -128,14 +160,12 @@ function pruneEmptyParents(raw: Record<string, unknown>): void {
128
160
  }
129
161
  }
130
162
 
131
- function cmdUnset(key: string): number {
132
- if (!KNOWN_KEYS.some((known) => known === key)) {
133
- console.error(c.red(`unknown config key: ${key}`));
134
- return 1;
135
- }
163
+ function cmdUnset(key: string, json: boolean): number {
164
+ if (!isKnownKey(key)) return unknownKey({ key, json });
136
165
  const raw = readRawFile();
137
166
  if (get(raw, key) === undefined) {
138
- console.log(c.dim(`${key} has no file override (default already applies)`));
167
+ if (json) emitJson({ ok: true, key, unset: false, value: get(loadConfig(), key) });
168
+ else console.log(c.dim(`${key} has no file override (default already applies)`));
139
169
  return 0;
140
170
  }
141
171
  const next = structuredClone(raw);
@@ -143,20 +173,25 @@ function cmdUnset(key: string): number {
143
173
  pruneEmptyParents(next);
144
174
  const validated = ConfigFileSchema.safeParse(next);
145
175
  if (!validated.success) {
146
- console.error(c.red(`rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`));
176
+ emitError({ json, message: `rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}` });
147
177
  return 1;
148
178
  }
149
179
  const mergedCheck = mergeConfigFile(validated.data);
150
180
  if (!mergedCheck.ok) {
151
- console.error(c.red(`rejected: ${mergedCheck.detail} (adjust the conflicting override before unsetting ${key})`));
181
+ emitError({ json, message: `rejected: ${mergedCheck.detail} (adjust the conflicting override before unsetting ${key})` });
152
182
  return 1;
153
183
  }
154
184
  writeRawFile({ raw: next });
155
- console.log(`${key} unset -> ${JSON.stringify(get(loadConfig(), key))} (default)`);
185
+ const value = get(loadConfig(), key);
186
+ if (json) {
187
+ emitJson({ ok: true, key, unset: true, value });
188
+ return 0;
189
+ }
190
+ console.log(`${key} unset -> ${JSON.stringify(value)} (default)`);
156
191
  return 0;
157
192
  }
158
193
 
159
- function cmdTidy(): number {
194
+ function cmdTidy(json: boolean): number {
160
195
  const raw = readRawFile();
161
196
  const dropped = unknownFileKeys(raw);
162
197
  const parsed = ConfigFileSchema.parse(raw);
@@ -167,30 +202,39 @@ function cmdTidy(): number {
167
202
  if (normalized != null) set(next, "policy.switchModels", normalized);
168
203
  pruneEmptyParents(next);
169
204
 
170
- if (JSON.stringify(next) === JSON.stringify(raw)) {
205
+ const changed = JSON.stringify(next) !== JSON.stringify(raw);
206
+ if (changed) writeRawFile({ raw: next });
207
+ if (json) {
208
+ emitJson({ ok: true, changed, droppedKeys: dropped, casingNormalized: casingChanged });
209
+ return 0;
210
+ }
211
+ if (!changed) {
171
212
  console.log(c.dim("nothing to tidy"));
172
213
  return 0;
173
214
  }
174
- writeRawFile({ raw: next });
175
215
  if (dropped.length > 0) console.log(`dropped unknown keys: ${dropped.join(", ")}`);
176
216
  if (casingChanged) console.log("normalized switchModels casing");
177
217
  if (dropped.length === 0 && !casingChanged) console.log("canonicalized file layout (pruned empty sections / key order)");
178
218
  return 0;
179
219
  }
180
220
 
181
- export function cmdConfig(args: string[]): number {
221
+ export function cmdConfig(args: string[], json = false): number {
182
222
  const [sub, key, value] = args;
183
223
  try {
184
- if (sub === undefined) return printEffective();
185
- if (sub === "get" && key !== undefined) return cmdGet(key);
186
- if (sub === "set" && key !== undefined && value !== undefined) return cmdSet(key, value);
187
- if (sub === "unset" && key !== undefined) return cmdUnset(key);
188
- if (sub === "tidy") return cmdTidy();
224
+ if (sub === undefined) return printEffective(json);
225
+ if (sub === "get" && key !== undefined) return cmdGet(key, json);
226
+ if (sub === "set" && key !== undefined && value !== undefined) return cmdSet(key, value, json);
227
+ if (sub === "unset" && key !== undefined) return cmdUnset(key, json);
228
+ if (sub === "tidy") return cmdTidy(json);
189
229
  } catch (e) {
190
- console.error(c.red(`config.json is unreadable: ${e instanceof Error ? e.message : String(e)}`));
191
- console.error(c.dim(`fix or delete ${paths.configJson} (defaults apply when it is absent), then re-run`));
230
+ emitError({
231
+ json,
232
+ message: `config.json is unreadable: ${e instanceof Error ? e.message : String(e)}`,
233
+ notes: [`fix or delete ${paths.configJson} (defaults apply when it is absent), then re-run`],
234
+ extra: { path: paths.configJson },
235
+ });
192
236
  return 1;
193
237
  }
194
- console.error(c.red("usage: tokenmaxxing config [get <key> | set <key> <value> | unset <key> | tidy]"));
238
+ emitError({ json, message: "usage: tokenmaxxing config [get <key> | set <key> <value> | unset <key> | tidy]" });
195
239
  return 2;
196
240
  }
package/src/cli/doctor.ts CHANGED
@@ -7,7 +7,7 @@ import { loadAccounts, loadConfig } from "../lib/state.ts";
7
7
  import { readItem, liveTarget, parkedTarget } from "../lib/credstore.ts";
8
8
  import { isAccessTokenExpiring, fetchTokenOrg } from "../lib/oauth.ts";
9
9
  import { CredentialBlobSchema, type RolesResponse } from "../lib/types.ts";
10
- import { c } from "./render.ts";
10
+ import { c, emitJson } from "./render.ts";
11
11
 
12
12
  async function blobOrg(raw: string): Promise<RolesResponse | null> {
13
13
  const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
@@ -15,11 +15,21 @@ async function blobOrg(raw: string): Promise<RolesResponse | null> {
15
15
  return fetchTokenOrg(creds.accessToken);
16
16
  }
17
17
 
18
- export async function cmdDoctor(): Promise<number> {
19
- let ok = true;
18
+ export async function cmdDoctor(json = false): Promise<number> {
19
+ const checks: { ok: boolean; label: string; hint: string | null }[] = [];
20
+ const notes: string[] = [];
21
+ const warnings: string[] = [];
20
22
  const check = (cond: boolean, label: string, hint?: string) => {
21
- console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
22
- if (!cond) ok = false;
23
+ checks.push({ ok: cond, label, hint: cond ? null : (hint ?? null) });
24
+ if (!json) console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
25
+ };
26
+ const note = (text: string) => {
27
+ notes.push(text);
28
+ if (!json) console.log(c.dim(` - ${text}`));
29
+ };
30
+ const warn = (text: string) => {
31
+ warnings.push(text);
32
+ if (!json) console.log(c.yellow(`⚠ ${text}`));
23
33
  };
24
34
 
25
35
  check(existsSync(paths.supervisorLink), "claude supervisor wrapper present", "run `tokenmaxxing init`");
@@ -46,7 +56,7 @@ export async function cmdDoctor(): Promise<number> {
46
56
  try {
47
57
  const org = await blobOrg(live);
48
58
  if (org) check(org.organization_uuid === active.organizationUuid, `live credential identity matches active (${active.email})`, `token belongs to ${org.organization_name} - run \`tokenmaxxing switch\``);
49
- else console.log(c.dim(` - live credential identity unverifiable (access token expired)`));
59
+ else note("live credential identity unverifiable (access token expired)");
50
60
  } catch (e) {
51
61
  check(false, `live credential identity matches active (${active.email})`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
52
62
  }
@@ -59,7 +69,7 @@ export async function cmdDoctor(): Promise<number> {
59
69
  try {
60
70
  const org = await blobOrg(parked);
61
71
  if (org) check(org.organization_uuid === a.organizationUuid, `parked credential identity matches ${a.email}`, `token belongs to ${org.organization_name} - run \`tokenmaxxing auth ${a.label}\``);
62
- else console.log(c.dim(` - ${a.email} identity unverifiable (access token expired)`));
72
+ else note(`${a.email} identity unverifiable (access token expired)`);
63
73
  } catch (e) {
64
74
  check(false, `parked credential identity matches ${a.email}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
65
75
  }
@@ -77,11 +87,17 @@ export async function cmdDoctor(): Promise<number> {
77
87
  const rc = shellRcPath();
78
88
  if (rc && existsSync(rc)) {
79
89
  for (const s of findClaudeShadowers(readFileSync(rc, "utf8"))) {
80
- if (s.kind === "shadow") console.log(c.yellow(`⚠ ${rc}: \`${s.line}\` shadows the supervised claude wrapper - launches through it skip tokenmaxxing`));
81
- else console.log(c.yellow(`⚠ ${rc}: alias \`${s.name}\` hardcodes a claude path and bypasses the supervisor - use plain \`claude\` in its body instead`));
90
+ if (s.kind === "shadow") warn(`${rc}: ${s.line.startsWith("alias ") ? "alias" : "function"} \`claude\` shadows the supervised claude wrapper - launches through it skip tokenmaxxing`);
91
+ else warn(`${rc}: alias \`${s.name}\` hardcodes a claude path and bypasses the supervisor - use plain \`claude\` in its body instead`);
82
92
  }
83
93
  }
84
94
 
95
+ const failed = checks.filter((entry) => !entry.ok).length;
96
+ const ok = failed === 0;
97
+ if (json) {
98
+ emitJson(ok ? { ok, checks, notes, warnings } : { ok, error: `issues found - ${failed} of ${checks.length} checks failed`, checks, notes, warnings });
99
+ return ok ? 0 : 1;
100
+ }
85
101
  console.log();
86
102
  console.log(ok ? c.green("all good ✓") : c.yellow("issues found - see above"));
87
103
  return ok ? 0 : 1;
package/src/cli/ls.ts CHANGED
@@ -1,10 +1,45 @@
1
1
  import { loadAccounts } from "../lib/state.ts";
2
2
  import { loadCodexAccounts } from "../lib/codexstate.ts";
3
3
  import { liveCodexAccountId } from "../lib/codexsample.ts";
4
- import { c, claudeTierLabel } from "./render.ts";
4
+ import { c, claudeTierLabel, emitJson } from "./render.ts";
5
5
 
6
- export function cmdLs(): number {
6
+ export function cmdLs(json = false): number {
7
7
  const idx = loadAccounts();
8
+
9
+ if (json) {
10
+ const codex = loadCodexAccounts();
11
+ const liveId = codex.accounts.length > 0 ? liveCodexAccountId() : null;
12
+ emitJson({
13
+ ok: true,
14
+ claude: {
15
+ activeAccountUuid: idx.activeAccountUuid,
16
+ accounts: idx.accounts.map((a) => ({
17
+ label: a.label,
18
+ email: a.email,
19
+ accountUuid: a.accountUuid,
20
+ organizationUuid: a.organizationUuid,
21
+ tier: claudeTierLabel(a),
22
+ active: a.accountUuid === idx.activeAccountUuid,
23
+ needsReauth: a.needsReauth === true,
24
+ addedAt: a.addedAt,
25
+ })),
26
+ },
27
+ codex: {
28
+ activeAccountId: liveId,
29
+ accounts: codex.accounts.map((account) => ({
30
+ label: account.label,
31
+ email: account.email,
32
+ accountId: account.accountId,
33
+ planType: account.planType,
34
+ active: account.accountId === liveId,
35
+ needsReauth: account.needsReauth === true,
36
+ addedAt: account.addedAt,
37
+ })),
38
+ },
39
+ });
40
+ return 0;
41
+ }
42
+
8
43
  if (idx.accounts.length === 0) {
9
44
  console.log(c.dim("no accounts yet - run `tokenmaxxing init` then `tokenmaxxing add`"));
10
45
  }
package/src/cli/rename.ts CHANGED
@@ -2,7 +2,7 @@ import { withLock } from "../lib/lock.ts";
2
2
  import { codexPaths, paths } from "../lib/paths.ts";
3
3
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
4
4
  import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
5
- import { c } from "./render.ts";
5
+ import { c, emitError, emitJson, plain } from "./render.ts";
6
6
  import type { Account, CodexAccount } from "../lib/types.ts";
7
7
 
8
8
  export function findAccount(accounts: Account[], selector: string): Account | undefined {
@@ -23,51 +23,54 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
23
23
  );
24
24
  }
25
25
 
26
- async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
26
+ async function renameCodexAccount(input: { selector: string; newLabel: string; json: boolean }): Promise<number> {
27
+ const { selector, newLabel, json } = input;
27
28
  return withLock(codexPaths.lockFile, async () => {
28
29
  const index = loadCodexAccounts();
29
- const account = findCodexAccount(index.accounts, input.selector);
30
+ const account = findCodexAccount(index.accounts, selector);
30
31
  if (!account) {
31
- console.error(c.red(`no codex account matches "${input.selector}"`));
32
+ emitError({ json, message: `no codex account matches "${selector}"` });
32
33
  return 1;
33
34
  }
34
- const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === input.newLabel.toLowerCase());
35
+ const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
35
36
  if (taken) {
36
- console.error(c.red(`label "${input.newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool`));
37
+ emitError({ json, message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
37
38
  return 1;
38
39
  }
39
40
  const old = account.label;
40
- account.label = input.newLabel;
41
+ account.label = newLabel;
41
42
  saveCodexAccounts({ index });
42
- console.log(`renamed codex account ${c.dim(old)} ${c.bold(input.newLabel)}`);
43
+ if (json) emitJson({ ok: true, pool: "codex", from: old, to: newLabel });
44
+ else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
43
45
  return 0;
44
46
  });
45
47
  }
46
48
 
47
- export async function cmdRename(argv: string[]): Promise<number> {
49
+ export async function cmdRename(argv: string[], json = false): Promise<number> {
48
50
  const codex = argv.includes("--codex");
49
51
  const [selector, newLabel] = argv.filter((a) => a !== "--codex");
50
52
  if (!selector || !newLabel) {
51
- console.error("usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>");
53
+ emitError({ json, message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
52
54
  return 2;
53
55
  }
54
- if (codex) return renameCodexAccount({ selector, newLabel });
56
+ if (codex) return renameCodexAccount({ selector, newLabel, json });
55
57
  return withLock(paths.lockFile, async () => {
56
58
  const idx = loadAccounts();
57
59
  const a = findAccount(idx.accounts, selector);
58
60
  if (!a) {
59
- console.error(c.red(`no claude account matches "${selector}" (codex accounts rename via --codex)`));
61
+ emitError({ json, message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
60
62
  return 1;
61
63
  }
62
64
  const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
63
65
  if (taken) {
64
- console.error(c.red(`label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool`));
66
+ emitError({ json, message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
65
67
  return 1;
66
68
  }
67
69
  const old = a.label;
68
70
  a.label = newLabel;
69
71
  saveAccounts(idx);
70
- console.log(`renamed ${c.dim(old)} ${c.bold(newLabel)}`);
72
+ if (json) emitJson({ ok: true, pool: "claude", from: old, to: newLabel });
73
+ else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
71
74
  return 0;
72
75
  });
73
76
  }
package/src/cli/render.ts CHANGED
@@ -75,3 +75,23 @@ export function fmtAgo(epochMs: number, now = Date.now()): string {
75
75
  return `${m}m ago`;
76
76
  }
77
77
 
78
+ export const plain = (s: string): string => s;
79
+
80
+ export function emitJson(value: unknown): void {
81
+ console.log(JSON.stringify(value, null, 2));
82
+ }
83
+
84
+ export function emitError(input: {
85
+ json: boolean;
86
+ message: string;
87
+ notes?: string[];
88
+ extra?: Record<string, unknown>;
89
+ paint?: (s: string) => string;
90
+ }): void {
91
+ if (input.json) {
92
+ emitJson({ ok: false, error: input.message, ...input.extra });
93
+ return;
94
+ }
95
+ console.error((input.paint ?? c.red)(input.message));
96
+ for (const note of input.notes ?? []) console.error(c.dim(note));
97
+ }
package/src/cli/rm.ts CHANGED
@@ -7,22 +7,22 @@ import { credItemFor, paths } from "../lib/paths.ts";
7
7
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
8
8
  import { CredentialBlobSchema } from "../lib/types.ts";
9
9
  import { findAccount } from "./rename.ts";
10
- import { c } from "./render.ts";
10
+ import { c, emitError, emitJson, plain } from "./render.ts";
11
11
 
12
- export async function cmdRm(selector?: string): Promise<number> {
12
+ export async function cmdRm(selector?: string, json = false): Promise<number> {
13
13
  if (!selector) {
14
- console.error("usage: tokenmaxxing rm <email|label|uuid>");
14
+ emitError({ json, message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
15
15
  return 2;
16
16
  }
17
17
  return withLock(paths.lockFile, async () => {
18
18
  const idx = loadAccounts();
19
19
  const a = findAccount(idx.accounts, selector);
20
20
  if (!a) {
21
- console.error(c.red(`no account matches "${selector}"`));
21
+ emitError({ json, message: `no account matches "${selector}"` });
22
22
  return 1;
23
23
  }
24
24
  if (a.accountUuid === idx.activeAccountUuid) {
25
- console.error(c.red(`${a.email} is the ACTIVE account - switch away before removing it.`));
25
+ emitError({ json, message: `${a.email} is the ACTIVE account - switch away before removing it.` });
26
26
  return 1;
27
27
  }
28
28
  const live = await readItem(liveTarget());
@@ -32,11 +32,14 @@ export async function cmdRm(selector?: string): Promise<number> {
32
32
  const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
33
33
  liveOrg = (await fetchTokenOrg(liveCreds.accessToken)).organization_uuid;
34
34
  } catch (e) {
35
- console.error(c.red(`cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`));
35
+ emitError({
36
+ json,
37
+ message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`,
38
+ });
36
39
  return 1;
37
40
  }
38
41
  if (liveOrg === a.organizationUuid) {
39
- console.error(c.red(`${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.`));
42
+ emitError({ json, message: `${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.` });
40
43
  return 1;
41
44
  }
42
45
  }
@@ -46,7 +49,8 @@ export async function cmdRm(selector?: string): Promise<number> {
46
49
  rmSync(sampleDir, { recursive: true, force: true });
47
50
  idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
48
51
  saveAccounts(idx);
49
- console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
52
+ if (json) emitJson({ ok: true, pool: "claude", removed: a.label, remaining: idx.accounts.length });
53
+ else console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
50
54
  return 0;
51
55
  });
52
56
  }