Skip to content

Commit 299094e

Browse files
author
liyanbowne
committed
refactor(layout): group account, desktop, and watch modules
1 parent 8c951bc commit 299094e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+5983
-5948
lines changed

src/account-store-config.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1 @@
1-
import type { AuthSnapshot } from "./auth-snapshot.js";
2-
3-
export function validateConfigSnapshot(
4-
name: string,
5-
snapshot: AuthSnapshot,
6-
rawConfig: string | null,
7-
): void {
8-
if (snapshot.auth_mode !== "apikey") {
9-
return;
10-
}
11-
12-
if (!rawConfig) {
13-
throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
14-
}
15-
16-
if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) {
17-
throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
18-
}
19-
20-
if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) {
21-
throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
22-
}
23-
}
24-
25-
export function sanitizeConfigForAccountAuth(rawConfig: string): string {
26-
const lines = rawConfig.split(/\r?\n/u);
27-
const result: string[] = [];
28-
let skippingProviderSection = false;
29-
30-
for (const line of lines) {
31-
const trimmed = line.trim();
32-
33-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
34-
skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
35-
if (skippingProviderSection) {
36-
continue;
37-
}
38-
}
39-
40-
if (skippingProviderSection) {
41-
continue;
42-
}
43-
44-
if (/^\s*model_provider\s*=/u.test(line)) {
45-
continue;
46-
}
47-
48-
if (/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) {
49-
continue;
50-
}
51-
52-
result.push(line);
53-
}
54-
55-
return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
56-
}
1+
export * from "./account-store/config.js";

src/account-store-repository.ts

Lines changed: 1 addition & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,227 +1 @@
1-
import { join } from "node:path";
2-
3-
import {
4-
AuthSnapshot,
5-
SnapshotMeta,
6-
createSnapshotMeta,
7-
getMetaIdentity,
8-
getSnapshotAccountId,
9-
getSnapshotIdentity,
10-
getSnapshotUserId,
11-
isSupportedChatGPTAuthMode,
12-
parseSnapshotMeta,
13-
readAuthSnapshotFile,
14-
} from "./auth-snapshot.js";
15-
import type { ManagedAccount, StorePaths, StoreState } from "./account-store-types.js";
16-
import {
17-
DIRECTORY_MODE,
18-
FILE_MODE,
19-
SCHEMA_VERSION,
20-
atomicWriteFile,
21-
ensureAccountName,
22-
ensureDirectory,
23-
pathExists,
24-
readJsonFile,
25-
stringifyJson,
26-
} from "./account-store-storage.js";
27-
28-
function canAutoMigrateLegacyChatGPTMeta(
29-
meta: SnapshotMeta,
30-
snapshot: AuthSnapshot,
31-
): boolean {
32-
if (!isSupportedChatGPTAuthMode(meta.auth_mode) || !isSupportedChatGPTAuthMode(snapshot.auth_mode)) {
33-
return false;
34-
}
35-
36-
if (typeof meta.user_id === "string" && meta.user_id.trim() !== "") {
37-
return false;
38-
}
39-
40-
const snapshotUserId = getSnapshotUserId(snapshot);
41-
if (!snapshotUserId) {
42-
return false;
43-
}
44-
45-
return meta.account_id === getSnapshotAccountId(snapshot);
46-
}
47-
48-
export class AccountStoreRepository {
49-
readonly paths: StorePaths;
50-
51-
constructor(paths: StorePaths) {
52-
this.paths = paths;
53-
}
54-
55-
accountDirectory(name: string): string {
56-
ensureAccountName(name);
57-
return join(this.paths.accountsDir, name);
58-
}
59-
60-
accountAuthPath(name: string): string {
61-
return join(this.accountDirectory(name), "auth.json");
62-
}
63-
64-
accountMetaPath(name: string): string {
65-
return join(this.accountDirectory(name), "meta.json");
66-
}
67-
68-
accountConfigPath(name: string): string {
69-
return join(this.accountDirectory(name), "config.toml");
70-
}
71-
72-
async writeAccountAuthSnapshot(name: string, snapshot: AuthSnapshot): Promise<void> {
73-
await atomicWriteFile(
74-
this.accountAuthPath(name),
75-
stringifyJson(snapshot),
76-
);
77-
}
78-
79-
async writeAccountMeta(name: string, meta: SnapshotMeta): Promise<void> {
80-
await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
81-
}
82-
83-
async ensureEmptyAccountConfigSnapshot(name: string): Promise<string> {
84-
const configPath = this.accountConfigPath(name);
85-
await atomicWriteFile(configPath, "");
86-
return configPath;
87-
}
88-
89-
async syncCurrentAuthIfMatching(snapshot: AuthSnapshot): Promise<void> {
90-
if (!(await pathExists(this.paths.currentAuthPath))) {
91-
return;
92-
}
93-
94-
try {
95-
const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
96-
if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) {
97-
return;
98-
}
99-
100-
await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
101-
} catch {
102-
// Ignore sync failures here; the stored snapshot is already updated.
103-
}
104-
}
105-
106-
async ensureLayout(): Promise<void> {
107-
await ensureDirectory(this.paths.codexTeamDir, DIRECTORY_MODE);
108-
await ensureDirectory(this.paths.accountsDir, DIRECTORY_MODE);
109-
await ensureDirectory(this.paths.backupsDir, DIRECTORY_MODE);
110-
}
111-
112-
async readState(): Promise<StoreState> {
113-
if (!(await pathExists(this.paths.statePath))) {
114-
return {
115-
schema_version: SCHEMA_VERSION,
116-
last_switched_account: null,
117-
last_backup_path: null,
118-
};
119-
}
120-
121-
const raw = await readJsonFile(this.paths.statePath);
122-
const parsed = JSON.parse(raw) as Partial<StoreState>;
123-
124-
return {
125-
schema_version: parsed.schema_version ?? SCHEMA_VERSION,
126-
last_switched_account: parsed.last_switched_account ?? null,
127-
last_backup_path: parsed.last_backup_path ?? null,
128-
};
129-
}
130-
131-
async writeState(state: StoreState): Promise<void> {
132-
await this.ensureLayout();
133-
await atomicWriteFile(this.paths.statePath, stringifyJson(state));
134-
}
135-
136-
async readManagedAccount(name: string): Promise<ManagedAccount> {
137-
const metaPath = this.accountMetaPath(name);
138-
const authPath = this.accountAuthPath(name);
139-
const [rawMeta, snapshot] = await Promise.all([
140-
readJsonFile(metaPath),
141-
readAuthSnapshotFile(authPath),
142-
]);
143-
let meta = parseSnapshotMeta(rawMeta);
144-
145-
if (meta.name !== name) {
146-
throw new Error(`Account metadata name mismatch for "${name}".`);
147-
}
148-
149-
const snapshotIdentity = getSnapshotIdentity(snapshot);
150-
if (getMetaIdentity(meta) !== snapshotIdentity) {
151-
if (canAutoMigrateLegacyChatGPTMeta(meta, snapshot)) {
152-
meta = {
153-
...meta,
154-
account_id: getSnapshotAccountId(snapshot),
155-
user_id: getSnapshotUserId(snapshot),
156-
};
157-
await this.writeAccountMeta(name, meta);
158-
} else {
159-
throw new Error(`Account metadata account_id mismatch for "${name}".`);
160-
}
161-
}
162-
163-
if (getMetaIdentity(meta) !== snapshotIdentity) {
164-
throw new Error(`Account metadata account_id mismatch for "${name}".`);
165-
}
166-
167-
return {
168-
...meta,
169-
identity: getMetaIdentity(meta),
170-
authPath,
171-
metaPath,
172-
configPath: (await pathExists(this.accountConfigPath(name))) ? this.accountConfigPath(name) : null,
173-
duplicateAccountId: false,
174-
};
175-
}
176-
177-
async listAccounts(): Promise<{ accounts: ManagedAccount[]; warnings: string[] }> {
178-
await this.ensureLayout();
179-
180-
const { readdir } = await import("node:fs/promises");
181-
const entries = await readdir(this.paths.accountsDir, { withFileTypes: true });
182-
const accounts: ManagedAccount[] = [];
183-
const warnings: string[] = [];
184-
185-
for (const entry of entries) {
186-
if (!entry.isDirectory()) {
187-
continue;
188-
}
189-
190-
try {
191-
accounts.push(await this.readManagedAccount(entry.name));
192-
} catch (error) {
193-
warnings.push(
194-
`Account "${entry.name}" is invalid: ${(error as Error).message}`,
195-
);
196-
}
197-
}
198-
199-
const counts = new Map<string, number>();
200-
for (const account of accounts) {
201-
counts.set(account.identity, (counts.get(account.identity) ?? 0) + 1);
202-
}
203-
204-
accounts.sort((left, right) => left.name.localeCompare(right.name));
205-
206-
return {
207-
accounts: accounts.map((account) => ({
208-
...account,
209-
duplicateAccountId: (counts.get(account.identity) ?? 0) > 1,
210-
})),
211-
warnings,
212-
};
213-
}
214-
215-
async readCurrentStatusAccounts() {
216-
return await this.listAccounts();
217-
}
218-
219-
createSnapshotMeta(
220-
name: string,
221-
snapshot: AuthSnapshot,
222-
now: Date,
223-
createdAt?: string | null,
224-
) {
225-
return createSnapshotMeta(name, snapshot, now, createdAt ?? undefined);
226-
}
227-
}
1+
export * from "./account-store/repository.js";

src/account-store-storage.ts

Lines changed: 1 addition & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1 @@
1-
import { homedir } from "node:os";
2-
import { basename, dirname, join } from "node:path";
3-
import { chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
4-
5-
import type { StorePaths } from "./account-store-types.js";
6-
7-
export const DIRECTORY_MODE = 0o700;
8-
export const FILE_MODE = 0o600;
9-
export const SCHEMA_VERSION = 1;
10-
export const QUOTA_REFRESH_CONCURRENCY = 3;
11-
12-
const ACCOUNT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
13-
14-
export function defaultPaths(homeDir = homedir()): StorePaths {
15-
const codexDir = join(homeDir, ".codex");
16-
const codexTeamDir = join(homeDir, ".codex-team");
17-
18-
return {
19-
homeDir,
20-
codexDir,
21-
codexTeamDir,
22-
currentAuthPath: join(codexDir, "auth.json"),
23-
currentConfigPath: join(codexDir, "config.toml"),
24-
accountsDir: join(codexTeamDir, "accounts"),
25-
backupsDir: join(codexTeamDir, "backups"),
26-
statePath: join(codexTeamDir, "state.json"),
27-
};
28-
}
29-
30-
export async function chmodIfPossible(path: string, mode: number): Promise<void> {
31-
try {
32-
await chmod(path, mode);
33-
} catch (error) {
34-
const nodeError = error as NodeJS.ErrnoException;
35-
if (nodeError.code !== "ENOENT") {
36-
throw error;
37-
}
38-
}
39-
}
40-
41-
export async function ensureDirectory(path: string, mode: number): Promise<void> {
42-
await mkdir(path, { recursive: true, mode });
43-
await chmodIfPossible(path, mode);
44-
}
45-
46-
export async function atomicWriteFile(
47-
path: string,
48-
content: string,
49-
mode = FILE_MODE,
50-
): Promise<void> {
51-
const directory = dirname(path);
52-
const tempPath = join(
53-
directory,
54-
`.${basename(path)}.${process.pid}.${Date.now()}.tmp`,
55-
);
56-
57-
await ensureDirectory(directory, DIRECTORY_MODE);
58-
await writeFile(tempPath, content, { encoding: "utf8", mode });
59-
await chmodIfPossible(tempPath, mode);
60-
await rename(tempPath, path);
61-
await chmodIfPossible(path, mode);
62-
}
63-
64-
export function stringifyJson(value: unknown): string {
65-
return `${JSON.stringify(value, null, 2)}\n`;
66-
}
67-
68-
export function ensureAccountName(name: string): void {
69-
if (!ACCOUNT_NAME_PATTERN.test(name)) {
70-
throw new Error(
71-
'Account name must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/ and cannot contain path separators.',
72-
);
73-
}
74-
}
75-
76-
export async function pathExists(path: string): Promise<boolean> {
77-
try {
78-
await stat(path);
79-
return true;
80-
} catch (error) {
81-
const nodeError = error as NodeJS.ErrnoException;
82-
if (nodeError.code === "ENOENT") {
83-
return false;
84-
}
85-
86-
throw error;
87-
}
88-
}
89-
90-
export async function readJsonFile(path: string): Promise<string> {
91-
return readFile(path, "utf8");
92-
}
1+
export * from "./account-store/storage.js";

0 commit comments

Comments
 (0)