diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f144cc..d6e86dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- [2026-07-23] an update re-applies routed configs on the next daemon start, fixes #17 - [2026-07-23] an out-of-date dashboard says restart and refuses changes - [2026-07-23] claude routing keeps the native login, fixes #15 - [2026-07-22] session reset time on analytics, fixes #9 diff --git a/bun.lock b/bun.lock index e4ed9ed..2c35b3e 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "devDependencies": { "@biomejs/biome": "^2.0.0", "@rubriclab/config": "*", - "@rubriclab/package": "^0.0.125", + "@rubriclab/package": "*", "@types/bun": "^1.2.0", "typescript": "^5.8.0", }, diff --git a/package.json b/package.json index 1e20747..4e156d2 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,5 @@ "post-commit": "bun x @rubriclab/package post-commit" }, "type": "module", - "version": "0.0.58" + "version": "0.0.59" } diff --git a/src/cli.ts b/src/cli.ts index 0662886..d450542 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import { z } from 'zod' import { registerClaudeAccount, registerClaudeApiKeyAccount } from './claude.ts' import { registerCodexAccount, registerOpenAiApiKeyAccount } from './codex.ts' import { + healInstalledConfigs, installClaudeConfig, installCodexConfig, installStatus, @@ -298,6 +299,19 @@ async function runDaemon(context: ApplicationContext): Promise { if (await managerAvailable(context.paths.managerSocket)) { throw new ApplicationError('DAEMON_RUNNING', 'The manager daemon is already running') } + // ensureDaemon restarts the daemon on a version change, so this runs on the + // first start after every update; a failure must not keep the daemon down. + const healed = await healInstalledConfigs(context.paths).catch(error => { + process.stderr.write( + `[${new Date().toISOString()}] config heal failed: ${errorMessage(error)}\n` + ) + return [] + }) + if (healed.length > 0) { + process.stdout.write( + `[${new Date().toISOString()}] re-applied ${healed.join(' and ')} routing for v${VERSION}\n` + ) + } const manager = new AccountManager({ paths: context.paths, store: context.store, diff --git a/src/config-install.test.ts b/src/config-install.test.ts index b1c7782..4c40e13 100644 --- a/src/config-install.test.ts +++ b/src/config-install.test.ts @@ -4,6 +4,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { + healInstalledConfigs, installClaudeConfig, installCodexConfig, installStatus, @@ -197,3 +198,37 @@ describe('installClaudeConfig', () => { expect(settings.env).toBeUndefined() }) }) + +describe('healInstalledConfigs', () => { + test('re-applies every routed config, healing what an older version wrote', async () => { + await installCodexConfig(paths()) + await writeClaudeSettings({ + env: { + ANTHROPIC_AUTH_TOKEN: 'managed-by-tokenmaxx', + ANTHROPIC_BASE_URL: 'http://127.0.0.1:8459/anthropic' + } + }) + expect(await healInstalledConfigs(paths())).toEqual(['codex', 'claude']) + const settings = await readClaudeSettings() + expect(settings.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined() + expect(settings.env?.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:8459/anthropic') + }) + + test('never adds routing to an unrouted harness', async () => { + expect(await healInstalledConfigs(paths())).toEqual([]) + await expect(readClaudeSettings()).rejects.toThrow() + expect((await installStatus()).codexRouted).toBe(false) + }) + + test('runs once per version, not on every start', async () => { + await healInstalledConfigs(paths()) + await writeClaudeSettings({ + env: { + ANTHROPIC_AUTH_TOKEN: 'managed-by-tokenmaxx', + ANTHROPIC_BASE_URL: 'http://127.0.0.1:8459/anthropic' + } + }) + expect(await healInstalledConfigs(paths())).toEqual([]) + expect((await readClaudeSettings()).env?.ANTHROPIC_AUTH_TOKEN).toBe('managed-by-tokenmaxx') + }) +}) diff --git a/src/config-install.ts b/src/config-install.ts index 73841b5..cbab16d 100644 --- a/src/config-install.ts +++ b/src/config-install.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os' import { dirname, join } from 'node:path' import type { ApplicationPaths } from './paths.ts' import { proxyBaseUrl } from './paths.ts' +import { VERSION } from './version.ts' const providerName = 'tokenmaxx' const topBeginMarker = '# >>> tokenmaxx managed (do not edit) >>>' @@ -195,3 +196,26 @@ export async function installStatus(): Promise { } return { claudeRouted, codexRouted, codexStale } } + +// Configs written by an older version stay stale after an update (#17): re-apply +// install for whatever is currently routed, once per version change. Never adds +// routing — a harness the user uninstalled or never installed stays untouched. +export async function healInstalledConfigs(paths: ApplicationPaths): Promise { + const stampPath = join(paths.root, 'healed-version') + if ((await readFileOrEmpty(stampPath)).trim() === VERSION) { + return [] + } + const { claudeRouted, codexRouted } = await installStatus() + const healed: string[] = [] + if (codexRouted) { + await installCodexConfig(paths) + healed.push('codex') + } + if (claudeRouted) { + await installClaudeConfig(paths) + healed.push('claude') + } + await mkdir(paths.root, { recursive: true }) + await writeFile(stampPath, `${VERSION}\n`) + return healed +}