tokenmaxxing 1.38.0 → 1.39.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/package.json +1 -1
- package/src/cli/check.ts +2 -0
- package/src/lib/update.ts +133 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/check.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { claude } from "../lib/claude.ts";
|
|
|
2
2
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
3
|
import { sampleOldestParked } from "../lib/sample.ts";
|
|
4
4
|
import { loadConfig } from "../lib/state.ts";
|
|
5
|
+
import { maybeAutoUpdate } from "../lib/update.ts";
|
|
5
6
|
import { log } from "../lib/log.ts";
|
|
6
7
|
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ export async function cmdCheck(json = false): Promise<number> {
|
|
|
11
12
|
try {
|
|
12
13
|
d = await evaluateAndMaybeSwap(claude, now);
|
|
13
14
|
await sampleOldestParked(loadConfig());
|
|
15
|
+
await maybeAutoUpdate();
|
|
14
16
|
} catch (e) {
|
|
15
17
|
const detail = e instanceof Error ? e.message : String(e);
|
|
16
18
|
log("check.error", { err: detail });
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
+
import { http } from "./http.ts";
|
|
6
|
+
import { isNixPackaged } from "./install.ts";
|
|
7
|
+
import { withLock } from "./lock.ts";
|
|
8
|
+
import { log } from "./log.ts";
|
|
9
|
+
import { HOME, paths } from "./paths.ts";
|
|
10
|
+
|
|
11
|
+
const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const INSTALL_DEADLINE_MS = 60_000;
|
|
13
|
+
const REGISTRY_DEADLINE_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
const OverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
16
|
+
const AttemptSchema = z.object({ attemptedAt: z.number() });
|
|
17
|
+
const VersionSchema = z.object({ version: z.string().min(1) });
|
|
18
|
+
const LatestSchema = z.object({ version: z.string().min(1), engines: z.looseObject({ bun: z.string().min(1).optional() }).optional() });
|
|
19
|
+
|
|
20
|
+
const updateJson = join(paths.home, "update.json");
|
|
21
|
+
const updateLock = join(paths.home, "update.lock");
|
|
22
|
+
|
|
23
|
+
function registry(): { base: URL; authorization: string | null } {
|
|
24
|
+
const raw = OverrideSchema.parse(process.env.TOKENMAXXING_NPM_REGISTRY) ?? "https://registry.npmjs.org";
|
|
25
|
+
const base = new URL(raw.endsWith("/") ? raw : `${raw}/`);
|
|
26
|
+
const authorization =
|
|
27
|
+
base.username !== "" || base.password !== "" ? `Basic ${btoa(`${decodeURIComponent(base.username)}:${decodeURIComponent(base.password)}`)}` : null;
|
|
28
|
+
base.username = "";
|
|
29
|
+
base.password = "";
|
|
30
|
+
return { base, authorization };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function realOrNull(path: string): string | null {
|
|
34
|
+
try {
|
|
35
|
+
return realpathSync(path);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function globalRootCandidates(): string[] {
|
|
42
|
+
const globalDir = OverrideSchema.parse(process.env.BUN_INSTALL_GLOBAL_DIR);
|
|
43
|
+
const bunInstall = OverrideSchema.parse(process.env.BUN_INSTALL);
|
|
44
|
+
return [
|
|
45
|
+
...(globalDir != null ? [globalDir] : []),
|
|
46
|
+
...(bunInstall != null ? [join(bunInstall, "install", "global")] : []),
|
|
47
|
+
join(dirname(dirname(process.execPath)), "install", "global"),
|
|
48
|
+
join(HOME, ".bun", "install", "global"),
|
|
49
|
+
].map((candidate) => resolve(candidate));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function packageDir(root: string): string {
|
|
53
|
+
return join(root, "node_modules", "tokenmaxxing");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registryPackageDir(root: string): string | null {
|
|
57
|
+
const dir = packageDir(root);
|
|
58
|
+
try {
|
|
59
|
+
return lstatSync(dir).isSymbolicLink() ? null : dir;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectGlobalRoot(): string | null {
|
|
66
|
+
if (isNixPackaged()) return null;
|
|
67
|
+
const entry = realOrNull(Bun.main);
|
|
68
|
+
if (entry == null) return null;
|
|
69
|
+
return (
|
|
70
|
+
globalRootCandidates().find((root) => {
|
|
71
|
+
const dir = registryPackageDir(root);
|
|
72
|
+
return dir != null && entry === realOrNull(join(dir, "src", "main.ts"));
|
|
73
|
+
}) ?? null
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function installedVersion(root: string): string {
|
|
78
|
+
return VersionSchema.parse(JSON.parse(readFileSync(join(packageDir(root), "package.json"), "utf8"))).version;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDue(now: number): boolean {
|
|
82
|
+
if (!existsSync(updateJson)) return true;
|
|
83
|
+
let json: unknown;
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.parse(readFileSync(updateJson, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`${updateJson} is corrupt (unparsable JSON) - repair or remove the file`);
|
|
88
|
+
}
|
|
89
|
+
return now - AttemptSchema.parse(json).attemptedAt >= UPDATE_INTERVAL_MS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function updateToLatest(root: string): Promise<void> {
|
|
93
|
+
const current = installedVersion(root);
|
|
94
|
+
const { base, authorization } = registry();
|
|
95
|
+
const latestUrl = new URL("tokenmaxxing/latest", base).href;
|
|
96
|
+
const signal = AbortSignal.timeout(REGISTRY_DEADLINE_MS);
|
|
97
|
+
const response = await http.get(latestUrl, { signal, headers: authorization ? { authorization } : undefined });
|
|
98
|
+
if (!response.ok) throw new Error(`${latestUrl} answered HTTP ${response.status}`);
|
|
99
|
+
const latest = LatestSchema.parse(await response.json());
|
|
100
|
+
if (Bun.semver.order(latest.version, current) <= 0) return;
|
|
101
|
+
const floor = latest.engines?.bun;
|
|
102
|
+
if (floor != null && !Bun.semver.satisfies(Bun.version, floor)) {
|
|
103
|
+
throw new Error(`tokenmaxxing@${latest.version} needs bun ${floor}, this is bun ${Bun.version}`);
|
|
104
|
+
}
|
|
105
|
+
const child = Bun.spawn([process.execPath, "add", "-g", "--registry", base.href, `tokenmaxxing@${latest.version}`], {
|
|
106
|
+
cwd: paths.home,
|
|
107
|
+
env: { ...process.env, BUN_INSTALL_GLOBAL_DIR: root },
|
|
108
|
+
stdout: "ignore",
|
|
109
|
+
stderr: "inherit",
|
|
110
|
+
timeout: INSTALL_DEADLINE_MS,
|
|
111
|
+
killSignal: "SIGKILL",
|
|
112
|
+
});
|
|
113
|
+
await child.exited;
|
|
114
|
+
if (child.exitCode !== 0) throw new Error(`bun add -g tokenmaxxing@${latest.version} exited ${child.signalCode ?? child.exitCode}`);
|
|
115
|
+
const installed = installedVersion(root);
|
|
116
|
+
if (installed !== latest.version) throw new Error(`bun add -g tokenmaxxing@${latest.version} left ${installed} installed`);
|
|
117
|
+
log("update.done", { from: current, to: latest.version });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function maybeAutoUpdate(): Promise<void> {
|
|
121
|
+
const root = detectGlobalRoot();
|
|
122
|
+
if (root == null) return;
|
|
123
|
+
await withLock(updateLock, async () => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (!isDue(now)) return;
|
|
126
|
+
writeFileAtomic(updateJson, JSON.stringify({ attemptedAt: now }) + "\n");
|
|
127
|
+
try {
|
|
128
|
+
await updateToLatest(root);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(`self-update from npm failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|