codexuse-cli 2.1.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/README.md +25 -0
- package/dist/index.js +3439 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// ../../lib/auto-roll-settings.ts
|
|
34
|
+
function clampNumber(value, min, max) {
|
|
35
|
+
return Math.min(max, Math.max(min, value));
|
|
36
|
+
}
|
|
37
|
+
function normalizeAutoRollSettings(raw) {
|
|
38
|
+
const enabled = typeof raw?.enabled === "boolean" ? raw.enabled : DEFAULT_AUTO_ROLL_ENABLED;
|
|
39
|
+
const rawWarning = typeof raw?.warningThreshold === "number" && Number.isFinite(raw.warningThreshold) ? raw.warningThreshold : DEFAULT_AUTO_ROLL_WARNING_THRESHOLD;
|
|
40
|
+
const rawSwitch = typeof raw?.switchThreshold === "number" && Number.isFinite(raw.switchThreshold) ? raw.switchThreshold : DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD;
|
|
41
|
+
const normalizedWarning = clampNumber(rawWarning, AUTO_ROLL_WARNING_MIN, 99);
|
|
42
|
+
const normalizedSwitch = clampNumber(rawSwitch, normalizedWarning + 1, 100);
|
|
43
|
+
return {
|
|
44
|
+
enabled,
|
|
45
|
+
warningThreshold: normalizedWarning,
|
|
46
|
+
switchThreshold: normalizedSwitch
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var DEFAULT_AUTO_ROLL_ENABLED, DEFAULT_AUTO_ROLL_WARNING_THRESHOLD, DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD, AUTO_ROLL_WARNING_MIN;
|
|
50
|
+
var init_auto_roll_settings = __esm({
|
|
51
|
+
"../../lib/auto-roll-settings.ts"() {
|
|
52
|
+
"use strict";
|
|
53
|
+
DEFAULT_AUTO_ROLL_ENABLED = false;
|
|
54
|
+
DEFAULT_AUTO_ROLL_WARNING_THRESHOLD = 85;
|
|
55
|
+
DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD = 95;
|
|
56
|
+
AUTO_ROLL_WARNING_MIN = 50;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ../../lib/sqlite-db.ts
|
|
61
|
+
var sqlite_db_exports = {};
|
|
62
|
+
__export(sqlite_db_exports, {
|
|
63
|
+
__debugResolveDbPath: () => __debugResolveDbPath,
|
|
64
|
+
flushDatabase: () => flushDatabase,
|
|
65
|
+
getDatabase: () => getDatabase,
|
|
66
|
+
resetDatabaseConnection: () => resetDatabaseConnection
|
|
67
|
+
});
|
|
68
|
+
async function flushDatabase() {
|
|
69
|
+
if (globalCache.cachedDbPromise) {
|
|
70
|
+
const db = await globalCache.cachedDbPromise;
|
|
71
|
+
if (db instanceof SqliteWasmDatabase) {
|
|
72
|
+
await db.flushPersistence();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isPlainObject(value) {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function normalizeNamedParameters(params) {
|
|
80
|
+
const normalized = {};
|
|
81
|
+
for (const [key, rawValue] of Object.entries(params)) {
|
|
82
|
+
if (key.startsWith("@") || key.startsWith(":") || key.startsWith("$")) {
|
|
83
|
+
normalized[key] = rawValue;
|
|
84
|
+
} else {
|
|
85
|
+
normalized[`@${key}`] = rawValue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
function bindParameters(statement, args) {
|
|
91
|
+
if (args.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (args.length === 1 && isPlainObject(args[0])) {
|
|
95
|
+
statement.bind(normalizeNamedParameters(args[0]));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
statement.bind(args);
|
|
99
|
+
}
|
|
100
|
+
function resolveHomeDir() {
|
|
101
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
102
|
+
if (!homeDir) {
|
|
103
|
+
throw new Error("HOME directory is not set. Unable to initialize CodexUse storage.");
|
|
104
|
+
}
|
|
105
|
+
return homeDir;
|
|
106
|
+
}
|
|
107
|
+
function resolveStorageDirectory() {
|
|
108
|
+
return import_node_path.default.join(resolveHomeDir(), STORAGE_DIRECTORY_NAME);
|
|
109
|
+
}
|
|
110
|
+
function resolveDbPath() {
|
|
111
|
+
return import_node_path.default.join(resolveStorageDirectory(), STORAGE_FILENAME);
|
|
112
|
+
}
|
|
113
|
+
function ensureStorageDirExists(targetPath) {
|
|
114
|
+
(0, import_node_fs.mkdirSync)(import_node_path.default.dirname(targetPath), { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
async function instantiateDatabase(dbPath) {
|
|
117
|
+
try {
|
|
118
|
+
const bakPath = `${dbPath}.bak`;
|
|
119
|
+
if ((0, import_node_fs.existsSync)(dbPath)) {
|
|
120
|
+
const size = (0, import_node_fs.statSync)(dbPath).size;
|
|
121
|
+
if (size < 1024 && (0, import_node_fs.existsSync)(bakPath)) {
|
|
122
|
+
const bakSize = (0, import_node_fs.statSync)(bakPath).size;
|
|
123
|
+
if (bakSize > size) {
|
|
124
|
+
const backup = (0, import_node_fs.readFileSync)(bakPath);
|
|
125
|
+
(0, import_node_fs.writeFileSync)(dbPath, backup);
|
|
126
|
+
console.warn("Detected near-empty SQLite file; restored from backup.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn("Failed to check/restore SQLite backup:", error);
|
|
132
|
+
}
|
|
133
|
+
const SQL = await sqlModulePromise;
|
|
134
|
+
let fileBuffer;
|
|
135
|
+
if ((0, import_node_fs.existsSync)(dbPath)) {
|
|
136
|
+
fileBuffer = new Uint8Array((0, import_node_fs.readFileSync)(dbPath));
|
|
137
|
+
}
|
|
138
|
+
const driver = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
|
|
139
|
+
const database = new SqliteWasmDatabase(driver, dbPath);
|
|
140
|
+
database.exec("PRAGMA journal_mode = WAL;");
|
|
141
|
+
database.exec("PRAGMA foreign_keys = ON;");
|
|
142
|
+
runMigrations(database);
|
|
143
|
+
database.persist();
|
|
144
|
+
return database;
|
|
145
|
+
}
|
|
146
|
+
function readUserVersion(database) {
|
|
147
|
+
const result = database.prepare("PRAGMA user_version").get();
|
|
148
|
+
const version = result?.user_version;
|
|
149
|
+
return typeof version === "number" ? version : 0;
|
|
150
|
+
}
|
|
151
|
+
function ensureChatTables(database) {
|
|
152
|
+
database.exec(`
|
|
153
|
+
PRAGMA foreign_keys = ON;
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS chat_threads (
|
|
156
|
+
thread_key TEXT PRIMARY KEY,
|
|
157
|
+
title TEXT,
|
|
158
|
+
account_id TEXT,
|
|
159
|
+
profile_name TEXT,
|
|
160
|
+
project_label TEXT,
|
|
161
|
+
project_path TEXT,
|
|
162
|
+
source_path TEXT,
|
|
163
|
+
started_at TEXT,
|
|
164
|
+
ended_at TEXT,
|
|
165
|
+
last_message_at TEXT,
|
|
166
|
+
message_count INTEGER DEFAULT 0
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
170
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
171
|
+
thread_key TEXT NOT NULL,
|
|
172
|
+
role TEXT,
|
|
173
|
+
content TEXT NOT NULL,
|
|
174
|
+
account_id TEXT,
|
|
175
|
+
profile_name TEXT,
|
|
176
|
+
created_at TEXT,
|
|
177
|
+
FOREIGN KEY(thread_key) REFERENCES chat_threads(thread_key) ON DELETE CASCADE
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_key);
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created ON chat_messages(created_at);
|
|
182
|
+
|
|
183
|
+
CREATE TABLE IF NOT EXISTS chat_ingest_offsets (
|
|
184
|
+
file_path TEXT PRIMARY KEY,
|
|
185
|
+
offset INTEGER NOT NULL,
|
|
186
|
+
mtime_ms INTEGER NOT NULL,
|
|
187
|
+
size INTEGER NOT NULL
|
|
188
|
+
);
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
function ensureSessionTables(database) {
|
|
192
|
+
database.exec(`
|
|
193
|
+
PRAGMA foreign_keys = ON;
|
|
194
|
+
|
|
195
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
196
|
+
id TEXT PRIMARY KEY,
|
|
197
|
+
title TEXT,
|
|
198
|
+
status TEXT,
|
|
199
|
+
model TEXT,
|
|
200
|
+
project_label TEXT,
|
|
201
|
+
project_path TEXT,
|
|
202
|
+
created_at TEXT NOT NULL,
|
|
203
|
+
updated_at TEXT NOT NULL,
|
|
204
|
+
last_message_at TEXT
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE TABLE IF NOT EXISTS tool_panels (
|
|
208
|
+
id TEXT PRIMARY KEY,
|
|
209
|
+
session_id TEXT NOT NULL,
|
|
210
|
+
type TEXT NOT NULL,
|
|
211
|
+
title TEXT,
|
|
212
|
+
state TEXT,
|
|
213
|
+
metadata TEXT,
|
|
214
|
+
created_at TEXT NOT NULL,
|
|
215
|
+
updated_at TEXT NOT NULL,
|
|
216
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_tool_panels_session ON tool_panels(session_id);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_tool_panels_type ON tool_panels(type);
|
|
221
|
+
|
|
222
|
+
CREATE TABLE IF NOT EXISTS panel_outputs (
|
|
223
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
224
|
+
panel_id TEXT NOT NULL,
|
|
225
|
+
session_id TEXT NOT NULL,
|
|
226
|
+
type TEXT NOT NULL,
|
|
227
|
+
data TEXT NOT NULL,
|
|
228
|
+
created_at TEXT NOT NULL,
|
|
229
|
+
FOREIGN KEY(panel_id) REFERENCES tool_panels(id) ON DELETE CASCADE,
|
|
230
|
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_panel_outputs_panel ON panel_outputs(panel_id);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_panel_outputs_session ON panel_outputs(session_id);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_panel_outputs_created ON panel_outputs(created_at);
|
|
236
|
+
|
|
237
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
238
|
+
id TEXT PRIMARY KEY,
|
|
239
|
+
user_id TEXT,
|
|
240
|
+
title TEXT NOT NULL,
|
|
241
|
+
content TEXT NOT NULL,
|
|
242
|
+
tags TEXT,
|
|
243
|
+
is_favorited INTEGER NOT NULL DEFAULT 0,
|
|
244
|
+
created_at TEXT NOT NULL,
|
|
245
|
+
updated_at TEXT NOT NULL,
|
|
246
|
+
synced_at TEXT
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updated_at);
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
function ensureUsageTables(database) {
|
|
254
|
+
database.exec(`
|
|
255
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
256
|
+
session_id TEXT PRIMARY KEY,
|
|
257
|
+
rollout_path TEXT NOT NULL,
|
|
258
|
+
project_path TEXT,
|
|
259
|
+
model TEXT,
|
|
260
|
+
input_tokens INTEGER NOT NULL,
|
|
261
|
+
cached_input_tokens INTEGER NOT NULL,
|
|
262
|
+
output_tokens INTEGER NOT NULL,
|
|
263
|
+
reasoning_output_tokens INTEGER NOT NULL,
|
|
264
|
+
total_tokens INTEGER NOT NULL,
|
|
265
|
+
timestamp TEXT NOT NULL
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage(timestamp);
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
271
|
+
function runMigrations(database) {
|
|
272
|
+
let userVersion = readUserVersion(database);
|
|
273
|
+
if (userVersion < 1) {
|
|
274
|
+
database.exec(`
|
|
275
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
276
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
277
|
+
name TEXT NOT NULL UNIQUE,
|
|
278
|
+
data TEXT NOT NULL,
|
|
279
|
+
account_id TEXT,
|
|
280
|
+
workspace_id TEXT,
|
|
281
|
+
workspace_name TEXT,
|
|
282
|
+
email TEXT,
|
|
283
|
+
auth_method TEXT,
|
|
284
|
+
created_at TEXT,
|
|
285
|
+
updated_at TEXT
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_account_workspace
|
|
289
|
+
ON profiles(account_id, workspace_id)
|
|
290
|
+
WHERE account_id IS NOT NULL;
|
|
291
|
+
|
|
292
|
+
PRAGMA user_version = 1;
|
|
293
|
+
`);
|
|
294
|
+
userVersion = 1;
|
|
295
|
+
}
|
|
296
|
+
if (userVersion < 2) {
|
|
297
|
+
database.exec(`PRAGMA user_version = 2;`);
|
|
298
|
+
userVersion = 2;
|
|
299
|
+
}
|
|
300
|
+
if (userVersion < 3) {
|
|
301
|
+
database.exec(`PRAGMA user_version = 3;`);
|
|
302
|
+
userVersion = 3;
|
|
303
|
+
}
|
|
304
|
+
if (userVersion < 4) {
|
|
305
|
+
let legacySettings = null;
|
|
306
|
+
try {
|
|
307
|
+
const hasKvStore = Boolean(
|
|
308
|
+
database.prepare(
|
|
309
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'kv_store'"
|
|
310
|
+
).get()
|
|
311
|
+
);
|
|
312
|
+
if (hasKvStore) {
|
|
313
|
+
const row = database.prepare("SELECT value FROM kv_store WHERE key = 'settings'").get();
|
|
314
|
+
if (row?.value) {
|
|
315
|
+
const parsed = JSON.parse(row.value);
|
|
316
|
+
if (parsed && typeof parsed === "object") {
|
|
317
|
+
legacySettings = parsed;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn("Failed to read legacy Codex settings:", error);
|
|
323
|
+
}
|
|
324
|
+
database.exec(`
|
|
325
|
+
PRAGMA foreign_keys = OFF;
|
|
326
|
+
|
|
327
|
+
DROP TABLE IF EXISTS rate_limit_snapshots;
|
|
328
|
+
DROP TABLE IF EXISTS chat_messages;
|
|
329
|
+
DROP TABLE IF EXISTS chat_threads;
|
|
330
|
+
DROP TABLE IF EXISTS kv_store;
|
|
331
|
+
|
|
332
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
333
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
334
|
+
last_profile_name TEXT,
|
|
335
|
+
license_key TEXT,
|
|
336
|
+
purchase_email TEXT,
|
|
337
|
+
last_verified_at TEXT,
|
|
338
|
+
next_check_at TEXT,
|
|
339
|
+
last_verification_error TEXT,
|
|
340
|
+
status TEXT
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
INSERT OR IGNORE INTO app_settings (id) VALUES (1);
|
|
344
|
+
|
|
345
|
+
PRAGMA foreign_keys = ON;
|
|
346
|
+
`);
|
|
347
|
+
if (legacySettings) {
|
|
348
|
+
const license = legacySettings.license;
|
|
349
|
+
database.prepare(
|
|
350
|
+
`UPDATE app_settings
|
|
351
|
+
SET last_profile_name = @lastProfileName,
|
|
352
|
+
license_key = @licenseKey,
|
|
353
|
+
purchase_email = @purchaseEmail,
|
|
354
|
+
last_verified_at = @lastVerifiedAt,
|
|
355
|
+
next_check_at = @nextCheckAt,
|
|
356
|
+
last_verification_error = @lastVerificationError,
|
|
357
|
+
status = @status
|
|
358
|
+
WHERE id = 1`
|
|
359
|
+
).run({
|
|
360
|
+
lastProfileName: typeof legacySettings.lastProfileName === "string" ? legacySettings.lastProfileName : null,
|
|
361
|
+
licenseKey: license && typeof license.licenseKey === "string" ? license.licenseKey : null,
|
|
362
|
+
purchaseEmail: license && typeof license.purchaseEmail === "string" ? license.purchaseEmail : null,
|
|
363
|
+
lastVerifiedAt: license && typeof license.lastVerifiedAt === "string" ? license.lastVerifiedAt : null,
|
|
364
|
+
nextCheckAt: license && typeof license.nextCheckAt === "string" ? license.nextCheckAt : null,
|
|
365
|
+
lastVerificationError: license && typeof license.lastVerificationError === "string" ? license.lastVerificationError : null,
|
|
366
|
+
status: license && typeof license.status === "string" ? license.status : null
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
database.exec(`PRAGMA user_version = 4;`);
|
|
370
|
+
userVersion = 4;
|
|
371
|
+
}
|
|
372
|
+
if (userVersion < 5) {
|
|
373
|
+
database.exec(`
|
|
374
|
+
ALTER TABLE app_settings ADD COLUMN auto_roll_enabled INTEGER;
|
|
375
|
+
ALTER TABLE app_settings ADD COLUMN auto_roll_warning_threshold INTEGER;
|
|
376
|
+
ALTER TABLE app_settings ADD COLUMN auto_roll_switch_threshold INTEGER;
|
|
377
|
+
|
|
378
|
+
PRAGMA user_version = 5;
|
|
379
|
+
`);
|
|
380
|
+
userVersion = 5;
|
|
381
|
+
}
|
|
382
|
+
if (userVersion < 6) {
|
|
383
|
+
database.exec(`
|
|
384
|
+
CREATE TABLE IF NOT EXISTS app_kv (
|
|
385
|
+
key TEXT PRIMARY KEY,
|
|
386
|
+
value TEXT NOT NULL
|
|
387
|
+
);
|
|
388
|
+
`);
|
|
389
|
+
let appSettingsRow = null;
|
|
390
|
+
try {
|
|
391
|
+
const hasAppSettings = Boolean(
|
|
392
|
+
database.prepare(
|
|
393
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'app_settings'"
|
|
394
|
+
).get()
|
|
395
|
+
);
|
|
396
|
+
if (hasAppSettings) {
|
|
397
|
+
appSettingsRow = database.prepare(
|
|
398
|
+
`SELECT last_profile_name, license_key, purchase_email,
|
|
399
|
+
last_verified_at, next_check_at, last_verification_error, status,
|
|
400
|
+
auto_roll_enabled, auto_roll_warning_threshold, auto_roll_switch_threshold
|
|
401
|
+
FROM app_settings
|
|
402
|
+
WHERE id = 1`
|
|
403
|
+
).get();
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.warn("Failed to read legacy app_settings row:", error);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const upsertKv = database.prepare("INSERT OR REPLACE INTO app_kv (key, value) VALUES (@key, @value)");
|
|
410
|
+
if (appSettingsRow) {
|
|
411
|
+
const lastProfile = typeof appSettingsRow.last_profile_name === "string" ? appSettingsRow.last_profile_name.trim() : "";
|
|
412
|
+
if (lastProfile) {
|
|
413
|
+
upsertKv.run({ key: "last_profile_name", value: JSON.stringify(lastProfile) });
|
|
414
|
+
}
|
|
415
|
+
const license = {
|
|
416
|
+
licenseKey: typeof appSettingsRow.license_key === "string" ? appSettingsRow.license_key : null,
|
|
417
|
+
purchaseEmail: typeof appSettingsRow.purchase_email === "string" ? appSettingsRow.purchase_email : null,
|
|
418
|
+
lastVerifiedAt: typeof appSettingsRow.last_verified_at === "string" ? appSettingsRow.last_verified_at : null,
|
|
419
|
+
nextCheckAt: typeof appSettingsRow.next_check_at === "string" ? appSettingsRow.next_check_at : null,
|
|
420
|
+
lastVerificationError: typeof appSettingsRow.last_verification_error === "string" ? appSettingsRow.last_verification_error : null,
|
|
421
|
+
status: typeof appSettingsRow.status === "string" ? appSettingsRow.status : null
|
|
422
|
+
};
|
|
423
|
+
const hasLicenseData = Boolean(
|
|
424
|
+
license.licenseKey ?? license.purchaseEmail ?? license.lastVerifiedAt ?? license.nextCheckAt ?? license.lastVerificationError ?? license.status
|
|
425
|
+
);
|
|
426
|
+
if (hasLicenseData) {
|
|
427
|
+
upsertKv.run({ key: "license", value: JSON.stringify(license) });
|
|
428
|
+
}
|
|
429
|
+
const hasAutoRollData = typeof appSettingsRow.auto_roll_enabled === "number" || typeof appSettingsRow.auto_roll_warning_threshold === "number" || typeof appSettingsRow.auto_roll_switch_threshold === "number";
|
|
430
|
+
if (hasAutoRollData) {
|
|
431
|
+
const warning = typeof appSettingsRow.auto_roll_warning_threshold === "number" ? appSettingsRow.auto_roll_warning_threshold : DEFAULT_AUTO_ROLL_WARNING_THRESHOLD;
|
|
432
|
+
const switchThreshold = typeof appSettingsRow.auto_roll_switch_threshold === "number" ? appSettingsRow.auto_roll_switch_threshold : DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD;
|
|
433
|
+
const autoRoll = {
|
|
434
|
+
enabled: Boolean(appSettingsRow.auto_roll_enabled ?? DEFAULT_AUTO_ROLL_ENABLED),
|
|
435
|
+
warningThreshold: warning,
|
|
436
|
+
switchThreshold
|
|
437
|
+
};
|
|
438
|
+
upsertKv.run({ key: "auto_roll", value: JSON.stringify(autoRoll) });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
database.exec("DROP TABLE IF EXISTS app_settings;");
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn("Failed to drop legacy app_settings table:", error);
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.warn("Failed to migrate app_settings to app_kv:", error);
|
|
448
|
+
}
|
|
449
|
+
database.exec(`PRAGMA user_version = 6;`);
|
|
450
|
+
userVersion = 6;
|
|
451
|
+
}
|
|
452
|
+
if (userVersion < 7) {
|
|
453
|
+
try {
|
|
454
|
+
const columns = database.prepare("PRAGMA table_info(profiles)").all();
|
|
455
|
+
const hasWorkspaceId = Boolean(columns?.some((column) => column.name === "workspace_id"));
|
|
456
|
+
const hasWorkspaceName = Boolean(columns?.some((column) => column.name === "workspace_name"));
|
|
457
|
+
if (!hasWorkspaceId) {
|
|
458
|
+
database.exec(`ALTER TABLE profiles ADD COLUMN workspace_id TEXT;`);
|
|
459
|
+
}
|
|
460
|
+
if (!hasWorkspaceName) {
|
|
461
|
+
database.exec(`ALTER TABLE profiles ADD COLUMN workspace_name TEXT;`);
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.warn("Failed to add workspace columns during migration:", error);
|
|
465
|
+
}
|
|
466
|
+
database.exec(`
|
|
467
|
+
DROP INDEX IF EXISTS idx_profiles_account;
|
|
468
|
+
DROP INDEX IF EXISTS idx_profiles_account_workspace;
|
|
469
|
+
|
|
470
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_profiles_account_workspace
|
|
471
|
+
ON profiles(account_id, workspace_id)
|
|
472
|
+
WHERE account_id IS NOT NULL;
|
|
473
|
+
`);
|
|
474
|
+
try {
|
|
475
|
+
const rows = database.prepare("SELECT name, workspace_id FROM profiles").all();
|
|
476
|
+
const update = database.prepare(
|
|
477
|
+
"UPDATE profiles SET workspace_id = @workspaceId WHERE name = @name AND (workspace_id IS NULL OR workspace_id = '')"
|
|
478
|
+
);
|
|
479
|
+
for (const row of rows) {
|
|
480
|
+
const name = typeof row.name === "string" ? row.name : null;
|
|
481
|
+
if (!name) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const existingWorkspace = typeof row.workspace_id === "string" ? row.workspace_id.trim() : "";
|
|
485
|
+
if (existingWorkspace) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
update.run({ name, workspaceId: "__default__" });
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.warn("Failed to backfill workspace_id during migration:", error);
|
|
492
|
+
}
|
|
493
|
+
database.exec(`PRAGMA user_version = 7;`);
|
|
494
|
+
userVersion = 7;
|
|
495
|
+
}
|
|
496
|
+
if (userVersion < 8) {
|
|
497
|
+
ensureChatTables(database);
|
|
498
|
+
database.exec(`PRAGMA user_version = 8;`);
|
|
499
|
+
userVersion = 8;
|
|
500
|
+
}
|
|
501
|
+
if (userVersion < 9) {
|
|
502
|
+
ensureSessionTables(database);
|
|
503
|
+
database.exec(`PRAGMA user_version = 9;`);
|
|
504
|
+
userVersion = 9;
|
|
505
|
+
}
|
|
506
|
+
if (userVersion < 10) {
|
|
507
|
+
ensureUsageTables(database);
|
|
508
|
+
database.exec(`PRAGMA user_version = 10;`);
|
|
509
|
+
userVersion = 10;
|
|
510
|
+
}
|
|
511
|
+
if (userVersion < 11) {
|
|
512
|
+
try {
|
|
513
|
+
database.exec(`ALTER TABLE usage ADD COLUMN model TEXT;`);
|
|
514
|
+
} catch {
|
|
515
|
+
}
|
|
516
|
+
database.exec(`PRAGMA user_version = 11;`);
|
|
517
|
+
userVersion = 11;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
database.exec("PRAGMA foreign_keys = ON;");
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
ensureChatTables(database);
|
|
524
|
+
ensureSessionTables(database);
|
|
525
|
+
ensureUsageTables(database);
|
|
526
|
+
}
|
|
527
|
+
async function getDatabase() {
|
|
528
|
+
const dbPath = resolveDbPath();
|
|
529
|
+
if (!globalCache.cachedDbPromise || globalCache.cachedDbPath !== dbPath) {
|
|
530
|
+
if (globalCache.cachedDbPromise) {
|
|
531
|
+
try {
|
|
532
|
+
const previous = await globalCache.cachedDbPromise;
|
|
533
|
+
previous.close();
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
globalCache.cachedDbPath = dbPath;
|
|
538
|
+
globalCache.cachedDbPromise = instantiateDatabase(dbPath);
|
|
539
|
+
}
|
|
540
|
+
const db = await globalCache.cachedDbPromise;
|
|
541
|
+
try {
|
|
542
|
+
runMigrations(db);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.warn("Failed to ensure migrations:", error);
|
|
545
|
+
}
|
|
546
|
+
return db;
|
|
547
|
+
}
|
|
548
|
+
async function resetDatabaseConnection() {
|
|
549
|
+
if (globalCache.cachedDbPromise) {
|
|
550
|
+
try {
|
|
551
|
+
const db = await globalCache.cachedDbPromise;
|
|
552
|
+
db.close();
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
globalCache.cachedDbPromise = null;
|
|
557
|
+
globalCache.cachedDbPath = null;
|
|
558
|
+
}
|
|
559
|
+
function __debugResolveDbPath() {
|
|
560
|
+
return resolveDbPath();
|
|
561
|
+
}
|
|
562
|
+
var import_node_fs, import_node_path, import_node_module, import_sql, require2, wasmDir, sqlModulePromise, SqliteWasmDatabase, SqliteWasmStatement, STORAGE_DIRECTORY_NAME, STORAGE_FILENAME, GLOBAL_CACHE_KEY, globalCache;
|
|
563
|
+
var init_sqlite_db = __esm({
|
|
564
|
+
"../../lib/sqlite-db.ts"() {
|
|
565
|
+
"use strict";
|
|
566
|
+
import_node_fs = require("fs");
|
|
567
|
+
import_node_path = __toESM(require("path"));
|
|
568
|
+
import_node_module = require("module");
|
|
569
|
+
import_sql = __toESM(require("sql.js"));
|
|
570
|
+
init_auto_roll_settings();
|
|
571
|
+
require2 = (0, import_node_module.createRequire)(__filename);
|
|
572
|
+
wasmDir = import_node_path.default.dirname(require2.resolve("sql.js/dist/sql-wasm.wasm"));
|
|
573
|
+
sqlModulePromise = (0, import_sql.default)({
|
|
574
|
+
locateFile: (file) => import_node_path.default.join(wasmDir, file)
|
|
575
|
+
});
|
|
576
|
+
SqliteWasmDatabase = class {
|
|
577
|
+
constructor(driver, dbPath) {
|
|
578
|
+
this.driver = driver;
|
|
579
|
+
this.dbPath = dbPath;
|
|
580
|
+
this.closed = false;
|
|
581
|
+
this.persistTimeout = null;
|
|
582
|
+
}
|
|
583
|
+
get open() {
|
|
584
|
+
return !this.closed;
|
|
585
|
+
}
|
|
586
|
+
prepare(sql) {
|
|
587
|
+
this.assertOpen();
|
|
588
|
+
return new SqliteWasmStatement(this, sql);
|
|
589
|
+
}
|
|
590
|
+
createStatement(sql) {
|
|
591
|
+
this.assertOpen();
|
|
592
|
+
return this.driver.prepare(sql);
|
|
593
|
+
}
|
|
594
|
+
exec(sql) {
|
|
595
|
+
this.assertOpen();
|
|
596
|
+
this.driver.exec(sql);
|
|
597
|
+
}
|
|
598
|
+
getRowsModified() {
|
|
599
|
+
this.assertOpen();
|
|
600
|
+
return this.driver.getRowsModified();
|
|
601
|
+
}
|
|
602
|
+
async persist() {
|
|
603
|
+
this.assertOpen();
|
|
604
|
+
ensureStorageDirExists(this.dbPath);
|
|
605
|
+
if (this.persistTimeout) {
|
|
606
|
+
clearTimeout(this.persistTimeout);
|
|
607
|
+
}
|
|
608
|
+
this.persistTimeout = setTimeout(async () => {
|
|
609
|
+
try {
|
|
610
|
+
const contents = Buffer.from(this.driver.export());
|
|
611
|
+
await import_node_fs.promises.writeFile(this.dbPath, contents);
|
|
612
|
+
this.persistTimeout = null;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
console.error("Failed to persist database:", error);
|
|
615
|
+
}
|
|
616
|
+
}, 500);
|
|
617
|
+
}
|
|
618
|
+
close() {
|
|
619
|
+
if (!this.closed) {
|
|
620
|
+
if (this.persistTimeout) {
|
|
621
|
+
clearTimeout(this.persistTimeout);
|
|
622
|
+
this.persistTimeout = null;
|
|
623
|
+
try {
|
|
624
|
+
const contents = Buffer.from(this.driver.export());
|
|
625
|
+
(0, import_node_fs.writeFileSync)(this.dbPath, contents);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
if (error.code !== "ENOENT") {
|
|
628
|
+
console.error("Failed to flush database on close:", error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
this.driver.close();
|
|
633
|
+
this.closed = true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async flushPersistence() {
|
|
637
|
+
if (this.persistTimeout) {
|
|
638
|
+
clearTimeout(this.persistTimeout);
|
|
639
|
+
this.persistTimeout = null;
|
|
640
|
+
try {
|
|
641
|
+
const contents = Buffer.from(this.driver.export());
|
|
642
|
+
await import_node_fs.promises.writeFile(this.dbPath, contents);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
console.error("Failed to flush database persistence:", error);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
assertOpen() {
|
|
649
|
+
if (!this.open) {
|
|
650
|
+
throw new Error("SQLite database connection is closed.");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
SqliteWasmStatement = class {
|
|
655
|
+
constructor(database, sql) {
|
|
656
|
+
this.database = database;
|
|
657
|
+
this.sql = sql;
|
|
658
|
+
}
|
|
659
|
+
get(...params) {
|
|
660
|
+
const statement = this.database.createStatement(this.sql);
|
|
661
|
+
try {
|
|
662
|
+
bindParameters(statement, params);
|
|
663
|
+
if (!statement.step()) {
|
|
664
|
+
return void 0;
|
|
665
|
+
}
|
|
666
|
+
return statement.getAsObject();
|
|
667
|
+
} finally {
|
|
668
|
+
statement.free();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
all(...params) {
|
|
672
|
+
const rows = [];
|
|
673
|
+
const statement = this.database.createStatement(this.sql);
|
|
674
|
+
try {
|
|
675
|
+
bindParameters(statement, params);
|
|
676
|
+
while (statement.step()) {
|
|
677
|
+
rows.push(statement.getAsObject());
|
|
678
|
+
}
|
|
679
|
+
} finally {
|
|
680
|
+
statement.free();
|
|
681
|
+
}
|
|
682
|
+
return rows;
|
|
683
|
+
}
|
|
684
|
+
run(...params) {
|
|
685
|
+
const statement = this.database.createStatement(this.sql);
|
|
686
|
+
try {
|
|
687
|
+
bindParameters(statement, params);
|
|
688
|
+
statement.step();
|
|
689
|
+
} finally {
|
|
690
|
+
statement.free();
|
|
691
|
+
}
|
|
692
|
+
const changes = this.database.getRowsModified();
|
|
693
|
+
void this.database.persist();
|
|
694
|
+
return { changes };
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
STORAGE_DIRECTORY_NAME = ".f86eb5e712267207";
|
|
698
|
+
STORAGE_FILENAME = "state-d64ce728d7a20214.sqlite";
|
|
699
|
+
GLOBAL_CACHE_KEY = /* @__PURE__ */ Symbol.for("codex.sqliteCache");
|
|
700
|
+
globalCache = globalThis[GLOBAL_CACHE_KEY] ?? (globalThis[GLOBAL_CACHE_KEY] = {
|
|
701
|
+
cachedDbPath: null,
|
|
702
|
+
cachedDbPromise: null
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// ../../lib/profile-manager.ts
|
|
708
|
+
var import_fs = require("fs");
|
|
709
|
+
var import_path = __toESM(require("path"));
|
|
710
|
+
var import_path2 = require("path");
|
|
711
|
+
var import_toml = require("@iarna/toml");
|
|
712
|
+
|
|
713
|
+
// ../../lib/codex-settings.ts
|
|
714
|
+
var import_node_fs2 = require("fs");
|
|
715
|
+
var import_node_path2 = __toESM(require("path"));
|
|
716
|
+
|
|
717
|
+
// ../../lib/logger.ts
|
|
718
|
+
var isTestEnv = process.env.NODE_ENV === "test" || process.env.VITEST === "true" || process.env.VITEST === "1";
|
|
719
|
+
function isMocked(fn) {
|
|
720
|
+
return Boolean(fn && typeof fn === "function" && "mock" in fn);
|
|
721
|
+
}
|
|
722
|
+
function logWarn(...args) {
|
|
723
|
+
if (isTestEnv && !isMocked(console.warn)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
console.warn(...args);
|
|
727
|
+
}
|
|
728
|
+
function logError(...args) {
|
|
729
|
+
if (isTestEnv && !isMocked(console.error)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
console.error(...args);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ../../lib/codex-settings.ts
|
|
736
|
+
init_auto_roll_settings();
|
|
737
|
+
|
|
738
|
+
// ../../lib/codex-cli-channel.ts
|
|
739
|
+
var KNOWN_CHANNELS = /* @__PURE__ */ new Set(["stable", "alpha"]);
|
|
740
|
+
function parseCodexCliChannel(value) {
|
|
741
|
+
if (typeof value !== "string") {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
const normalized = value.trim().toLowerCase();
|
|
745
|
+
if (!normalized) {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
return KNOWN_CHANNELS.has(normalized) ? normalized : null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ../../lib/codex-settings.ts
|
|
752
|
+
var KEY_LAST_PROFILE_NAME = "last_profile_name";
|
|
753
|
+
var KEY_LICENSE = "license";
|
|
754
|
+
var KEY_AUTO_ROLL = "auto_roll";
|
|
755
|
+
var KEY_LAST_APP_VERSION = "last_app_version";
|
|
756
|
+
var KEY_PENDING_UPDATE_VERSION = "pending_update_version";
|
|
757
|
+
var KEY_CLI_CHANNEL = "cli_channel";
|
|
758
|
+
var SETTINGS_FILE = "settings.json";
|
|
759
|
+
var SETTINGS_BACKUP_FILE = "settings.json.bak";
|
|
760
|
+
function resolveCodexDir() {
|
|
761
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
762
|
+
return homeDir ? import_node_path2.default.join(homeDir, ".codex") : ".codex";
|
|
763
|
+
}
|
|
764
|
+
function resolveSettingsPath() {
|
|
765
|
+
return import_node_path2.default.join(resolveCodexDir(), SETTINGS_FILE);
|
|
766
|
+
}
|
|
767
|
+
function resolveBackupSettingsPath() {
|
|
768
|
+
return import_node_path2.default.join(resolveCodexDir(), SETTINGS_BACKUP_FILE);
|
|
769
|
+
}
|
|
770
|
+
async function ensureCodexDir() {
|
|
771
|
+
try {
|
|
772
|
+
await import_node_fs2.promises.mkdir(resolveCodexDir(), { recursive: true });
|
|
773
|
+
} catch (error) {
|
|
774
|
+
logWarn("Failed to ensure Codex directory exists:", error);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function writeAtomic(filePath, contents) {
|
|
778
|
+
const tempPath = `${filePath}.tmp`;
|
|
779
|
+
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(filePath), { recursive: true });
|
|
780
|
+
await import_node_fs2.promises.writeFile(tempPath, contents, "utf8");
|
|
781
|
+
try {
|
|
782
|
+
await import_node_fs2.promises.rename(tempPath, filePath);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
const code = error.code;
|
|
785
|
+
if (code === "ENOENT") {
|
|
786
|
+
await import_node_fs2.promises.writeFile(filePath, contents, "utf8");
|
|
787
|
+
} else {
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
await import_node_fs2.promises.rm(tempPath, { force: true });
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function normalizeStoredLicense(raw) {
|
|
797
|
+
if (!raw || typeof raw !== "object") {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const source = raw;
|
|
801
|
+
const licenseKey = typeof source.licenseKey === "string" ? source.licenseKey : typeof source.license_key === "string" ? source.license_key : null;
|
|
802
|
+
const purchaseEmail = typeof source.purchaseEmail === "string" ? source.purchaseEmail : typeof source.purchase_email === "string" ? source.purchase_email : null;
|
|
803
|
+
const lastVerifiedAt = typeof source.lastVerifiedAt === "string" ? source.lastVerifiedAt : typeof source.last_verified_at === "string" ? source.last_verified_at : null;
|
|
804
|
+
const nextCheckAt = typeof source.nextCheckAt === "string" ? source.nextCheckAt : typeof source.next_check_at === "string" ? source.next_check_at : null;
|
|
805
|
+
const lastVerificationError = typeof source.lastVerificationError === "string" ? source.lastVerificationError : typeof source.last_verification_error === "string" ? source.last_verification_error : null;
|
|
806
|
+
const signature = typeof source.signature === "string" ? source.signature : null;
|
|
807
|
+
const statusCandidate = typeof source.status === "string" ? source.status : void 0;
|
|
808
|
+
const license = {
|
|
809
|
+
licenseKey,
|
|
810
|
+
purchaseEmail,
|
|
811
|
+
lastVerifiedAt,
|
|
812
|
+
nextCheckAt,
|
|
813
|
+
lastVerificationError,
|
|
814
|
+
signature,
|
|
815
|
+
status: statusCandidate
|
|
816
|
+
};
|
|
817
|
+
const hasLicenseData = Boolean(
|
|
818
|
+
license.licenseKey ?? license.purchaseEmail ?? license.lastVerifiedAt ?? license.nextCheckAt ?? license.lastVerificationError ?? license.status
|
|
819
|
+
);
|
|
820
|
+
return hasLicenseData ? license : null;
|
|
821
|
+
}
|
|
822
|
+
function normalizeStoredAutoRoll(raw) {
|
|
823
|
+
if (!raw) {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
const parsed = typeof raw === "string" ? parseJsonValue(raw) : raw;
|
|
827
|
+
if (!parsed || typeof parsed !== "object") {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
return normalizeAutoRollSettings(parsed);
|
|
832
|
+
} catch {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function parseStoredProfileName(raw) {
|
|
837
|
+
if (raw === null || raw === void 0) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
841
|
+
const normalized = String(raw).trim();
|
|
842
|
+
return normalized.length > 0 ? normalized : null;
|
|
843
|
+
}
|
|
844
|
+
const candidate = typeof raw === "string" ? parseJsonValue(raw) : parseJsonValue(raw);
|
|
845
|
+
if (typeof candidate === "string" || typeof candidate === "number") {
|
|
846
|
+
const normalized = String(candidate).trim();
|
|
847
|
+
return normalized.length > 0 ? normalized : null;
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
async function markSettingsCorrupt(settingsPath, raw) {
|
|
852
|
+
try {
|
|
853
|
+
const suffix = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
854
|
+
const backupPath = `${settingsPath}.corrupt-${suffix}`;
|
|
855
|
+
await import_node_fs2.promises.writeFile(backupPath, raw, "utf8");
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function tryRestoreSettings(fromPath, _targetPath) {
|
|
860
|
+
try {
|
|
861
|
+
const raw = await import_node_fs2.promises.readFile(fromPath, "utf8");
|
|
862
|
+
const parsed = JSON.parse(raw);
|
|
863
|
+
if (!parsed || typeof parsed !== "object") {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
const normalized = normalizeParsedSettings(parsed);
|
|
867
|
+
await writeSettingsFile(normalized);
|
|
868
|
+
return normalized;
|
|
869
|
+
} catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function normalizeParsedSettings(parsed) {
|
|
874
|
+
const license = normalizeStoredLicense(parsed.license ?? parsed[KEY_LICENSE]);
|
|
875
|
+
const autoRoll = normalizeStoredAutoRoll(parsed.autoRoll ?? parsed[KEY_AUTO_ROLL]);
|
|
876
|
+
const lastProfileName = parseStoredProfileName(
|
|
877
|
+
parsed.lastProfileName ?? parsed[KEY_LAST_PROFILE_NAME]
|
|
878
|
+
);
|
|
879
|
+
const lastAppVersion = typeof parsed.lastAppVersion === "string" ? parsed.lastAppVersion : typeof parsed[KEY_LAST_APP_VERSION] === "string" ? parsed[KEY_LAST_APP_VERSION] : null;
|
|
880
|
+
const pendingUpdateVersion = typeof parsed.pendingUpdateVersion === "string" ? parsed.pendingUpdateVersion : typeof parsed[KEY_PENDING_UPDATE_VERSION] === "string" ? parsed[KEY_PENDING_UPDATE_VERSION] : null;
|
|
881
|
+
const cliChannel = parseCodexCliChannel(
|
|
882
|
+
parsed.cliChannel ?? parsed[KEY_CLI_CHANNEL]
|
|
883
|
+
);
|
|
884
|
+
return {
|
|
885
|
+
...license ? { license } : {},
|
|
886
|
+
...autoRoll ? { autoRoll } : {},
|
|
887
|
+
...lastProfileName ? { lastProfileName } : {},
|
|
888
|
+
...lastAppVersion ? { lastAppVersion } : {},
|
|
889
|
+
...pendingUpdateVersion ? { pendingUpdateVersion } : {},
|
|
890
|
+
...cliChannel ? { cliChannel } : {}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function readSettingsFromDisk() {
|
|
894
|
+
const settingsPath = resolveSettingsPath();
|
|
895
|
+
try {
|
|
896
|
+
const raw = await import_node_fs2.promises.readFile(settingsPath, "utf8");
|
|
897
|
+
const parsed = JSON.parse(raw);
|
|
898
|
+
if (!parsed || typeof parsed !== "object") {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
return normalizeParsedSettings(parsed);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (error.code === "ENOENT") {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
if (error instanceof SyntaxError) {
|
|
907
|
+
try {
|
|
908
|
+
const raw = await import_node_fs2.promises.readFile(settingsPath, "utf8");
|
|
909
|
+
await markSettingsCorrupt(settingsPath, raw);
|
|
910
|
+
const restoredFromTemp = await tryRestoreSettings(`${settingsPath}.tmp`, settingsPath);
|
|
911
|
+
if (restoredFromTemp) {
|
|
912
|
+
return restoredFromTemp;
|
|
913
|
+
}
|
|
914
|
+
const restoredFromBackup = await tryRestoreSettings(resolveBackupSettingsPath(), settingsPath);
|
|
915
|
+
if (restoredFromBackup) {
|
|
916
|
+
return restoredFromBackup;
|
|
917
|
+
}
|
|
918
|
+
await import_node_fs2.promises.writeFile(settingsPath, "{}\n", "utf8");
|
|
919
|
+
} catch {
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
logWarn("Failed to parse Codex settings file, using defaults:", error);
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function migrateSettingsFromDatabase() {
|
|
927
|
+
try {
|
|
928
|
+
const { __debugResolveDbPath: __debugResolveDbPath2, getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_sqlite_db(), sqlite_db_exports));
|
|
929
|
+
const dbPath = __debugResolveDbPath2();
|
|
930
|
+
try {
|
|
931
|
+
await import_node_fs2.promises.access(dbPath);
|
|
932
|
+
} catch {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
const db = await getDatabase2();
|
|
936
|
+
const rows = db.prepare(
|
|
937
|
+
`SELECT key, value
|
|
938
|
+
FROM app_kv
|
|
939
|
+
WHERE key IN ('${KEY_LAST_PROFILE_NAME}', '${KEY_LICENSE}', '${KEY_AUTO_ROLL}', '${KEY_LAST_APP_VERSION}', '${KEY_PENDING_UPDATE_VERSION}', '${KEY_CLI_CHANNEL}')`
|
|
940
|
+
).all();
|
|
941
|
+
if (!rows || rows.length === 0) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
const kv = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const row of rows) {
|
|
946
|
+
if (!row.key) continue;
|
|
947
|
+
if (typeof row.value === "string") {
|
|
948
|
+
kv.set(row.key, parseJsonValue(row.value));
|
|
949
|
+
} else {
|
|
950
|
+
kv.set(row.key, row.value);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const license = normalizeStoredLicense(kv.get(KEY_LICENSE));
|
|
954
|
+
const autoRoll = normalizeStoredAutoRoll(kv.get(KEY_AUTO_ROLL));
|
|
955
|
+
const lastProfileName = parseStoredProfileName(kv.get(KEY_LAST_PROFILE_NAME));
|
|
956
|
+
const lastAppVersion = typeof kv.get(KEY_LAST_APP_VERSION) === "string" ? kv.get(KEY_LAST_APP_VERSION) : null;
|
|
957
|
+
const pendingUpdateVersion = typeof kv.get(KEY_PENDING_UPDATE_VERSION) === "string" ? kv.get(KEY_PENDING_UPDATE_VERSION) : null;
|
|
958
|
+
const cliChannel = parseCodexCliChannel(kv.get(KEY_CLI_CHANNEL));
|
|
959
|
+
const settings = {
|
|
960
|
+
...license ? { license } : {},
|
|
961
|
+
...autoRoll ? { autoRoll } : {},
|
|
962
|
+
...lastProfileName ? { lastProfileName } : {},
|
|
963
|
+
...lastAppVersion ? { lastAppVersion } : {},
|
|
964
|
+
...pendingUpdateVersion ? { pendingUpdateVersion } : {},
|
|
965
|
+
...cliChannel ? { cliChannel } : {}
|
|
966
|
+
};
|
|
967
|
+
if (Object.keys(settings).length === 0) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
await writeSettingsFile(settings);
|
|
971
|
+
return settings;
|
|
972
|
+
} catch (error) {
|
|
973
|
+
logWarn("Failed to migrate Codex settings from database:", error);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function readSettingsFile() {
|
|
978
|
+
await ensureCodexDir();
|
|
979
|
+
const diskSettings = await readSettingsFromDisk();
|
|
980
|
+
if (diskSettings) {
|
|
981
|
+
return diskSettings;
|
|
982
|
+
}
|
|
983
|
+
const migrated = await migrateSettingsFromDatabase();
|
|
984
|
+
if (migrated) {
|
|
985
|
+
return migrated;
|
|
986
|
+
}
|
|
987
|
+
return {};
|
|
988
|
+
}
|
|
989
|
+
async function writeSettingsFile(settings) {
|
|
990
|
+
await ensureCodexDir();
|
|
991
|
+
const normalizedAutoRoll = settings.autoRoll ? normalizeAutoRollSettings(settings.autoRoll) : null;
|
|
992
|
+
const normalizedLicense = settings.license ? {
|
|
993
|
+
licenseKey: settings.license.licenseKey ?? null,
|
|
994
|
+
purchaseEmail: settings.license.purchaseEmail ?? null,
|
|
995
|
+
lastVerifiedAt: settings.license.lastVerifiedAt ?? null,
|
|
996
|
+
nextCheckAt: settings.license.nextCheckAt ?? null,
|
|
997
|
+
lastVerificationError: settings.license.lastVerificationError ?? null,
|
|
998
|
+
status: settings.license.status ?? void 0,
|
|
999
|
+
signature: settings.license.signature ?? null
|
|
1000
|
+
} : null;
|
|
1001
|
+
const payload = {
|
|
1002
|
+
...settings.lastProfileName ? { lastProfileName: settings.lastProfileName } : {},
|
|
1003
|
+
...normalizedLicense ? { license: normalizedLicense } : {},
|
|
1004
|
+
...normalizedAutoRoll ? { autoRoll: normalizedAutoRoll } : {},
|
|
1005
|
+
...settings.lastAppVersion ? { lastAppVersion: settings.lastAppVersion } : {},
|
|
1006
|
+
...settings.pendingUpdateVersion ? { pendingUpdateVersion: settings.pendingUpdateVersion } : {},
|
|
1007
|
+
...settings.cliChannel ? { cliChannel: settings.cliChannel } : {}
|
|
1008
|
+
};
|
|
1009
|
+
const serialized = JSON.stringify(payload, null, 2);
|
|
1010
|
+
const settingsPath = resolveSettingsPath();
|
|
1011
|
+
const backupPath = resolveBackupSettingsPath();
|
|
1012
|
+
const contents = `${serialized}
|
|
1013
|
+
`;
|
|
1014
|
+
await writeAtomic(settingsPath, contents);
|
|
1015
|
+
try {
|
|
1016
|
+
await writeAtomic(backupPath, contents);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
logWarn("Failed to write Codex settings backup:", error);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function parseJsonValue(value) {
|
|
1022
|
+
if (typeof value !== "string") {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
return JSON.parse(value);
|
|
1027
|
+
} catch {
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async function getCodexSettings() {
|
|
1032
|
+
return readSettingsFile();
|
|
1033
|
+
}
|
|
1034
|
+
async function setCodexSettings(settings) {
|
|
1035
|
+
await writeSettingsFile(settings);
|
|
1036
|
+
}
|
|
1037
|
+
async function getLastProfileName() {
|
|
1038
|
+
const settings = await getCodexSettings();
|
|
1039
|
+
const candidate = settings.lastProfileName;
|
|
1040
|
+
if (typeof candidate !== "string") {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
const trimmed = candidate.trim();
|
|
1044
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
1045
|
+
}
|
|
1046
|
+
async function persistLastProfileName(profileName) {
|
|
1047
|
+
const settings = await getCodexSettings();
|
|
1048
|
+
if (profileName && profileName.trim().length > 0) {
|
|
1049
|
+
settings.lastProfileName = profileName.trim();
|
|
1050
|
+
} else if (settings.lastProfileName) {
|
|
1051
|
+
delete settings.lastProfileName;
|
|
1052
|
+
}
|
|
1053
|
+
await setCodexSettings(settings);
|
|
1054
|
+
}
|
|
1055
|
+
async function getStoredLicense() {
|
|
1056
|
+
const settings = await getCodexSettings();
|
|
1057
|
+
if (!settings.license) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
return settings.license;
|
|
1061
|
+
}
|
|
1062
|
+
async function persistLicense(license) {
|
|
1063
|
+
const settings = await getCodexSettings();
|
|
1064
|
+
if (license) {
|
|
1065
|
+
settings.license = {
|
|
1066
|
+
licenseKey: license.licenseKey ?? null,
|
|
1067
|
+
purchaseEmail: license.purchaseEmail ?? null,
|
|
1068
|
+
lastVerifiedAt: license.lastVerifiedAt ?? null,
|
|
1069
|
+
nextCheckAt: license.nextCheckAt ?? null,
|
|
1070
|
+
lastVerificationError: license.lastVerificationError ?? null,
|
|
1071
|
+
status: license.status ?? "inactive",
|
|
1072
|
+
signature: license.signature ?? null
|
|
1073
|
+
};
|
|
1074
|
+
} else if (settings.license) {
|
|
1075
|
+
delete settings.license;
|
|
1076
|
+
}
|
|
1077
|
+
await setCodexSettings(settings);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// ../../lib/profile-manager.ts
|
|
1081
|
+
var TOKEN_EXPIRING_SOON_WINDOW_MS = 15 * 60 * 1e3;
|
|
1082
|
+
var REFRESH_TOKEN_REDEEMED_SNIPPET = "refresh token was already used";
|
|
1083
|
+
var REFRESH_TOKEN_REDEEMED_REASON = "Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.";
|
|
1084
|
+
var AUTH_BACKUP_MISSING_MARKER = "__codexuse_missing_auth__";
|
|
1085
|
+
var DEFAULT_WORKSPACE_ID = "__default__";
|
|
1086
|
+
var PROFILE_RECORD_FILENAME = "profile.json";
|
|
1087
|
+
var ProfileManager = class {
|
|
1088
|
+
constructor() {
|
|
1089
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
1090
|
+
this.codexDir = (0, import_path2.join)(homeDir, ".codex");
|
|
1091
|
+
this.profilesDir = (0, import_path2.join)(this.codexDir, "profiles");
|
|
1092
|
+
this.profileHomesRoot = (0, import_path2.join)(this.codexDir, "profile-homes");
|
|
1093
|
+
this.migrationsDir = (0, import_path2.join)(this.codexDir, "migrations");
|
|
1094
|
+
this.profileMigrationMarker = (0, import_path2.join)(this.migrationsDir, "profiles-v1");
|
|
1095
|
+
this.activeAuth = (0, import_path2.join)(this.codexDir, "auth.json");
|
|
1096
|
+
this.activeAuthBackup = `${this.activeAuth}.swap`;
|
|
1097
|
+
this.lastActiveAuthErrorSignature = null;
|
|
1098
|
+
this.authSwapLock = Promise.resolve();
|
|
1099
|
+
}
|
|
1100
|
+
computeExpiryIso(data) {
|
|
1101
|
+
if (!data) {
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
if (typeof data.expired === "string" && data.expired.trim()) {
|
|
1105
|
+
const parsed = Date.parse(data.expired);
|
|
1106
|
+
if (!Number.isNaN(parsed)) {
|
|
1107
|
+
return new Date(parsed).toISOString();
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const expiresIn = typeof data.expires_in === "number" && Number.isFinite(data.expires_in) ? data.expires_in : void 0;
|
|
1111
|
+
const issuedMs = typeof data.timestamp === "number" && Number.isFinite(data.timestamp) ? data.timestamp : void 0;
|
|
1112
|
+
const issuedAt = issuedMs ? issuedMs : void 0;
|
|
1113
|
+
const baseMs = issuedAt ?? Date.now();
|
|
1114
|
+
if (expiresIn && expiresIn > 0) {
|
|
1115
|
+
return new Date(baseMs + expiresIn * 1e3).toISOString();
|
|
1116
|
+
}
|
|
1117
|
+
return void 0;
|
|
1118
|
+
}
|
|
1119
|
+
normalizeProfileName(name) {
|
|
1120
|
+
if (typeof name !== "string") {
|
|
1121
|
+
throw new Error("Profile name is required");
|
|
1122
|
+
}
|
|
1123
|
+
const trimmed = name.trim();
|
|
1124
|
+
if (!trimmed) {
|
|
1125
|
+
throw new Error("Profile name must not be empty.");
|
|
1126
|
+
}
|
|
1127
|
+
if (trimmed === "." || trimmed === "..") {
|
|
1128
|
+
throw new Error(`Profile name '${name}' is not allowed.`);
|
|
1129
|
+
}
|
|
1130
|
+
if (/[\\/]/.test(trimmed)) {
|
|
1131
|
+
throw new Error("Profile name cannot contain path separators.");
|
|
1132
|
+
}
|
|
1133
|
+
if (trimmed.includes("\0")) {
|
|
1134
|
+
throw new Error("Profile name contains invalid characters.");
|
|
1135
|
+
}
|
|
1136
|
+
return trimmed;
|
|
1137
|
+
}
|
|
1138
|
+
isNotFoundError(error) {
|
|
1139
|
+
return Boolean(
|
|
1140
|
+
error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
async readPreferredProfileName() {
|
|
1144
|
+
try {
|
|
1145
|
+
return await getLastProfileName();
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
logWarn("Failed to read preferred profile name:", error);
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async persistPreferredProfileName(name) {
|
|
1152
|
+
try {
|
|
1153
|
+
await persistLastProfileName(name);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
logWarn("Failed to persist preferred profile name:", error);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
getProfileRecordPath(profileName) {
|
|
1159
|
+
return (0, import_path2.join)(this.getProfileHomePath(profileName), PROFILE_RECORD_FILENAME);
|
|
1160
|
+
}
|
|
1161
|
+
parseProfileRecordPayload(profileName, parsed) {
|
|
1162
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
const dataRaw = parsed["data"];
|
|
1166
|
+
let data = null;
|
|
1167
|
+
if (dataRaw && typeof dataRaw === "object") {
|
|
1168
|
+
data = dataRaw;
|
|
1169
|
+
} else if (typeof dataRaw === "string") {
|
|
1170
|
+
try {
|
|
1171
|
+
data = JSON.parse(dataRaw);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
logWarn(`Failed to parse data payload for profile '${profileName}':`, error);
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
const createdAt = typeof parsed["createdAt"] === "string" ? parsed["createdAt"] : typeof parsed["created_at"] === "string" ? parsed["created_at"] : null;
|
|
1180
|
+
const updatedAt = typeof parsed["updatedAt"] === "string" ? parsed["updatedAt"] : typeof parsed["updated_at"] === "string" ? parsed["updated_at"] : null;
|
|
1181
|
+
const metadata = parsed["metadata"] && typeof parsed["metadata"] === "object" ? parsed["metadata"] : void 0;
|
|
1182
|
+
const displayName = typeof parsed["displayName"] === "string" ? parsed["displayName"] : typeof parsed["display_name"] === "string" ? parsed["display_name"] : null;
|
|
1183
|
+
return {
|
|
1184
|
+
name: profileName,
|
|
1185
|
+
displayName,
|
|
1186
|
+
data,
|
|
1187
|
+
metadata,
|
|
1188
|
+
accountId: typeof parsed["accountId"] === "string" ? parsed["accountId"] : typeof parsed["account_id"] === "string" ? parsed["account_id"] : null,
|
|
1189
|
+
workspaceId: typeof parsed["workspaceId"] === "string" ? parsed["workspaceId"] : typeof parsed["workspace_id"] === "string" ? parsed["workspace_id"] : null,
|
|
1190
|
+
workspaceName: typeof parsed["workspaceName"] === "string" ? parsed["workspaceName"] : typeof parsed["workspace_name"] === "string" ? parsed["workspace_name"] : null,
|
|
1191
|
+
email: typeof parsed["email"] === "string" ? parsed["email"] : typeof parsed["email"] === "boolean" ? null : null,
|
|
1192
|
+
authMethod: typeof parsed["authMethod"] === "string" ? parsed["authMethod"] : typeof parsed["auth_method"] === "string" ? parsed["auth_method"] : null,
|
|
1193
|
+
createdAt,
|
|
1194
|
+
updatedAt
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
decodeProfileRecord(profileName, raw, sourceLabel) {
|
|
1198
|
+
try {
|
|
1199
|
+
const parsed = JSON.parse(raw);
|
|
1200
|
+
return this.parseProfileRecordPayload(profileName, parsed);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
logWarn(`Failed to parse profile record '${profileName}' from '${sourceLabel}':`, error);
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async recoverProfileRecord(recordPath, profileName) {
|
|
1207
|
+
const candidates = [`${recordPath}.bak`, `${recordPath}.tmp`];
|
|
1208
|
+
for (const candidate of candidates) {
|
|
1209
|
+
let raw;
|
|
1210
|
+
try {
|
|
1211
|
+
raw = await import_fs.promises.readFile(candidate, "utf8");
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (!this.isNotFoundError(error)) {
|
|
1214
|
+
logWarn(`Failed to read backup for '${profileName}' from '${candidate}':`, error);
|
|
1215
|
+
}
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const decoded = this.decodeProfileRecord(profileName, raw, candidate);
|
|
1219
|
+
if (!decoded) {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
await this.writeProfileRecord(decoded);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
logWarn(`Failed to restore profile '${profileName}' from '${candidate}':`, error);
|
|
1226
|
+
}
|
|
1227
|
+
return decoded;
|
|
1228
|
+
}
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
async readProfileRecord(profileName) {
|
|
1232
|
+
const recordPath = this.getProfileRecordPath(profileName);
|
|
1233
|
+
try {
|
|
1234
|
+
const raw = await import_fs.promises.readFile(recordPath, "utf8");
|
|
1235
|
+
const decoded = this.decodeProfileRecord(profileName, raw, recordPath);
|
|
1236
|
+
if (decoded) {
|
|
1237
|
+
return decoded;
|
|
1238
|
+
}
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
if (!this.isNotFoundError(error)) {
|
|
1241
|
+
logWarn(`Failed to read profile record '${profileName}':`, error);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return this.recoverProfileRecord(recordPath, profileName);
|
|
1245
|
+
}
|
|
1246
|
+
async listProfileRecords() {
|
|
1247
|
+
const records = [];
|
|
1248
|
+
let entries = [];
|
|
1249
|
+
try {
|
|
1250
|
+
entries = await import_fs.promises.readdir(this.profileHomesRoot);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
if (!this.isNotFoundError(error)) {
|
|
1253
|
+
logWarn("Failed to list profile homes:", error);
|
|
1254
|
+
}
|
|
1255
|
+
return records;
|
|
1256
|
+
}
|
|
1257
|
+
for (const entry of entries) {
|
|
1258
|
+
const homePath = (0, import_path2.join)(this.profileHomesRoot, entry);
|
|
1259
|
+
try {
|
|
1260
|
+
const stat = await import_fs.promises.stat(homePath);
|
|
1261
|
+
if (!stat.isDirectory()) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
if (!this.isNotFoundError(error)) {
|
|
1266
|
+
logWarn(`Failed to inspect profile home '${entry}':`, error);
|
|
1267
|
+
}
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
const normalizedName = this.normalizeProfileName(entry);
|
|
1272
|
+
const record = await this.readProfileRecord(normalizedName);
|
|
1273
|
+
if (record) {
|
|
1274
|
+
records.push(record);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const authPath = (0, import_path2.join)(homePath, "auth.json");
|
|
1278
|
+
try {
|
|
1279
|
+
const authRaw = await import_fs.promises.readFile(authPath, "utf8");
|
|
1280
|
+
const authData = JSON.parse(authRaw);
|
|
1281
|
+
const normalized = this.normalizeProfileData(authData);
|
|
1282
|
+
const metadata = this.extractProfileMetadata(normalized);
|
|
1283
|
+
await this.persistProfileRecord(normalizedName, normalized, metadata);
|
|
1284
|
+
const restored = await this.readProfileRecord(normalizedName);
|
|
1285
|
+
if (restored) {
|
|
1286
|
+
records.push(restored);
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
if (!this.isNotFoundError(error)) {
|
|
1290
|
+
logWarn(`Failed to restore profile record for '${normalizedName}' from auth.json:`, error);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
logWarn(`Skipping invalid profile home '${entry}':`, error);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return records;
|
|
1298
|
+
}
|
|
1299
|
+
async snapshotExistingProfileRecord(recordPath) {
|
|
1300
|
+
try {
|
|
1301
|
+
const existing = await import_fs.promises.readFile(recordPath, "utf8");
|
|
1302
|
+
if (existing && existing.trim()) {
|
|
1303
|
+
await this.writeAtomic(`${recordPath}.bak`, existing);
|
|
1304
|
+
}
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
if (!this.isNotFoundError(error)) {
|
|
1307
|
+
logWarn(`Failed to snapshot existing profile record '${recordPath}':`, error);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
async writeProfileRecord(record) {
|
|
1312
|
+
const recordPath = this.getProfileRecordPath(record.name);
|
|
1313
|
+
const payload = {
|
|
1314
|
+
name: record.name,
|
|
1315
|
+
display_name: record.displayName,
|
|
1316
|
+
data: record.data,
|
|
1317
|
+
metadata: record.metadata,
|
|
1318
|
+
account_id: record.accountId,
|
|
1319
|
+
workspace_id: record.workspaceId,
|
|
1320
|
+
workspace_name: record.workspaceName,
|
|
1321
|
+
email: record.email,
|
|
1322
|
+
auth_method: record.authMethod,
|
|
1323
|
+
created_at: record.createdAt,
|
|
1324
|
+
updated_at: record.updatedAt
|
|
1325
|
+
};
|
|
1326
|
+
const serialized = `${JSON.stringify(payload, null, 2)}
|
|
1327
|
+
`;
|
|
1328
|
+
await this.snapshotExistingProfileRecord(recordPath);
|
|
1329
|
+
await this.writeAtomic(recordPath, serialized);
|
|
1330
|
+
try {
|
|
1331
|
+
await this.writeAtomic(`${recordPath}.bak`, serialized);
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
logWarn(`Failed to persist profile backup for '${record.name}':`, error);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
resolveAuthMethod(data) {
|
|
1337
|
+
const raw = typeof data?.auth_method === "string" ? data.auth_method.trim().toLowerCase() : "";
|
|
1338
|
+
return raw || "codex-cli";
|
|
1339
|
+
}
|
|
1340
|
+
async readActiveAuthFile() {
|
|
1341
|
+
try {
|
|
1342
|
+
const raw = await import_fs.promises.readFile(this.activeAuth, "utf8");
|
|
1343
|
+
const trimmed = raw.trim();
|
|
1344
|
+
if (!trimmed) {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
return JSON.parse(trimmed);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
if (this.isNotFoundError(error)) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
1353
|
+
const signature = typeof message === "string" ? `${message}:${this.activeAuth}` : this.activeAuth;
|
|
1354
|
+
if (this.lastActiveAuthErrorSignature !== signature) {
|
|
1355
|
+
logWarn("Failed to read active auth file:", error);
|
|
1356
|
+
this.lastActiveAuthErrorSignature = signature;
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async getActiveAuthAccountId() {
|
|
1362
|
+
const activeAuth = await this.readActiveAuthFile();
|
|
1363
|
+
if (!activeAuth) {
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
return this.getAccountIdFromData(activeAuth);
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Decode a JWT payload into an object without validating the signature.
|
|
1370
|
+
*/
|
|
1371
|
+
decodeJwtPayload(token) {
|
|
1372
|
+
if (!token || typeof token !== "string") {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
const segments = token.split(".");
|
|
1376
|
+
if (segments.length < 2) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const payload = Buffer.from(segments[1], "base64url").toString("utf8");
|
|
1381
|
+
return JSON.parse(payload);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
toIsoStringFromSeconds(seconds) {
|
|
1387
|
+
if (typeof seconds !== "number" || Number.isNaN(seconds)) {
|
|
1388
|
+
return void 0;
|
|
1389
|
+
}
|
|
1390
|
+
try {
|
|
1391
|
+
return new Date(seconds * 1e3).toISOString();
|
|
1392
|
+
} catch {
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
normalizeEmailCandidate(value) {
|
|
1397
|
+
if (typeof value !== "string") {
|
|
1398
|
+
return void 0;
|
|
1399
|
+
}
|
|
1400
|
+
const trimmed = value.trim();
|
|
1401
|
+
if (!trimmed || trimmed.includes(" ")) {
|
|
1402
|
+
return void 0;
|
|
1403
|
+
}
|
|
1404
|
+
const atIndex = trimmed.indexOf("@");
|
|
1405
|
+
if (atIndex <= 0 || atIndex === trimmed.length - 1) {
|
|
1406
|
+
return void 0;
|
|
1407
|
+
}
|
|
1408
|
+
const domain = trimmed.slice(atIndex + 1);
|
|
1409
|
+
if (!domain || !domain.includes(".")) {
|
|
1410
|
+
return void 0;
|
|
1411
|
+
}
|
|
1412
|
+
return trimmed;
|
|
1413
|
+
}
|
|
1414
|
+
pickFirstEmail(candidates) {
|
|
1415
|
+
for (const candidate of candidates) {
|
|
1416
|
+
const normalized = this.normalizeEmailCandidate(candidate);
|
|
1417
|
+
if (normalized) {
|
|
1418
|
+
return normalized;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return void 0;
|
|
1422
|
+
}
|
|
1423
|
+
findEmailInObject(value, seen = /* @__PURE__ */ new Set()) {
|
|
1424
|
+
if (!value || typeof value !== "object") {
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
if (seen.has(value)) {
|
|
1428
|
+
return void 0;
|
|
1429
|
+
}
|
|
1430
|
+
seen.add(value);
|
|
1431
|
+
if (Array.isArray(value)) {
|
|
1432
|
+
for (const entry of value) {
|
|
1433
|
+
const nested = this.findEmailInObject(entry, seen);
|
|
1434
|
+
if (nested) {
|
|
1435
|
+
return nested;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1441
|
+
if (typeof entry === "string") {
|
|
1442
|
+
const normalized = this.normalizeEmailCandidate(entry);
|
|
1443
|
+
const lowerKey = key.toLowerCase();
|
|
1444
|
+
const keyHintsAtEmail = lowerKey.includes("email") || lowerKey.includes("contact") || lowerKey.includes("username") || lowerKey.includes("login");
|
|
1445
|
+
if (normalized && (keyHintsAtEmail || entry.includes("@"))) {
|
|
1446
|
+
return normalized;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (entry && typeof entry === "object") {
|
|
1450
|
+
const nested = this.findEmailInObject(entry, seen);
|
|
1451
|
+
if (nested) {
|
|
1452
|
+
return nested;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return void 0;
|
|
1457
|
+
}
|
|
1458
|
+
resolveProfileEmail(data, metadata) {
|
|
1459
|
+
const direct = this.normalizeEmailCandidate(data.email);
|
|
1460
|
+
if (direct) {
|
|
1461
|
+
return direct;
|
|
1462
|
+
}
|
|
1463
|
+
if (metadata?.email) {
|
|
1464
|
+
return metadata.email;
|
|
1465
|
+
}
|
|
1466
|
+
return this.findEmailInObject(data);
|
|
1467
|
+
}
|
|
1468
|
+
evaluateTokenStatus(data, metadata) {
|
|
1469
|
+
const expiresAt = metadata?.tokenExpiresAt ?? this.computeExpiryIso(data);
|
|
1470
|
+
const parsedExpiry = expiresAt ? Date.parse(expiresAt) : Number.NaN;
|
|
1471
|
+
const hasExpiry = !Number.isNaN(parsedExpiry);
|
|
1472
|
+
const tokenAlert = data.tokenAlert;
|
|
1473
|
+
if (!data || typeof data !== "object" || Object.keys(data).length === 0) {
|
|
1474
|
+
return {
|
|
1475
|
+
state: "missing",
|
|
1476
|
+
reason: "No authentication data saved for this profile.",
|
|
1477
|
+
expiresAt,
|
|
1478
|
+
issue: "auth-missing",
|
|
1479
|
+
requiresUserAction: true
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
if (tokenAlert?.issue) {
|
|
1483
|
+
const reason = typeof tokenAlert.reason === "string" && tokenAlert.reason.trim().length > 0 ? tokenAlert.reason.trim() : tokenAlert.issue === "refresh-redeemed" ? REFRESH_TOKEN_REDEEMED_REASON : "Authentication needs attention. Re-login this profile.";
|
|
1484
|
+
const issue = tokenAlert.issue ?? "auth-missing";
|
|
1485
|
+
return {
|
|
1486
|
+
state: "invalid",
|
|
1487
|
+
reason,
|
|
1488
|
+
expiresAt,
|
|
1489
|
+
issue,
|
|
1490
|
+
requiresUserAction: true
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
if (!data.access_token) {
|
|
1494
|
+
return {
|
|
1495
|
+
state: "missing",
|
|
1496
|
+
reason: "Access token is missing. Re-authenticate with Codex CLI.",
|
|
1497
|
+
expiresAt,
|
|
1498
|
+
issue: "access-missing",
|
|
1499
|
+
requiresUserAction: true
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.decodeJwtPayload(data.access_token)) {
|
|
1503
|
+
return {
|
|
1504
|
+
state: "invalid",
|
|
1505
|
+
reason: "Access token is corrupted or not a valid JWT.",
|
|
1506
|
+
expiresAt,
|
|
1507
|
+
issue: "access-invalid",
|
|
1508
|
+
requiresUserAction: true
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
const refreshToken = typeof data.refresh_token === "string" ? data.refresh_token.trim() : "";
|
|
1512
|
+
if (!refreshToken) {
|
|
1513
|
+
return {
|
|
1514
|
+
state: "missing",
|
|
1515
|
+
reason: "Refresh token is missing. Run Codex login again.",
|
|
1516
|
+
expiresAt,
|
|
1517
|
+
issue: "refresh-missing",
|
|
1518
|
+
requiresUserAction: true
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
if (hasExpiry) {
|
|
1522
|
+
const diff = parsedExpiry - Date.now();
|
|
1523
|
+
if (diff <= 0) {
|
|
1524
|
+
return {
|
|
1525
|
+
state: "expiring",
|
|
1526
|
+
reason: "Access token expired.",
|
|
1527
|
+
expiresAt,
|
|
1528
|
+
accessTokenExpired: true,
|
|
1529
|
+
issue: "access-expired",
|
|
1530
|
+
requiresUserAction: false
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
if (diff <= TOKEN_EXPIRING_SOON_WINDOW_MS) {
|
|
1534
|
+
return {
|
|
1535
|
+
state: "expiring",
|
|
1536
|
+
reason: "Access token expires soon.",
|
|
1537
|
+
expiresAt,
|
|
1538
|
+
issue: "access-expiring",
|
|
1539
|
+
requiresUserAction: false
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
state: "ok",
|
|
1545
|
+
expiresAt,
|
|
1546
|
+
requiresUserAction: false
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
extractProfileMetadata(data) {
|
|
1550
|
+
const idPayload = this.decodeJwtPayload(data.id_token);
|
|
1551
|
+
const accessPayload = this.decodeJwtPayload(data.access_token);
|
|
1552
|
+
if (!idPayload && !accessPayload) {
|
|
1553
|
+
return void 0;
|
|
1554
|
+
}
|
|
1555
|
+
const authInfo = idPayload?.["https://api.openai.com/auth"] ?? accessPayload?.["https://api.openai.com/auth"];
|
|
1556
|
+
const profileInfo = accessPayload?.["https://api.openai.com/profile"];
|
|
1557
|
+
const getNumber = (obj, key) => typeof obj?.[key] === "number" ? obj[key] : void 0;
|
|
1558
|
+
const getString = (obj, key) => typeof obj?.[key] === "string" ? obj[key] : void 0;
|
|
1559
|
+
const exp = getNumber(idPayload, "exp") ?? getNumber(accessPayload, "exp");
|
|
1560
|
+
const iat = getNumber(idPayload, "iat") ?? getNumber(accessPayload, "iat");
|
|
1561
|
+
const authTime = getNumber(idPayload, "auth_time") ?? getNumber(accessPayload, "auth_time");
|
|
1562
|
+
const organizationsRaw = Array.isArray(authInfo?.["organizations"]) ? authInfo?.["organizations"] : void 0;
|
|
1563
|
+
const organizations = organizationsRaw ? organizationsRaw.map((org) => {
|
|
1564
|
+
const record = org;
|
|
1565
|
+
return {
|
|
1566
|
+
id: getString(record, "id"),
|
|
1567
|
+
title: getString(record, "title"),
|
|
1568
|
+
role: getString(record, "role"),
|
|
1569
|
+
isDefault: typeof record?.["is_default"] === "boolean" ? record?.["is_default"] : void 0
|
|
1570
|
+
};
|
|
1571
|
+
}).filter(
|
|
1572
|
+
(org) => org.id || org.title || org.role || typeof org.isDefault === "boolean"
|
|
1573
|
+
) : void 0;
|
|
1574
|
+
const groups = Array.isArray(authInfo?.["groups"]) ? (authInfo?.["groups"]).filter((group) => typeof group === "string") : void 0;
|
|
1575
|
+
const emailVerified = typeof profileInfo?.["email_verified"] === "boolean" ? profileInfo?.["email_verified"] : typeof idPayload?.["email_verified"] === "boolean" ? idPayload?.["email_verified"] : typeof accessPayload?.["email_verified"] === "boolean" ? accessPayload?.["email_verified"] : void 0;
|
|
1576
|
+
const email = this.pickFirstEmail([
|
|
1577
|
+
getString(profileInfo, "email"),
|
|
1578
|
+
getString(profileInfo, "email_address"),
|
|
1579
|
+
getString(profileInfo, "primary_email"),
|
|
1580
|
+
getString(profileInfo, "default_email"),
|
|
1581
|
+
getString(profileInfo, "contact_email")
|
|
1582
|
+
]) ?? this.findEmailInObject(profileInfo) ?? this.pickFirstEmail([
|
|
1583
|
+
getString(authInfo, "user_email"),
|
|
1584
|
+
getString(authInfo, "email"),
|
|
1585
|
+
getString(authInfo, "userEmail"),
|
|
1586
|
+
getString(authInfo, "chatgpt_user_email")
|
|
1587
|
+
]) ?? this.findEmailInObject(authInfo) ?? this.pickFirstEmail([
|
|
1588
|
+
getString(idPayload, "email"),
|
|
1589
|
+
getString(idPayload, "preferred_username"),
|
|
1590
|
+
getString(idPayload, "username")
|
|
1591
|
+
]) ?? this.findEmailInObject(idPayload) ?? this.pickFirstEmail([
|
|
1592
|
+
getString(accessPayload, "email")
|
|
1593
|
+
]) ?? this.findEmailInObject(accessPayload);
|
|
1594
|
+
const subscription = authInfo ? {
|
|
1595
|
+
activeStart: getString(authInfo, "chatgpt_subscription_active_start"),
|
|
1596
|
+
activeUntil: getString(authInfo, "chatgpt_subscription_active_until"),
|
|
1597
|
+
lastChecked: getString(authInfo, "chatgpt_subscription_last_checked")
|
|
1598
|
+
} : void 0;
|
|
1599
|
+
const planType = getString(authInfo, "chatgpt_plan_type");
|
|
1600
|
+
const chatgptUserId = getString(authInfo, "chatgpt_user_id");
|
|
1601
|
+
const chatgptAccountUserId = getString(authInfo, "chatgpt_account_user_id");
|
|
1602
|
+
const userId = getString(authInfo, "user_id") ?? chatgptAccountUserId ?? getString(idPayload ?? void 0, "sub") ?? getString(accessPayload ?? void 0, "sub");
|
|
1603
|
+
const metadata = {
|
|
1604
|
+
email,
|
|
1605
|
+
planType,
|
|
1606
|
+
subscription,
|
|
1607
|
+
organizations,
|
|
1608
|
+
groups,
|
|
1609
|
+
userId,
|
|
1610
|
+
chatgptUserId,
|
|
1611
|
+
emailVerified,
|
|
1612
|
+
tokenExpiresAt: this.toIsoStringFromSeconds(exp),
|
|
1613
|
+
tokenIssuedAt: this.toIsoStringFromSeconds(iat),
|
|
1614
|
+
tokenAuthTime: this.toIsoStringFromSeconds(authTime)
|
|
1615
|
+
};
|
|
1616
|
+
const hasMeaningfulData = Boolean(metadata.planType) || Boolean(
|
|
1617
|
+
metadata.subscription && (metadata.subscription.activeStart || metadata.subscription.activeUntil || metadata.subscription.lastChecked)
|
|
1618
|
+
) || Boolean(metadata.organizations && metadata.organizations.length > 0) || Boolean(metadata.groups && metadata.groups.length > 0) || Boolean(metadata.userId) || Boolean(metadata.chatgptUserId) || Boolean(metadata.email) || typeof metadata.emailVerified === "boolean" || Boolean(metadata.tokenExpiresAt) || Boolean(metadata.tokenIssuedAt) || Boolean(metadata.tokenAuthTime);
|
|
1619
|
+
return hasMeaningfulData ? metadata : void 0;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Initialize the profile manager and create necessary directories
|
|
1623
|
+
*/
|
|
1624
|
+
async initialize() {
|
|
1625
|
+
try {
|
|
1626
|
+
await import_fs.promises.mkdir(this.codexDir, { recursive: true });
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
logError("Failed to ensure Codex directory exists:", error);
|
|
1629
|
+
throw new Error("Failed to initialize profile manager");
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
await import_fs.promises.mkdir(this.profileHomesRoot, { recursive: true });
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
logError("Failed to ensure profile homes directory exists:", error);
|
|
1635
|
+
throw new Error("Failed to initialize profile manager");
|
|
1636
|
+
}
|
|
1637
|
+
await this.recoverActiveAuthBackup();
|
|
1638
|
+
const migrationsComplete = await this.hasCompletedMigrations();
|
|
1639
|
+
if (!migrationsComplete) {
|
|
1640
|
+
await this.migrateLegacyProfiles();
|
|
1641
|
+
await this.migrateProfilesFromDatabase();
|
|
1642
|
+
await this.removeLegacyArtifacts();
|
|
1643
|
+
await this.markMigrationsComplete();
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Remove deprecated files that previously tracked the current profile.
|
|
1648
|
+
* These are now redundant since we infer the active profile from auth.json.
|
|
1649
|
+
*/
|
|
1650
|
+
async removeLegacyArtifacts() {
|
|
1651
|
+
const legacyTargets = [
|
|
1652
|
+
(0, import_path2.join)(this.codexDir, "current-profile.json"),
|
|
1653
|
+
(0, import_path2.join)(this.profilesDir, ".current"),
|
|
1654
|
+
`${this.activeAuth}.backup`,
|
|
1655
|
+
(0, import_path2.join)(this.codexDir, "cache", "rate-limits")
|
|
1656
|
+
];
|
|
1657
|
+
for (const target of legacyTargets) {
|
|
1658
|
+
try {
|
|
1659
|
+
await import_fs.promises.rm(target, { recursive: true, force: true });
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
if (!this.isNotFoundError(error)) {
|
|
1662
|
+
logWarn(`Failed to remove legacy artifact '${target}':`, error);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
await import_fs.promises.rm(this.profilesDir, { recursive: true, force: true });
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
if (!this.isNotFoundError(error)) {
|
|
1670
|
+
logWarn(`Failed to remove legacy profiles directory '${this.profilesDir}':`, error);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
async hasCompletedMigrations() {
|
|
1675
|
+
try {
|
|
1676
|
+
await import_fs.promises.access(this.profileMigrationMarker);
|
|
1677
|
+
return true;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (!this.isNotFoundError(error)) {
|
|
1680
|
+
logWarn("Failed to read profile migration marker:", error);
|
|
1681
|
+
}
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async markMigrationsComplete() {
|
|
1686
|
+
try {
|
|
1687
|
+
await import_fs.promises.mkdir(this.migrationsDir, { recursive: true });
|
|
1688
|
+
await import_fs.promises.writeFile(this.profileMigrationMarker, "ok");
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
logWarn("Failed to persist profile migration marker:", error);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
async migrateLegacyProfiles() {
|
|
1694
|
+
let files;
|
|
1695
|
+
try {
|
|
1696
|
+
files = await import_fs.promises.readdir(this.profilesDir);
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
if (!this.isNotFoundError(error)) {
|
|
1699
|
+
logWarn("Failed to inspect legacy profiles directory:", error);
|
|
1700
|
+
}
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (files.length === 0) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const accountCandidates = /* @__PURE__ */ new Map();
|
|
1707
|
+
const orphanCandidates = [];
|
|
1708
|
+
const insertedNames = /* @__PURE__ */ new Set();
|
|
1709
|
+
for (const file of files) {
|
|
1710
|
+
if (!file.endsWith(".json")) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
const name = file.replace(/\.json$/, "");
|
|
1714
|
+
const filePath = (0, import_path2.join)(this.profilesDir, file);
|
|
1715
|
+
try {
|
|
1716
|
+
const raw = await import_fs.promises.readFile(filePath, "utf8");
|
|
1717
|
+
const data = JSON.parse(raw);
|
|
1718
|
+
const normalizedName = this.normalizeProfileName(name);
|
|
1719
|
+
const metadata = this.extractProfileMetadata(data);
|
|
1720
|
+
const resolvedEmail = this.resolveProfileEmail(data, metadata);
|
|
1721
|
+
if (resolvedEmail) {
|
|
1722
|
+
data.email = resolvedEmail;
|
|
1723
|
+
}
|
|
1724
|
+
const accountId = this.getAccountIdFromData(data);
|
|
1725
|
+
const candidate = {
|
|
1726
|
+
name: normalizedName,
|
|
1727
|
+
data,
|
|
1728
|
+
profile: this.buildProfileFromData(normalizedName, data),
|
|
1729
|
+
accountId
|
|
1730
|
+
};
|
|
1731
|
+
if (accountId) {
|
|
1732
|
+
const existing = accountCandidates.get(accountId);
|
|
1733
|
+
if (existing) {
|
|
1734
|
+
const preferred = this.selectPreferredProfile([existing.profile, candidate.profile]);
|
|
1735
|
+
if (preferred === candidate.profile) {
|
|
1736
|
+
logWarn(
|
|
1737
|
+
`Replacing legacy profile '${existing.name}' with '${normalizedName}' for account '${accountId}'.`
|
|
1738
|
+
);
|
|
1739
|
+
accountCandidates.set(accountId, candidate);
|
|
1740
|
+
} else {
|
|
1741
|
+
logWarn(
|
|
1742
|
+
`Skipping legacy profile '${name}' because account '${accountId}' already exists as '${existing.name}'.`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
accountCandidates.set(accountId, candidate);
|
|
1747
|
+
}
|
|
1748
|
+
} else {
|
|
1749
|
+
orphanCandidates.push(candidate);
|
|
1750
|
+
}
|
|
1751
|
+
await import_fs.promises.rm(filePath, { force: true });
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
logWarn(`Failed to migrate legacy profile '${name}':`, error);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const persistQueue = [...accountCandidates.values(), ...orphanCandidates];
|
|
1757
|
+
const existingRecords = await this.listProfileRecords();
|
|
1758
|
+
for (const record of existingRecords) {
|
|
1759
|
+
insertedNames.add(record.name);
|
|
1760
|
+
}
|
|
1761
|
+
for (const candidate of persistQueue) {
|
|
1762
|
+
if (insertedNames.has(candidate.name)) {
|
|
1763
|
+
logWarn(`Skipping legacy profile '${candidate.name}' because it was already imported.`);
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
try {
|
|
1767
|
+
await this.persistProfileRecord(candidate.name, candidate.data, candidate.profile.metadata);
|
|
1768
|
+
insertedNames.add(candidate.name);
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
logWarn(`Failed to persist migrated profile '${candidate.name}':`, error);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async migrateProfilesFromDatabase() {
|
|
1775
|
+
let rows = [];
|
|
1776
|
+
try {
|
|
1777
|
+
const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_sqlite_db(), sqlite_db_exports));
|
|
1778
|
+
const db = await getDatabase2();
|
|
1779
|
+
rows = db.prepare(
|
|
1780
|
+
"SELECT name, data, account_id, workspace_id, workspace_name, email, auth_method, created_at, updated_at FROM profiles"
|
|
1781
|
+
).all();
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
logWarn("Failed to read existing database profiles for migration:", error);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (!rows || rows.length === 0) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const existingNames = new Set((await this.listProfileRecords()).map((record) => record.name));
|
|
1790
|
+
for (const row of rows) {
|
|
1791
|
+
const name = typeof row?.name === "string" ? row.name : null;
|
|
1792
|
+
const serialized = typeof row?.data === "string" ? row.data : null;
|
|
1793
|
+
if (!name || !serialized) {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
let data;
|
|
1797
|
+
try {
|
|
1798
|
+
data = JSON.parse(serialized);
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
logWarn(`Failed to parse database profile '${name}' during migration:`, error);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
try {
|
|
1804
|
+
const normalizedName = this.normalizeProfileName(name);
|
|
1805
|
+
if (existingNames.has(normalizedName)) {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
data.created_at = data.created_at ?? (typeof row?.created_at === "string" ? row.created_at : void 0);
|
|
1809
|
+
data.email = data.email ?? (typeof row?.email === "string" ? row.email : void 0);
|
|
1810
|
+
data.auth_method = data.auth_method ?? (typeof row?.auth_method === "string" ? row.auth_method : void 0);
|
|
1811
|
+
data.workspace_id = data.workspace_id ?? (typeof row?.workspace_id === "string" ? row.workspace_id : void 0);
|
|
1812
|
+
data.workspace_name = data.workspace_name ?? (typeof row?.workspace_name === "string" ? row.workspace_name : void 0);
|
|
1813
|
+
data.updated_at = data.updated_at ?? (typeof row?.updated_at === "string" ? row.updated_at : void 0);
|
|
1814
|
+
const metadata = this.extractProfileMetadata(data);
|
|
1815
|
+
await this.persistProfileRecord(normalizedName, data, metadata);
|
|
1816
|
+
existingNames.add(normalizedName);
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
logWarn(`Failed to migrate database profile '${name}':`, error);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
enqueueAuthSwap(task) {
|
|
1823
|
+
const run = this.authSwapLock.then(task, task);
|
|
1824
|
+
this.authSwapLock = run.then(
|
|
1825
|
+
() => void 0,
|
|
1826
|
+
() => void 0
|
|
1827
|
+
);
|
|
1828
|
+
return run;
|
|
1829
|
+
}
|
|
1830
|
+
async recoverActiveAuthBackup() {
|
|
1831
|
+
let backup;
|
|
1832
|
+
try {
|
|
1833
|
+
backup = await import_fs.promises.readFile(this.activeAuthBackup, "utf8");
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
if (!this.isNotFoundError(error)) {
|
|
1836
|
+
logWarn("Failed to inspect active auth backup:", error);
|
|
1837
|
+
}
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
try {
|
|
1841
|
+
if (backup === AUTH_BACKUP_MISSING_MARKER) {
|
|
1842
|
+
await import_fs.promises.rm(this.activeAuth, { force: true });
|
|
1843
|
+
} else {
|
|
1844
|
+
try {
|
|
1845
|
+
await this.writeAtomic(this.activeAuth, backup);
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
logWarn("Failed to restore active auth from backup, falling back to direct write:", error);
|
|
1848
|
+
try {
|
|
1849
|
+
await import_fs.promises.writeFile(this.activeAuth, backup, "utf8");
|
|
1850
|
+
} catch (fallbackError) {
|
|
1851
|
+
logWarn("Direct write failed while restoring active auth backup:", fallbackError);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
logWarn("Failed to restore active auth from backup:", error);
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
await import_fs.promises.rm(this.activeAuthBackup, { force: true });
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
if (!this.isNotFoundError(error)) {
|
|
1862
|
+
logWarn("Failed to remove active auth backup:", error);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async writeAtomic(filePath, contents) {
|
|
1867
|
+
const tempPath = `${filePath}.tmp`;
|
|
1868
|
+
const dir = import_path.default.dirname(filePath);
|
|
1869
|
+
await import_fs.promises.mkdir(dir, { recursive: true });
|
|
1870
|
+
await import_fs.promises.writeFile(tempPath, contents, "utf8");
|
|
1871
|
+
try {
|
|
1872
|
+
await import_fs.promises.rename(tempPath, filePath);
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
try {
|
|
1875
|
+
await import_fs.promises.writeFile(filePath, contents, "utf8");
|
|
1876
|
+
} catch (writeError) {
|
|
1877
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
1878
|
+
const fallback = writeError instanceof Error ? writeError.message : String(writeError);
|
|
1879
|
+
throw new Error(`Atomic write failed (rename: ${original}; direct write: ${fallback})`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
try {
|
|
1883
|
+
await import_fs.promises.rm(tempPath, { force: true });
|
|
1884
|
+
} catch {
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Extract account_id from auth data (supports both formats)
|
|
1889
|
+
*/
|
|
1890
|
+
getAccountIdFromData(data) {
|
|
1891
|
+
if ("account_id" in data && typeof data.account_id === "string" && data.account_id.trim()) {
|
|
1892
|
+
return data.account_id.trim();
|
|
1893
|
+
}
|
|
1894
|
+
if ("tokens" in data && data.tokens && typeof data.tokens === "object") {
|
|
1895
|
+
const accountId = data.tokens.account_id;
|
|
1896
|
+
if (typeof accountId === "string" && accountId.trim()) {
|
|
1897
|
+
return accountId.trim();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const projectId = "project_id" in data && typeof data.project_id === "string" ? data.project_id?.trim() : void 0;
|
|
1901
|
+
if (projectId) {
|
|
1902
|
+
return projectId;
|
|
1903
|
+
}
|
|
1904
|
+
const email = "email" in data && typeof data.email === "string" ? data.email?.trim() : void 0;
|
|
1905
|
+
return email || void 0;
|
|
1906
|
+
}
|
|
1907
|
+
resolveWorkspaceIdentity(data, metadata) {
|
|
1908
|
+
const directId = "workspace_id" in data && typeof data.workspace_id === "string" ? data.workspace_id.trim() : void 0;
|
|
1909
|
+
const directName = "workspace_name" in data && typeof data.workspace_name === "string" ? data.workspace_name.trim() : void 0;
|
|
1910
|
+
if (directId) {
|
|
1911
|
+
return { id: directId, name: directName };
|
|
1912
|
+
}
|
|
1913
|
+
const organizations = metadata?.organizations;
|
|
1914
|
+
if (organizations && organizations.length > 0) {
|
|
1915
|
+
const preferred = organizations.find((org) => org.isDefault) ?? organizations[0];
|
|
1916
|
+
if (preferred?.id) {
|
|
1917
|
+
return { id: preferred.id, name: preferred.title ?? directName };
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return { id: DEFAULT_WORKSPACE_ID, name: directName };
|
|
1921
|
+
}
|
|
1922
|
+
async getProfileRowByName(name) {
|
|
1923
|
+
const normalized = this.normalizeProfileName(name);
|
|
1924
|
+
const record = await this.readProfileRecord(normalized);
|
|
1925
|
+
return record ?? void 0;
|
|
1926
|
+
}
|
|
1927
|
+
async getProfileRowByAccountId(accountId, options) {
|
|
1928
|
+
const records = await this.listProfileRecords();
|
|
1929
|
+
const matches = records.filter((record) => {
|
|
1930
|
+
const storedAccountId = record.accountId ?? this.getAccountIdFromData(record.data);
|
|
1931
|
+
return storedAccountId === accountId;
|
|
1932
|
+
});
|
|
1933
|
+
if (matches.length === 0) {
|
|
1934
|
+
return void 0;
|
|
1935
|
+
}
|
|
1936
|
+
const mustMatch = typeof options?.authMethod === "string" ? options.authMethod.trim().toLowerCase() : null;
|
|
1937
|
+
if (mustMatch) {
|
|
1938
|
+
return matches.find((record) => this.resolveAuthMethod(record.data) === mustMatch);
|
|
1939
|
+
}
|
|
1940
|
+
const prefer = typeof options?.preferAuthMethod === "string" ? options.preferAuthMethod.trim().toLowerCase() : null;
|
|
1941
|
+
if (prefer) {
|
|
1942
|
+
return matches.find((record) => this.resolveAuthMethod(record.data) === prefer) ?? matches[0];
|
|
1943
|
+
}
|
|
1944
|
+
return matches[0];
|
|
1945
|
+
}
|
|
1946
|
+
async getProfileRowByAccountAndWorkspace(accountId, workspaceId, options) {
|
|
1947
|
+
const workspaceKey = workspaceId && workspaceId.trim().length > 0 ? workspaceId.trim() : DEFAULT_WORKSPACE_ID;
|
|
1948
|
+
const records = await this.listProfileRecords();
|
|
1949
|
+
const matches = records.filter((record) => {
|
|
1950
|
+
const storedAccountId = record.accountId ?? this.getAccountIdFromData(record.data);
|
|
1951
|
+
if (storedAccountId !== accountId) {
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
const storedWorkspace = record.workspaceId ?? record.data.workspace_id ?? DEFAULT_WORKSPACE_ID;
|
|
1955
|
+
return (storedWorkspace && storedWorkspace.trim() ? storedWorkspace.trim() : DEFAULT_WORKSPACE_ID) === workspaceKey;
|
|
1956
|
+
});
|
|
1957
|
+
if (matches.length === 0) {
|
|
1958
|
+
return void 0;
|
|
1959
|
+
}
|
|
1960
|
+
const mustMatch = typeof options?.authMethod === "string" ? options.authMethod.trim().toLowerCase() : null;
|
|
1961
|
+
if (mustMatch) {
|
|
1962
|
+
return matches.find((record) => this.resolveAuthMethod(record.data) === mustMatch);
|
|
1963
|
+
}
|
|
1964
|
+
const prefer = typeof options?.preferAuthMethod === "string" ? options.preferAuthMethod.trim().toLowerCase() : null;
|
|
1965
|
+
if (prefer) {
|
|
1966
|
+
return matches.find((record) => this.resolveAuthMethod(record.data) === prefer) ?? matches[0];
|
|
1967
|
+
}
|
|
1968
|
+
return matches[0];
|
|
1969
|
+
}
|
|
1970
|
+
async persistProfileRecord(name, data, metadata, options) {
|
|
1971
|
+
const resolvedName = this.normalizeProfileName(name);
|
|
1972
|
+
const existingRecord = await this.readProfileRecord(resolvedName);
|
|
1973
|
+
if (typeof data.auth_method === "string") {
|
|
1974
|
+
data.auth_method = data.auth_method.trim().toLowerCase();
|
|
1975
|
+
}
|
|
1976
|
+
if (!data.auth_method) {
|
|
1977
|
+
data.auth_method = "codex-cli";
|
|
1978
|
+
}
|
|
1979
|
+
const resolvedMetadata = metadata ?? this.extractProfileMetadata(data) ?? existingRecord?.metadata;
|
|
1980
|
+
const workspace = this.resolveWorkspaceIdentity(data, resolvedMetadata);
|
|
1981
|
+
const workspaceId = workspace.id || DEFAULT_WORKSPACE_ID;
|
|
1982
|
+
const workspaceName = workspace.name ?? null;
|
|
1983
|
+
if (!data.workspace_id) {
|
|
1984
|
+
data.workspace_id = workspaceId;
|
|
1985
|
+
}
|
|
1986
|
+
if (!data.workspace_name && workspaceName) {
|
|
1987
|
+
data.workspace_name = workspaceName;
|
|
1988
|
+
}
|
|
1989
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1990
|
+
const accountId = this.getAccountIdFromData(data) ?? null;
|
|
1991
|
+
const resolvedEmail = this.resolveProfileEmail(data, resolvedMetadata) ?? data.email ?? null;
|
|
1992
|
+
if (resolvedEmail) {
|
|
1993
|
+
data.email = resolvedEmail;
|
|
1994
|
+
}
|
|
1995
|
+
const createdAt = data.created_at ?? existingRecord?.createdAt ?? now;
|
|
1996
|
+
data.created_at = createdAt;
|
|
1997
|
+
const updatedAt = data.updated_at ?? now;
|
|
1998
|
+
data.updated_at = updatedAt;
|
|
1999
|
+
const displayName = typeof options?.displayName === "string" && options.displayName.trim().length > 0 ? options.displayName.trim() : existingRecord?.displayName ?? resolvedName;
|
|
2000
|
+
const record = {
|
|
2001
|
+
name: resolvedName,
|
|
2002
|
+
displayName,
|
|
2003
|
+
data,
|
|
2004
|
+
metadata: resolvedMetadata,
|
|
2005
|
+
accountId,
|
|
2006
|
+
workspaceId: data.workspace_id ?? workspaceId,
|
|
2007
|
+
workspaceName: data.workspace_name ?? workspaceName,
|
|
2008
|
+
email: resolvedEmail ?? null,
|
|
2009
|
+
authMethod: typeof data.auth_method === "string" ? data.auth_method : null,
|
|
2010
|
+
createdAt,
|
|
2011
|
+
updatedAt
|
|
2012
|
+
};
|
|
2013
|
+
await import_fs.promises.mkdir(this.getProfileHomePath(resolvedName), { recursive: true });
|
|
2014
|
+
await this.writeProfileRecord(record);
|
|
2015
|
+
}
|
|
2016
|
+
buildProfileFromRow(row) {
|
|
2017
|
+
const profile = this.buildProfileFromData(row.name, row.data, {
|
|
2018
|
+
displayName: row.displayName ?? void 0,
|
|
2019
|
+
email: row.email ?? void 0,
|
|
2020
|
+
createdAt: row.createdAt ?? void 0,
|
|
2021
|
+
authMethod: row.authMethod ?? void 0,
|
|
2022
|
+
accountId: row.accountId ?? void 0,
|
|
2023
|
+
workspaceId: row.workspaceId ?? void 0,
|
|
2024
|
+
workspaceName: row.workspaceName ?? void 0,
|
|
2025
|
+
metadata: row.metadata
|
|
2026
|
+
});
|
|
2027
|
+
return profile;
|
|
2028
|
+
}
|
|
2029
|
+
buildProfileFromData(name, data, fallback) {
|
|
2030
|
+
const metadata = fallback?.metadata ?? this.extractProfileMetadata(data);
|
|
2031
|
+
const workspace = this.resolveWorkspaceIdentity(data, metadata);
|
|
2032
|
+
const workspaceId = workspace.id || fallback?.workspaceId || DEFAULT_WORKSPACE_ID;
|
|
2033
|
+
const workspaceName = workspace.name ?? fallback?.workspaceName ?? void 0;
|
|
2034
|
+
const email = this.resolveProfileEmail(data, metadata) ?? fallback?.email ?? void 0;
|
|
2035
|
+
const tokenStatus = this.evaluateTokenStatus(data, metadata);
|
|
2036
|
+
const accountId = this.getAccountIdFromData(data) ?? fallback?.accountId;
|
|
2037
|
+
const projectId = typeof data.project_id === "string" && data.project_id.trim().length > 0 ? data.project_id.trim() : fallback?.projectId;
|
|
2038
|
+
return {
|
|
2039
|
+
name,
|
|
2040
|
+
displayName: fallback?.displayName ?? name,
|
|
2041
|
+
isValid: Boolean(data && Object.keys(data).length > 0),
|
|
2042
|
+
accountId,
|
|
2043
|
+
projectId,
|
|
2044
|
+
workspaceId,
|
|
2045
|
+
workspaceName,
|
|
2046
|
+
email,
|
|
2047
|
+
createdAt: data.created_at ?? fallback?.createdAt ?? void 0,
|
|
2048
|
+
authMethod: typeof data.auth_method === "string" ? data.auth_method.trim().toLowerCase() : fallback?.authMethod ?? void 0,
|
|
2049
|
+
metadata,
|
|
2050
|
+
tokenStatus
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
parseTimestamp(value) {
|
|
2054
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
const parsed = Date.parse(value);
|
|
2058
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2059
|
+
}
|
|
2060
|
+
compareProfilesForPreference(a, b) {
|
|
2061
|
+
if (Boolean(a.isValid) !== Boolean(b.isValid)) {
|
|
2062
|
+
return a.isValid ? -1 : 1;
|
|
2063
|
+
}
|
|
2064
|
+
const aTimestamp = this.parseTimestamp(a.createdAt ?? null);
|
|
2065
|
+
const bTimestamp = this.parseTimestamp(b.createdAt ?? null);
|
|
2066
|
+
if (aTimestamp !== null || bTimestamp !== null) {
|
|
2067
|
+
if (aTimestamp !== null && bTimestamp !== null) {
|
|
2068
|
+
if (aTimestamp > bTimestamp) {
|
|
2069
|
+
return -1;
|
|
2070
|
+
}
|
|
2071
|
+
if (aTimestamp < bTimestamp) {
|
|
2072
|
+
return 1;
|
|
2073
|
+
}
|
|
2074
|
+
} else if (aTimestamp !== null) {
|
|
2075
|
+
return -1;
|
|
2076
|
+
} else {
|
|
2077
|
+
return 1;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return a.name.localeCompare(b.name);
|
|
2081
|
+
}
|
|
2082
|
+
selectPreferredProfile(profiles) {
|
|
2083
|
+
if (profiles.length <= 1) {
|
|
2084
|
+
return profiles[0];
|
|
2085
|
+
}
|
|
2086
|
+
return profiles.slice().sort((a, b) => this.compareProfilesForPreference(a, b))[0];
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Convert profile data to Codex's expected format
|
|
2090
|
+
*/
|
|
2091
|
+
convertToCodexFormat(data) {
|
|
2092
|
+
if ("tokens" in data) {
|
|
2093
|
+
return data;
|
|
2094
|
+
}
|
|
2095
|
+
return {
|
|
2096
|
+
OPENAI_API_KEY: null,
|
|
2097
|
+
tokens: {
|
|
2098
|
+
id_token: data.id_token,
|
|
2099
|
+
access_token: data.access_token,
|
|
2100
|
+
refresh_token: data.refresh_token,
|
|
2101
|
+
account_id: data.account_id
|
|
2102
|
+
},
|
|
2103
|
+
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Normalize Codex auth data to flat profile format
|
|
2108
|
+
*/
|
|
2109
|
+
normalizeProfileData(data) {
|
|
2110
|
+
if ("id_token" in data) {
|
|
2111
|
+
return data;
|
|
2112
|
+
}
|
|
2113
|
+
const tokens = data.tokens || {};
|
|
2114
|
+
const profile = {
|
|
2115
|
+
id_token: tokens.id_token,
|
|
2116
|
+
access_token: tokens.access_token,
|
|
2117
|
+
refresh_token: tokens.refresh_token,
|
|
2118
|
+
account_id: tokens.account_id
|
|
2119
|
+
};
|
|
2120
|
+
if (data.email) {
|
|
2121
|
+
profile.email = data.email;
|
|
2122
|
+
}
|
|
2123
|
+
return profile;
|
|
2124
|
+
}
|
|
2125
|
+
mergeProfileRecords(existing, incoming) {
|
|
2126
|
+
const merged = {
|
|
2127
|
+
...existing,
|
|
2128
|
+
...incoming,
|
|
2129
|
+
created_at: existing.created_at ?? incoming.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2130
|
+
};
|
|
2131
|
+
if (typeof merged.auth_method === "string") {
|
|
2132
|
+
merged.auth_method = merged.auth_method.trim().toLowerCase();
|
|
2133
|
+
}
|
|
2134
|
+
if (!merged.auth_method) {
|
|
2135
|
+
merged.auth_method = "codex-cli";
|
|
2136
|
+
}
|
|
2137
|
+
const metadata = this.extractProfileMetadata(merged);
|
|
2138
|
+
const resolvedEmail = this.resolveProfileEmail(merged, metadata);
|
|
2139
|
+
if (resolvedEmail) {
|
|
2140
|
+
merged.email = resolvedEmail;
|
|
2141
|
+
}
|
|
2142
|
+
const workspace = this.resolveWorkspaceIdentity(merged, metadata);
|
|
2143
|
+
if (!merged.workspace_id) {
|
|
2144
|
+
merged.workspace_id = workspace.id || existing.workspace_id || DEFAULT_WORKSPACE_ID;
|
|
2145
|
+
}
|
|
2146
|
+
if (!merged.workspace_name) {
|
|
2147
|
+
merged.workspace_name = workspace.name ?? existing.workspace_name;
|
|
2148
|
+
}
|
|
2149
|
+
if (this.hasTokenChanges(existing, incoming)) {
|
|
2150
|
+
delete merged.tokenAlert;
|
|
2151
|
+
}
|
|
2152
|
+
return { profile: merged, metadata };
|
|
2153
|
+
}
|
|
2154
|
+
hasTokenChanges(existing, incoming) {
|
|
2155
|
+
const tokenKeys = ["id_token", "access_token", "refresh_token", "account_id", "workspace_id"];
|
|
2156
|
+
return tokenKeys.some((key) => {
|
|
2157
|
+
const nextValue = incoming[key];
|
|
2158
|
+
if (typeof nextValue !== "string" || nextValue.length === 0) {
|
|
2159
|
+
return false;
|
|
2160
|
+
}
|
|
2161
|
+
return nextValue !== existing[key];
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
async flagRefreshTokenRedeemed(name, reason, options) {
|
|
2165
|
+
await this.initialize();
|
|
2166
|
+
const profileName = this.normalizeProfileName(name);
|
|
2167
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2168
|
+
if (!record) {
|
|
2169
|
+
logWarn(`Cannot flag refresh token issue for missing profile '${profileName}'.`);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
const observedAt = options?.observedAt ? Date.parse(options.observedAt) : null;
|
|
2173
|
+
const updatedAt = record.updatedAt ? Date.parse(record.updatedAt) : null;
|
|
2174
|
+
if (observedAt !== null && updatedAt !== null && observedAt <= updatedAt) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const data = record.data;
|
|
2178
|
+
const trimmedReason = typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : void 0;
|
|
2179
|
+
const normalized = trimmedReason?.toLowerCase() ?? "";
|
|
2180
|
+
const storedReason = normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET) ? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason ?? REFRESH_TOKEN_REDEEMED_REASON;
|
|
2181
|
+
const alert = {
|
|
2182
|
+
issue: "refresh-redeemed",
|
|
2183
|
+
reason: storedReason,
|
|
2184
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2185
|
+
};
|
|
2186
|
+
data.tokenAlert = alert;
|
|
2187
|
+
try {
|
|
2188
|
+
const metadata = this.extractProfileMetadata(data);
|
|
2189
|
+
await this.persistProfileRecord(profileName, data, metadata);
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
logError(`Failed to persist token alert for profile '${profileName}':`, error);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async syncProfileTokensFromActiveAuth(profileName, baselineProfile, authPath) {
|
|
2195
|
+
const targetPath = authPath ?? this.activeAuth;
|
|
2196
|
+
try {
|
|
2197
|
+
const authRaw = await import_fs.promises.readFile(targetPath, "utf8");
|
|
2198
|
+
await this.syncProfileTokensFromAuthContent(profileName, baselineProfile, authRaw);
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
if (!this.isNotFoundError(error)) {
|
|
2201
|
+
logWarn(`Failed to read Codex auth for '${profileName}' while syncing tokens:`, error);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
async syncProfileTokensFromAuthContent(profileName, baselineProfile, authContent) {
|
|
2206
|
+
let activeAuth;
|
|
2207
|
+
try {
|
|
2208
|
+
activeAuth = JSON.parse(authContent);
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
logWarn(`Failed to parse Codex auth while syncing tokens for '${profileName}':`, error);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
const normalized = this.normalizeProfileData(activeAuth);
|
|
2214
|
+
const metadata = this.extractProfileMetadata(normalized);
|
|
2215
|
+
const workspace = this.resolveWorkspaceIdentity(normalized, metadata);
|
|
2216
|
+
normalized.workspace_id = normalized.workspace_id ?? workspace.id ?? DEFAULT_WORKSPACE_ID;
|
|
2217
|
+
if (workspace.name) {
|
|
2218
|
+
normalized.workspace_name = normalized.workspace_name ?? workspace.name;
|
|
2219
|
+
}
|
|
2220
|
+
const hasTokens = typeof normalized.id_token === "string" || typeof normalized.access_token === "string" || typeof normalized.refresh_token === "string";
|
|
2221
|
+
if (!hasTokens) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
let existing = baselineProfile;
|
|
2225
|
+
const latestRow = await this.getProfileRowByName(profileName);
|
|
2226
|
+
if (latestRow) {
|
|
2227
|
+
existing = latestRow.data;
|
|
2228
|
+
}
|
|
2229
|
+
const existingAccountId = this.getAccountIdFromData(existing);
|
|
2230
|
+
const newAccountId = this.getAccountIdFromData(normalized);
|
|
2231
|
+
if (existingAccountId && newAccountId && existingAccountId !== newAccountId) {
|
|
2232
|
+
logWarn(
|
|
2233
|
+
`Skipped syncing tokens for profile '${profileName}' because Codex auth switched to account '${newAccountId}'.`
|
|
2234
|
+
);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (!this.hasTokenChanges(existing, normalized)) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const { profile: merged, metadata: mergedMetadata } = this.mergeProfileRecords(existing, normalized);
|
|
2241
|
+
try {
|
|
2242
|
+
await this.persistProfileRecord(profileName, merged, mergedMetadata);
|
|
2243
|
+
} catch (error) {
|
|
2244
|
+
logError(`Failed to persist refreshed tokens for profile '${profileName}':`, error);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
async copyCodexConfig(targetCodexHome) {
|
|
2248
|
+
const candidates = ["config.toml", "config.json", "config.yaml", "config.yml"];
|
|
2249
|
+
for (const file of candidates) {
|
|
2250
|
+
const source = (0, import_path2.join)(this.codexDir, file);
|
|
2251
|
+
const destination = (0, import_path2.join)(targetCodexHome, file);
|
|
2252
|
+
try {
|
|
2253
|
+
await import_fs.promises.copyFile(source, destination);
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
if (!this.isNotFoundError(error)) {
|
|
2256
|
+
logWarn(`Failed to copy Codex config '${file}' to profile home:`, error);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
getProfileHomePath(profileName) {
|
|
2262
|
+
return (0, import_path2.join)(this.profileHomesRoot, profileName);
|
|
2263
|
+
}
|
|
2264
|
+
ensureTrailingNewline(content) {
|
|
2265
|
+
return content.endsWith("\n") ? content : `${content}
|
|
2266
|
+
`;
|
|
2267
|
+
}
|
|
2268
|
+
async sanitizeProfileConfig(profileHome) {
|
|
2269
|
+
const configPath = (0, import_path2.join)(profileHome, "config.toml");
|
|
2270
|
+
let raw;
|
|
2271
|
+
try {
|
|
2272
|
+
raw = await import_fs.promises.readFile(configPath, "utf8");
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
if (!this.isNotFoundError(error)) {
|
|
2275
|
+
logWarn(`Failed to read config for profile home '${profileHome}':`, error);
|
|
2276
|
+
}
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
let parsed;
|
|
2280
|
+
try {
|
|
2281
|
+
parsed = (0, import_toml.parse)(raw);
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
logWarn(`Failed to parse config.toml for profile home '${profileHome}':`, error);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (!Object.prototype.hasOwnProperty.call(parsed, "model_reasoning_effort")) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
delete parsed["model_reasoning_effort"];
|
|
2290
|
+
const sanitized = this.ensureTrailingNewline((0, import_toml.stringify)(parsed));
|
|
2291
|
+
try {
|
|
2292
|
+
await import_fs.promises.writeFile(configPath, sanitized, "utf8");
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
logWarn(`Failed to write sanitized config.toml for profile home '${profileHome}':`, error);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
async prepareProfileHome(profileName, authData) {
|
|
2298
|
+
const profileHome = this.getProfileHomePath(profileName);
|
|
2299
|
+
await import_fs.promises.mkdir(profileHome, { recursive: true });
|
|
2300
|
+
const authPath = (0, import_path2.join)(profileHome, "auth.json");
|
|
2301
|
+
await this.writeAtomic(authPath, JSON.stringify(authData, null, 2));
|
|
2302
|
+
await this.copyCodexConfig(profileHome);
|
|
2303
|
+
await this.sanitizeProfileConfig(profileHome);
|
|
2304
|
+
return profileHome;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* List all available profiles
|
|
2308
|
+
*/
|
|
2309
|
+
async profileExists(name) {
|
|
2310
|
+
await this.initialize();
|
|
2311
|
+
const profileName = this.normalizeProfileName(name);
|
|
2312
|
+
return Boolean(await this.getProfileRowByName(profileName));
|
|
2313
|
+
}
|
|
2314
|
+
async listProfiles() {
|
|
2315
|
+
await this.initialize();
|
|
2316
|
+
try {
|
|
2317
|
+
const records = await this.listProfileRecords();
|
|
2318
|
+
return records.map((record) => this.buildProfileFromRow(record)).filter((profile) => !profile.authMethod || profile.authMethod === "codex-cli").sort((a, b) => {
|
|
2319
|
+
const aLabel = a.displayName ?? a.name;
|
|
2320
|
+
const bLabel = b.displayName ?? b.name;
|
|
2321
|
+
const base = aLabel.localeCompare(bLabel);
|
|
2322
|
+
return base !== 0 ? base : a.name.localeCompare(b.name);
|
|
2323
|
+
});
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
logError("Error reading profiles from disk:", error);
|
|
2326
|
+
return [];
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Create a new profile by running codex login
|
|
2331
|
+
*/
|
|
2332
|
+
async createProfile(name) {
|
|
2333
|
+
await this.initialize();
|
|
2334
|
+
const profileName = this.normalizeProfileName(name);
|
|
2335
|
+
if (await this.getProfileRowByName(profileName)) {
|
|
2336
|
+
throw new Error(`Profile '${profileName}' already exists!`);
|
|
2337
|
+
}
|
|
2338
|
+
let authData;
|
|
2339
|
+
try {
|
|
2340
|
+
const authRaw = await import_fs.promises.readFile(this.activeAuth, "utf8");
|
|
2341
|
+
authData = JSON.parse(authRaw);
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
logError("Error creating profile:", error);
|
|
2344
|
+
throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
|
|
2345
|
+
}
|
|
2346
|
+
const normalizedProfile = this.normalizeProfileData(authData);
|
|
2347
|
+
normalizedProfile.created_at = normalizedProfile.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2348
|
+
if (typeof normalizedProfile.auth_method === "string") {
|
|
2349
|
+
normalizedProfile.auth_method = normalizedProfile.auth_method.trim().toLowerCase();
|
|
2350
|
+
}
|
|
2351
|
+
normalizedProfile.auth_method = normalizedProfile.auth_method ?? "codex-cli";
|
|
2352
|
+
const metadata = this.extractProfileMetadata(normalizedProfile);
|
|
2353
|
+
const workspace = this.resolveWorkspaceIdentity(normalizedProfile, metadata);
|
|
2354
|
+
normalizedProfile.workspace_id = normalizedProfile.workspace_id ?? workspace.id ?? DEFAULT_WORKSPACE_ID;
|
|
2355
|
+
if (workspace.name) {
|
|
2356
|
+
normalizedProfile.workspace_name = normalizedProfile.workspace_name ?? workspace.name;
|
|
2357
|
+
}
|
|
2358
|
+
const accountId = this.getAccountIdFromData(normalizedProfile);
|
|
2359
|
+
if (accountId) {
|
|
2360
|
+
const requestedWorkspace = normalizedProfile.workspace_id ?? DEFAULT_WORKSPACE_ID;
|
|
2361
|
+
const duplicate = await this.getProfileRowByAccountAndWorkspace(accountId, requestedWorkspace, { authMethod: "codex-cli" });
|
|
2362
|
+
if (duplicate) {
|
|
2363
|
+
normalizedProfile.workspace_id = `${requestedWorkspace}:${profileName}`;
|
|
2364
|
+
normalizedProfile.workspace_name = normalizedProfile.workspace_name ?? profileName;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
const resolvedEmail = this.resolveProfileEmail(normalizedProfile, metadata);
|
|
2368
|
+
if (resolvedEmail) {
|
|
2369
|
+
normalizedProfile.email = resolvedEmail;
|
|
2370
|
+
}
|
|
2371
|
+
try {
|
|
2372
|
+
await this.persistProfileRecord(profileName, normalizedProfile, metadata);
|
|
2373
|
+
const stored = await this.getProfileRowByName(profileName);
|
|
2374
|
+
return stored ? this.buildProfileFromRow(stored) : null;
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
logError("Error creating profile:", error);
|
|
2377
|
+
throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Switch to a different profile
|
|
2382
|
+
*/
|
|
2383
|
+
async switchToProfile(name) {
|
|
2384
|
+
await this.initialize();
|
|
2385
|
+
const profileName = this.normalizeProfileName(name);
|
|
2386
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2387
|
+
if (!record) {
|
|
2388
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2389
|
+
}
|
|
2390
|
+
try {
|
|
2391
|
+
const profileData = record.data;
|
|
2392
|
+
const codexFormat = this.convertToCodexFormat(profileData);
|
|
2393
|
+
await this.writeAtomic(this.activeAuth, JSON.stringify(codexFormat, null, 2));
|
|
2394
|
+
await this.persistPreferredProfileName(profileName);
|
|
2395
|
+
return true;
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
logError("Error switching profile:", error);
|
|
2398
|
+
throw new Error("Failed to switch profile");
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async refreshProfileAuth(name) {
|
|
2402
|
+
await this.initialize();
|
|
2403
|
+
const profileName = this.normalizeProfileName(name);
|
|
2404
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2405
|
+
if (!record) {
|
|
2406
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2407
|
+
}
|
|
2408
|
+
const existing = record.data;
|
|
2409
|
+
let activeAuth;
|
|
2410
|
+
try {
|
|
2411
|
+
const authContent = await import_fs.promises.readFile(this.activeAuth, "utf8");
|
|
2412
|
+
activeAuth = JSON.parse(authContent);
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
if (this.isNotFoundError(error)) {
|
|
2415
|
+
throw new Error("Codex CLI did not produce an auth file. Complete the login before refreshing this profile.");
|
|
2416
|
+
}
|
|
2417
|
+
logError("Failed to read Codex auth file during refresh:", error);
|
|
2418
|
+
throw new Error("Failed to read Codex auth file. Complete the login and try again.");
|
|
2419
|
+
}
|
|
2420
|
+
const normalized = this.normalizeProfileData(activeAuth);
|
|
2421
|
+
const existingAccountId = this.getAccountIdFromData(existing);
|
|
2422
|
+
const newAccountId = this.getAccountIdFromData(normalized);
|
|
2423
|
+
const normalizedMetadata = this.extractProfileMetadata(normalized);
|
|
2424
|
+
const normalizedWorkspace = this.resolveWorkspaceIdentity(normalized, normalizedMetadata);
|
|
2425
|
+
normalized.workspace_id = normalized.workspace_id ?? normalizedWorkspace.id ?? DEFAULT_WORKSPACE_ID;
|
|
2426
|
+
if (normalizedWorkspace.name) {
|
|
2427
|
+
normalized.workspace_name = normalized.workspace_name ?? normalizedWorkspace.name;
|
|
2428
|
+
}
|
|
2429
|
+
if (existingAccountId && newAccountId && existingAccountId !== newAccountId) {
|
|
2430
|
+
throw new Error(
|
|
2431
|
+
`Active Codex login is for account '${newAccountId}', but profile '${profileName}' is tied to '${existingAccountId}'. Create a new profile for the new account instead.`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
if (existingAccountId && normalized.workspace_id) {
|
|
2435
|
+
const currentWorkspace = this.resolveWorkspaceIdentity(existing, this.extractProfileMetadata(existing));
|
|
2436
|
+
const targetWorkspaceId = normalized.workspace_id;
|
|
2437
|
+
const currentWorkspaceId = currentWorkspace.id ?? DEFAULT_WORKSPACE_ID;
|
|
2438
|
+
if (targetWorkspaceId !== currentWorkspaceId) {
|
|
2439
|
+
const duplicate = await this.getProfileRowByAccountAndWorkspace(existingAccountId, targetWorkspaceId, {
|
|
2440
|
+
authMethod: this.resolveAuthMethod(existing)
|
|
2441
|
+
});
|
|
2442
|
+
if (duplicate && duplicate.name !== profileName) {
|
|
2443
|
+
throw new Error(
|
|
2444
|
+
`Account is already saved as profile '${duplicate.name}'. Switch to or delete that profile before creating another.`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const { profile: merged, metadata } = this.mergeProfileRecords(existing, normalized);
|
|
2450
|
+
delete merged.tokenAlert;
|
|
2451
|
+
await this.persistProfileRecord(profileName, merged, metadata);
|
|
2452
|
+
const updated = await this.getProfileRowByName(profileName);
|
|
2453
|
+
if (!updated) {
|
|
2454
|
+
throw new Error("Failed to persist refreshed profile data.");
|
|
2455
|
+
}
|
|
2456
|
+
const profile = this.buildProfileFromRow(updated);
|
|
2457
|
+
if (metadata) {
|
|
2458
|
+
profile.metadata = metadata;
|
|
2459
|
+
profile.tokenStatus = this.evaluateTokenStatus(merged, metadata);
|
|
2460
|
+
}
|
|
2461
|
+
return profile;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Execute an action with a profile's auth injected.
|
|
2465
|
+
* Creates an isolated CODEX_HOME with the profile's auth/config, invokes the action
|
|
2466
|
+
* with env overrides, then syncs any refreshed tokens back to the profile.
|
|
2467
|
+
*/
|
|
2468
|
+
async runWithProfileAuth(name, action) {
|
|
2469
|
+
return this.enqueueAuthSwap(async () => {
|
|
2470
|
+
await this.initialize();
|
|
2471
|
+
const profileName = this.normalizeProfileName(name);
|
|
2472
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2473
|
+
if (!record) {
|
|
2474
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2475
|
+
}
|
|
2476
|
+
const profileData = record.data;
|
|
2477
|
+
const profileHome = this.getProfileHomePath(profileName);
|
|
2478
|
+
await import_fs.promises.mkdir(profileHome, { recursive: true });
|
|
2479
|
+
if (profileData.auth_method && profileData.auth_method !== "codex-cli") {
|
|
2480
|
+
throw new Error("Unsupported auth method. Codex CLI profiles only.");
|
|
2481
|
+
}
|
|
2482
|
+
const codexFormat = this.convertToCodexFormat(profileData);
|
|
2483
|
+
await this.prepareProfileHome(profileName, codexFormat);
|
|
2484
|
+
const authPath = import_path.default.join(profileHome, "auth.json");
|
|
2485
|
+
const envOverrides = {
|
|
2486
|
+
...process.env,
|
|
2487
|
+
HOME: profileHome,
|
|
2488
|
+
USERPROFILE: profileHome,
|
|
2489
|
+
CODEX_HOME: profileHome
|
|
2490
|
+
};
|
|
2491
|
+
try {
|
|
2492
|
+
return await action(envOverrides);
|
|
2493
|
+
} finally {
|
|
2494
|
+
let finalAuthContent = null;
|
|
2495
|
+
try {
|
|
2496
|
+
finalAuthContent = await import_fs.promises.readFile(authPath, "utf8");
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
if (!this.isNotFoundError(error)) {
|
|
2499
|
+
logWarn(`Failed to read isolated auth for '${profileName}' after action:`, error);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
if (finalAuthContent) {
|
|
2504
|
+
await this.syncProfileTokensFromAuthContent(profileName, profileData, finalAuthContent);
|
|
2505
|
+
} else {
|
|
2506
|
+
await this.syncProfileTokensFromActiveAuth(profileName, profileData, authPath);
|
|
2507
|
+
}
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
logWarn(`Failed to sync refreshed tokens for profile '${profileName}':`, error);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Rename a profile
|
|
2516
|
+
*/
|
|
2517
|
+
async renameProfile(oldName, newName) {
|
|
2518
|
+
await this.initialize();
|
|
2519
|
+
const sourceName = this.normalizeProfileName(oldName);
|
|
2520
|
+
const targetName = this.normalizeProfileName(newName);
|
|
2521
|
+
const existing = await this.getProfileRowByName(sourceName);
|
|
2522
|
+
if (!existing) {
|
|
2523
|
+
throw new Error(`Profile '${sourceName}' not found!`);
|
|
2524
|
+
}
|
|
2525
|
+
if (await this.getProfileRowByName(targetName)) {
|
|
2526
|
+
throw new Error(`Profile '${targetName}' already exists!`);
|
|
2527
|
+
}
|
|
2528
|
+
const preferred = await this.readPreferredProfileName();
|
|
2529
|
+
if (preferred && preferred === sourceName) {
|
|
2530
|
+
await this.persistPreferredProfileName(targetName);
|
|
2531
|
+
}
|
|
2532
|
+
const oldHome = this.getProfileHomePath(sourceName);
|
|
2533
|
+
const newHome = this.getProfileHomePath(targetName);
|
|
2534
|
+
try {
|
|
2535
|
+
await import_fs.promises.mkdir(this.profileHomesRoot, { recursive: true });
|
|
2536
|
+
await import_fs.promises.rename(oldHome, newHome);
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
if (!this.isNotFoundError(error)) {
|
|
2539
|
+
logWarn(`Failed to move profile home from '${sourceName}' to '${targetName}':`, error);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
try {
|
|
2543
|
+
const updatedRecord = {
|
|
2544
|
+
...existing,
|
|
2545
|
+
name: targetName
|
|
2546
|
+
};
|
|
2547
|
+
await this.writeProfileRecord(updatedRecord);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
logWarn(`Failed to rewrite profile record after renaming '${sourceName}' to '${targetName}':`, error);
|
|
2550
|
+
}
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
async updateProfileDisplayName(name, displayName) {
|
|
2554
|
+
await this.initialize();
|
|
2555
|
+
const profileName = this.normalizeProfileName(name);
|
|
2556
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2557
|
+
if (!record) {
|
|
2558
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2559
|
+
}
|
|
2560
|
+
const trimmed = typeof displayName === "string" ? displayName.trim() : "";
|
|
2561
|
+
if (!trimmed) {
|
|
2562
|
+
throw new Error("Profile name must not be empty.");
|
|
2563
|
+
}
|
|
2564
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2565
|
+
record.displayName = trimmed;
|
|
2566
|
+
record.updatedAt = now;
|
|
2567
|
+
record.data.updated_at = now;
|
|
2568
|
+
await this.writeProfileRecord(record);
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* Delete a profile
|
|
2573
|
+
*/
|
|
2574
|
+
async deleteProfile(name) {
|
|
2575
|
+
await this.initialize();
|
|
2576
|
+
const profileName = this.normalizeProfileName(name);
|
|
2577
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2578
|
+
if (!record) {
|
|
2579
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2580
|
+
}
|
|
2581
|
+
const preferred = await this.readPreferredProfileName();
|
|
2582
|
+
if (preferred && preferred === profileName) {
|
|
2583
|
+
await this.persistPreferredProfileName(null);
|
|
2584
|
+
}
|
|
2585
|
+
try {
|
|
2586
|
+
await import_fs.promises.rm(this.getProfileHomePath(profileName), { recursive: true, force: true });
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
if (!this.isNotFoundError(error)) {
|
|
2589
|
+
logWarn(`Failed to remove profile home for '${profileName}':`, error);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return true;
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Delete every profile that does not appear in the allow-list.
|
|
2596
|
+
* Used to enforce plan limits when local storage was tampered with.
|
|
2597
|
+
*/
|
|
2598
|
+
async deleteProfilesNotIn(allowedNames) {
|
|
2599
|
+
await this.initialize();
|
|
2600
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
2601
|
+
for (const name of allowedNames) {
|
|
2602
|
+
try {
|
|
2603
|
+
normalized.add(this.normalizeProfileName(name));
|
|
2604
|
+
} catch {
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const records = await this.listProfileRecords();
|
|
2608
|
+
const deleteCandidates = records.map((record) => {
|
|
2609
|
+
try {
|
|
2610
|
+
return this.normalizeProfileName(record.name);
|
|
2611
|
+
} catch {
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
}).filter((name) => Boolean(name) && !normalized.has(name));
|
|
2615
|
+
if (deleteCandidates.length === 0) {
|
|
2616
|
+
return [];
|
|
2617
|
+
}
|
|
2618
|
+
const removed = [];
|
|
2619
|
+
for (const name of deleteCandidates) {
|
|
2620
|
+
try {
|
|
2621
|
+
await import_fs.promises.rm(this.getProfileHomePath(name), { recursive: true, force: true });
|
|
2622
|
+
removed.push(name);
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
if (!this.isNotFoundError(error)) {
|
|
2625
|
+
logWarn(`Failed to remove profile '${name}' during plan enforcement:`, error);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
const preferred = await this.readPreferredProfileName();
|
|
2630
|
+
if (preferred && removed.includes(preferred)) {
|
|
2631
|
+
await this.persistPreferredProfileName(null);
|
|
2632
|
+
}
|
|
2633
|
+
return removed;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Check if the current auth matches a profile (for smart detection)
|
|
2637
|
+
*/
|
|
2638
|
+
async getCurrentProfile() {
|
|
2639
|
+
await this.initialize();
|
|
2640
|
+
const preferredName = await this.readPreferredProfileName();
|
|
2641
|
+
if (preferredName) {
|
|
2642
|
+
try {
|
|
2643
|
+
const normalizedPreferred = this.normalizeProfileName(preferredName);
|
|
2644
|
+
const preferredRecord = await this.getProfileRowByName(normalizedPreferred);
|
|
2645
|
+
if (preferredRecord) {
|
|
2646
|
+
return { name: normalizedPreferred, trusted: true };
|
|
2647
|
+
}
|
|
2648
|
+
} catch {
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const activeAuth = await this.readActiveAuthFile();
|
|
2652
|
+
if (activeAuth) {
|
|
2653
|
+
const normalizedAuth = this.normalizeProfileData(activeAuth);
|
|
2654
|
+
const authMetadata = this.extractProfileMetadata(normalizedAuth);
|
|
2655
|
+
const workspace = this.resolveWorkspaceIdentity(normalizedAuth, authMetadata);
|
|
2656
|
+
const activeAccountId = this.getAccountIdFromData(normalizedAuth);
|
|
2657
|
+
if (activeAccountId) {
|
|
2658
|
+
const scoped = await this.getProfileRowByAccountAndWorkspace(activeAccountId, workspace.id ?? DEFAULT_WORKSPACE_ID, {
|
|
2659
|
+
preferAuthMethod: "codex-cli"
|
|
2660
|
+
});
|
|
2661
|
+
if (scoped) {
|
|
2662
|
+
const normalized = this.normalizeProfileName(scoped.name);
|
|
2663
|
+
await this.persistPreferredProfileName(normalized);
|
|
2664
|
+
return { name: normalized, trusted: true };
|
|
2665
|
+
}
|
|
2666
|
+
const matching = await this.getProfileRowByAccountId(activeAccountId, { preferAuthMethod: "codex-cli" });
|
|
2667
|
+
if (matching) {
|
|
2668
|
+
const normalized = this.normalizeProfileName(matching.name);
|
|
2669
|
+
await this.persistPreferredProfileName(normalized);
|
|
2670
|
+
return { name: normalized, trusted: true };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const preferred = await this.readPreferredProfileName();
|
|
2675
|
+
if (preferred) {
|
|
2676
|
+
try {
|
|
2677
|
+
const normalized = this.normalizeProfileName(preferred);
|
|
2678
|
+
const exists = await this.getProfileRowByName(normalized);
|
|
2679
|
+
if (exists) {
|
|
2680
|
+
return { name: normalized, trusted: true };
|
|
2681
|
+
}
|
|
2682
|
+
} catch {
|
|
2683
|
+
await this.persistPreferredProfileName(null);
|
|
2684
|
+
return { name: null, trusted: false };
|
|
2685
|
+
}
|
|
2686
|
+
await this.persistPreferredProfileName(null);
|
|
2687
|
+
}
|
|
2688
|
+
return { name: null, trusted: false };
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Export a profile to a portable format.
|
|
2692
|
+
* Sensitive tokens are included but should be handled securely.
|
|
2693
|
+
*/
|
|
2694
|
+
async exportProfile(name) {
|
|
2695
|
+
await this.initialize();
|
|
2696
|
+
const profileName = this.normalizeProfileName(name);
|
|
2697
|
+
const record = await this.getProfileRowByName(profileName);
|
|
2698
|
+
if (!record) {
|
|
2699
|
+
throw new Error(`Profile '${profileName}' not found!`);
|
|
2700
|
+
}
|
|
2701
|
+
return {
|
|
2702
|
+
name: record.name,
|
|
2703
|
+
displayName: record.displayName,
|
|
2704
|
+
email: record.email,
|
|
2705
|
+
accountId: record.accountId,
|
|
2706
|
+
workspaceId: record.workspaceId,
|
|
2707
|
+
workspaceName: record.workspaceName,
|
|
2708
|
+
authMethod: record.authMethod,
|
|
2709
|
+
createdAt: record.createdAt,
|
|
2710
|
+
metadata: record.metadata,
|
|
2711
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Export all profiles to a portable format.
|
|
2716
|
+
*/
|
|
2717
|
+
async exportAllProfiles() {
|
|
2718
|
+
await this.initialize();
|
|
2719
|
+
const records = await this.listProfileRecords();
|
|
2720
|
+
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2721
|
+
return records.map((record) => ({
|
|
2722
|
+
name: record.name,
|
|
2723
|
+
displayName: record.displayName,
|
|
2724
|
+
email: record.email,
|
|
2725
|
+
accountId: record.accountId,
|
|
2726
|
+
workspaceId: record.workspaceId,
|
|
2727
|
+
workspaceName: record.workspaceName,
|
|
2728
|
+
authMethod: record.authMethod,
|
|
2729
|
+
createdAt: record.createdAt,
|
|
2730
|
+
metadata: record.metadata,
|
|
2731
|
+
exportedAt
|
|
2732
|
+
}));
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
|
|
2736
|
+
// ../../lib/license-service.ts
|
|
2737
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
2738
|
+
|
|
2739
|
+
// ../../lib/license-secret.ts
|
|
2740
|
+
var import_node_fs3 = require("fs");
|
|
2741
|
+
var import_node_path3 = __toESM(require("path"));
|
|
2742
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
2743
|
+
var SECRET_FILE = "license.secret";
|
|
2744
|
+
function resolveCodexDir2() {
|
|
2745
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
2746
|
+
return homeDir ? import_node_path3.default.join(homeDir, ".codex") : ".codex";
|
|
2747
|
+
}
|
|
2748
|
+
function resolveSecretPath() {
|
|
2749
|
+
return import_node_path3.default.join(resolveCodexDir2(), SECRET_FILE);
|
|
2750
|
+
}
|
|
2751
|
+
async function generateSecret() {
|
|
2752
|
+
return import_node_crypto.default.randomBytes(32).toString("hex");
|
|
2753
|
+
}
|
|
2754
|
+
async function getLicenseSecret() {
|
|
2755
|
+
const secretPath = resolveSecretPath();
|
|
2756
|
+
try {
|
|
2757
|
+
const existing = await import_node_fs3.promises.readFile(secretPath, "utf8");
|
|
2758
|
+
const trimmed = existing.trim();
|
|
2759
|
+
if (trimmed.length > 0) {
|
|
2760
|
+
return trimmed;
|
|
2761
|
+
}
|
|
2762
|
+
} catch {
|
|
2763
|
+
}
|
|
2764
|
+
const secret = await generateSecret();
|
|
2765
|
+
try {
|
|
2766
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(secretPath), { recursive: true });
|
|
2767
|
+
} catch {
|
|
2768
|
+
}
|
|
2769
|
+
await import_node_fs3.promises.writeFile(secretPath, `${secret}
|
|
2770
|
+
`, { mode: 384 });
|
|
2771
|
+
return secret;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// ../../lib/license-service.ts
|
|
2775
|
+
var PRODUCT_PERMALINK = "codex-use";
|
|
2776
|
+
var PRODUCT_ID = "3_CcyVEXt2FOMiEpPx8xzw==";
|
|
2777
|
+
var BASE_PRICE = 19;
|
|
2778
|
+
var BASE_PRICE_DISPLAY = "$19";
|
|
2779
|
+
var PROMO_CODE = "NOELNEWYEAR50";
|
|
2780
|
+
var PROMO_PERCENT = 50;
|
|
2781
|
+
var PROMO_ACTIVE = true;
|
|
2782
|
+
var PROMO_PRICE_DISPLAY = `$${(BASE_PRICE * (100 - PROMO_PERCENT) / 100).toFixed(2)}`;
|
|
2783
|
+
var PRODUCT_URL = PROMO_ACTIVE ? `https://hweihwang.gumroad.com/l/${PRODUCT_PERMALINK}/${PROMO_CODE}` : "https://hweihwang.gumroad.com/l/codex-use";
|
|
2784
|
+
var LICENSE_PRICE_DISPLAY = PROMO_ACTIVE ? PROMO_PRICE_DISPLAY : BASE_PRICE_DISPLAY;
|
|
2785
|
+
var LICENSE_MAX_USES = 5;
|
|
2786
|
+
var FREE_PROFILE_LIMIT = 2;
|
|
2787
|
+
var LICENSE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2788
|
+
var MAX_NEXT_CHECK_MS = 60 * 60 * 1e3;
|
|
2789
|
+
var GRACE_MAX_AGE_MS = 3 * 60 * 60 * 1e3;
|
|
2790
|
+
var LicenseError = class extends Error {
|
|
2791
|
+
constructor(message, code) {
|
|
2792
|
+
super(message);
|
|
2793
|
+
this.name = "LicenseError";
|
|
2794
|
+
this.code = code;
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
function nowIso() {
|
|
2798
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2799
|
+
}
|
|
2800
|
+
function maskLicenseKey(key) {
|
|
2801
|
+
if (!key || typeof key !== "string") {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
const trimmed = key.trim();
|
|
2805
|
+
if (trimmed.length <= 4) {
|
|
2806
|
+
return "\u2022\u2022\u2022\u2022";
|
|
2807
|
+
}
|
|
2808
|
+
const visible = trimmed.slice(-4);
|
|
2809
|
+
return `\u2022\u2022\u2022\u2022${visible}`;
|
|
2810
|
+
}
|
|
2811
|
+
function resolveProfilesRemaining(limit, currentProfiles) {
|
|
2812
|
+
if (typeof limit !== "number") {
|
|
2813
|
+
return null;
|
|
2814
|
+
}
|
|
2815
|
+
return Math.max(0, limit - currentProfiles);
|
|
2816
|
+
}
|
|
2817
|
+
function determineTierFromStored(stored) {
|
|
2818
|
+
if (!stored || !stored.licenseKey) {
|
|
2819
|
+
return "free";
|
|
2820
|
+
}
|
|
2821
|
+
if (stored.status === "inactive") {
|
|
2822
|
+
return "free";
|
|
2823
|
+
}
|
|
2824
|
+
return "pro";
|
|
2825
|
+
}
|
|
2826
|
+
function licenseSignaturePayload(license) {
|
|
2827
|
+
const payload = {
|
|
2828
|
+
licenseKey: license.licenseKey ?? null,
|
|
2829
|
+
purchaseEmail: license.purchaseEmail ?? null,
|
|
2830
|
+
lastVerifiedAt: license.lastVerifiedAt ?? null,
|
|
2831
|
+
nextCheckAt: license.nextCheckAt ?? null,
|
|
2832
|
+
lastVerificationError: license.lastVerificationError ?? null,
|
|
2833
|
+
status: license.status ?? null
|
|
2834
|
+
};
|
|
2835
|
+
return JSON.stringify(payload);
|
|
2836
|
+
}
|
|
2837
|
+
function signLicense(license, secret) {
|
|
2838
|
+
return import_node_crypto2.default.createHmac("sha256", secret).update(licenseSignaturePayload(license)).digest("hex");
|
|
2839
|
+
}
|
|
2840
|
+
function withSignature(license, secret) {
|
|
2841
|
+
return {
|
|
2842
|
+
...license,
|
|
2843
|
+
signature: signLicense(license, secret)
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
function isSignatureValid(license, secret) {
|
|
2847
|
+
if (!license.signature) {
|
|
2848
|
+
return false;
|
|
2849
|
+
}
|
|
2850
|
+
const expected = signLicense(license, secret);
|
|
2851
|
+
return expected === license.signature;
|
|
2852
|
+
}
|
|
2853
|
+
async function requestGumroadVerify(licenseKey, mode) {
|
|
2854
|
+
const controller = new AbortController();
|
|
2855
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
2856
|
+
try {
|
|
2857
|
+
const body = new URLSearchParams({
|
|
2858
|
+
product_permalink: PRODUCT_PERMALINK,
|
|
2859
|
+
product_id: PRODUCT_ID,
|
|
2860
|
+
license_key: licenseKey,
|
|
2861
|
+
increment_uses_count: mode === "activation" ? "true" : "false"
|
|
2862
|
+
});
|
|
2863
|
+
const response = await fetch("https://api.gumroad.com/v2/licenses/verify", {
|
|
2864
|
+
method: "POST",
|
|
2865
|
+
headers: {
|
|
2866
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2867
|
+
},
|
|
2868
|
+
body,
|
|
2869
|
+
signal: controller.signal
|
|
2870
|
+
});
|
|
2871
|
+
if (!response.ok) {
|
|
2872
|
+
const errorText = await response.text().catch(() => null);
|
|
2873
|
+
const message = errorText ? `License verification failed (${response.status}): ${errorText}` : `License verification failed (${response.status}).`;
|
|
2874
|
+
throw new LicenseError(message, "network");
|
|
2875
|
+
}
|
|
2876
|
+
const json = await response.json();
|
|
2877
|
+
return json;
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
if (error instanceof LicenseError) {
|
|
2880
|
+
throw error;
|
|
2881
|
+
}
|
|
2882
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2883
|
+
throw new LicenseError("License verification timed out.", "network");
|
|
2884
|
+
}
|
|
2885
|
+
throw new LicenseError(
|
|
2886
|
+
error instanceof Error ? error.message : "Unable to reach Gumroad for license verification.",
|
|
2887
|
+
"network"
|
|
2888
|
+
);
|
|
2889
|
+
} finally {
|
|
2890
|
+
clearTimeout(timeout);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
function normalizeVerificationResult(response) {
|
|
2894
|
+
if (!response.success) {
|
|
2895
|
+
const message = response.message ?? "License key is invalid.";
|
|
2896
|
+
throw new LicenseError(message, "invalid");
|
|
2897
|
+
}
|
|
2898
|
+
if (!response.purchase) {
|
|
2899
|
+
throw new LicenseError("License verification response was incomplete.", "network");
|
|
2900
|
+
}
|
|
2901
|
+
if (response.purchase.chargebacked || response.purchase.refunded || response.license_disabled) {
|
|
2902
|
+
throw new LicenseError("This license has been revoked or refunded.", "revoked");
|
|
2903
|
+
}
|
|
2904
|
+
if (response.purchase.subscription_cancelled || response.purchase.subscription_failed) {
|
|
2905
|
+
throw new LicenseError("This license subscription is no longer active.", "revoked");
|
|
2906
|
+
}
|
|
2907
|
+
const email = response.purchase.email ?? null;
|
|
2908
|
+
return { email };
|
|
2909
|
+
}
|
|
2910
|
+
function ensureWithinUseLimit(response) {
|
|
2911
|
+
if (!response.success) {
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
if (typeof response.uses !== "number") {
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
if (response.uses > LICENSE_MAX_USES) {
|
|
2918
|
+
throw new LicenseError(
|
|
2919
|
+
`This license has reached the activation limit (${LICENSE_MAX_USES}). Contact support to move it to another device.`,
|
|
2920
|
+
"invalid"
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function toLicenseStatus(stored, overrides = {}) {
|
|
2925
|
+
const tier = determineTierFromStored(stored);
|
|
2926
|
+
const isPro = tier === "pro";
|
|
2927
|
+
const state = stored && stored.status ? stored.status : isPro ? "active" : "inactive";
|
|
2928
|
+
const base = {
|
|
2929
|
+
tier,
|
|
2930
|
+
state,
|
|
2931
|
+
isPro,
|
|
2932
|
+
profileLimit: isPro ? null : FREE_PROFILE_LIMIT,
|
|
2933
|
+
profilesRemaining: null,
|
|
2934
|
+
purchaseEmail: stored?.purchaseEmail ?? null,
|
|
2935
|
+
maskedLicenseKey: maskLicenseKey(stored?.licenseKey),
|
|
2936
|
+
lastVerifiedAt: stored?.lastVerifiedAt ?? null,
|
|
2937
|
+
nextCheckAt: stored?.nextCheckAt ?? null,
|
|
2938
|
+
message: null,
|
|
2939
|
+
error: stored?.lastVerificationError ?? null,
|
|
2940
|
+
productUrl: PRODUCT_URL,
|
|
2941
|
+
productId: PRODUCT_ID,
|
|
2942
|
+
productPermalink: PRODUCT_PERMALINK,
|
|
2943
|
+
priceDisplay: LICENSE_PRICE_DISPLAY,
|
|
2944
|
+
basePriceDisplay: BASE_PRICE_DISPLAY,
|
|
2945
|
+
promoCode: PROMO_ACTIVE ? PROMO_CODE : null,
|
|
2946
|
+
promoPercent: PROMO_ACTIVE ? PROMO_PERCENT : null
|
|
2947
|
+
};
|
|
2948
|
+
return { ...base, ...overrides };
|
|
2949
|
+
}
|
|
2950
|
+
function parseTimestamp(value) {
|
|
2951
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2952
|
+
return null;
|
|
2953
|
+
}
|
|
2954
|
+
const parsed = Date.parse(value);
|
|
2955
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
2956
|
+
}
|
|
2957
|
+
var LicenseService = class {
|
|
2958
|
+
constructor() {
|
|
2959
|
+
this.cache = null;
|
|
2960
|
+
this.refreshPromise = null;
|
|
2961
|
+
this.verificationPromise = null;
|
|
2962
|
+
}
|
|
2963
|
+
async getCachedStatus() {
|
|
2964
|
+
return this.getStatus();
|
|
2965
|
+
}
|
|
2966
|
+
async getStatus(options = {}) {
|
|
2967
|
+
const forceRefresh = Boolean(options.forceRefresh);
|
|
2968
|
+
const secret = await getLicenseSecret();
|
|
2969
|
+
const stored = await getStoredLicense();
|
|
2970
|
+
if (!stored?.licenseKey) {
|
|
2971
|
+
const status = toLicenseStatus(null);
|
|
2972
|
+
this.cache = status;
|
|
2973
|
+
return status;
|
|
2974
|
+
}
|
|
2975
|
+
const now = Date.now();
|
|
2976
|
+
const nextCheckTs = parseTimestamp(stored.nextCheckAt);
|
|
2977
|
+
const lastVerifiedTs = parseTimestamp(stored.lastVerifiedAt);
|
|
2978
|
+
const graceStale = (stored.status === "grace" || stored.status === "error") && (lastVerifiedTs === null || lastVerifiedTs + GRACE_MAX_AGE_MS < now);
|
|
2979
|
+
let workingStored = { ...stored };
|
|
2980
|
+
const signatureValid = isSignatureValid(workingStored, secret);
|
|
2981
|
+
let cappedNextCheckTs = nextCheckTs;
|
|
2982
|
+
if (nextCheckTs !== null && nextCheckTs > now + MAX_NEXT_CHECK_MS) {
|
|
2983
|
+
cappedNextCheckTs = now;
|
|
2984
|
+
const updated = {
|
|
2985
|
+
...workingStored,
|
|
2986
|
+
nextCheckAt: new Date(cappedNextCheckTs).toISOString()
|
|
2987
|
+
};
|
|
2988
|
+
const signed = withSignature(updated, secret);
|
|
2989
|
+
workingStored = signed;
|
|
2990
|
+
await persistLicense(signed);
|
|
2991
|
+
}
|
|
2992
|
+
const tampered = Boolean(workingStored.licenseKey) && !signatureValid;
|
|
2993
|
+
const cached = tampered ? toLicenseStatus(null, {
|
|
2994
|
+
state: "verifying",
|
|
2995
|
+
message: "License data changed, rechecking.",
|
|
2996
|
+
error: "Untrusted license data."
|
|
2997
|
+
}) : toLicenseStatus(workingStored);
|
|
2998
|
+
this.cache = cached;
|
|
2999
|
+
const shouldRefresh = forceRefresh || graceStale || tampered || cappedNextCheckTs === null || cappedNextCheckTs <= now;
|
|
3000
|
+
if (!shouldRefresh) {
|
|
3001
|
+
return cached;
|
|
3002
|
+
}
|
|
3003
|
+
if (this.verificationPromise && !forceRefresh) {
|
|
3004
|
+
return this.verificationPromise;
|
|
3005
|
+
}
|
|
3006
|
+
const verify = async () => {
|
|
3007
|
+
try {
|
|
3008
|
+
const result = await requestGumroadVerify(workingStored.licenseKey, "refresh");
|
|
3009
|
+
ensureWithinUseLimit(result);
|
|
3010
|
+
const normalized = normalizeVerificationResult(result);
|
|
3011
|
+
const updated = {
|
|
3012
|
+
licenseKey: workingStored.licenseKey,
|
|
3013
|
+
purchaseEmail: normalized.email,
|
|
3014
|
+
lastVerifiedAt: nowIso(),
|
|
3015
|
+
nextCheckAt: new Date(Date.now() + LICENSE_REFRESH_INTERVAL_MS).toISOString(),
|
|
3016
|
+
status: "active",
|
|
3017
|
+
lastVerificationError: null
|
|
3018
|
+
};
|
|
3019
|
+
const signed = withSignature(updated, secret);
|
|
3020
|
+
await persistLicense(signed);
|
|
3021
|
+
const status = toLicenseStatus(signed);
|
|
3022
|
+
this.cache = status;
|
|
3023
|
+
return status;
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
const message = error instanceof Error ? error.message : "License verification failed.";
|
|
3026
|
+
if (error instanceof LicenseError && error.code === "network") {
|
|
3027
|
+
const fallbackStored2 = withSignature(
|
|
3028
|
+
{
|
|
3029
|
+
...workingStored,
|
|
3030
|
+
status: tampered ? "inactive" : workingStored.status === "inactive" ? "inactive" : "grace",
|
|
3031
|
+
lastVerificationError: message,
|
|
3032
|
+
nextCheckAt: new Date(Date.now() + LICENSE_REFRESH_INTERVAL_MS).toISOString()
|
|
3033
|
+
},
|
|
3034
|
+
secret
|
|
3035
|
+
);
|
|
3036
|
+
await persistLicense(fallbackStored2);
|
|
3037
|
+
const fallback2 = toLicenseStatus(fallbackStored2, {
|
|
3038
|
+
state: fallbackStored2.status ?? "grace",
|
|
3039
|
+
error: message
|
|
3040
|
+
});
|
|
3041
|
+
this.cache = fallback2;
|
|
3042
|
+
return fallback2;
|
|
3043
|
+
}
|
|
3044
|
+
const fallbackStored = withSignature({
|
|
3045
|
+
licenseKey: workingStored.licenseKey,
|
|
3046
|
+
purchaseEmail: workingStored.purchaseEmail ?? null,
|
|
3047
|
+
lastVerifiedAt: workingStored.lastVerifiedAt ?? null,
|
|
3048
|
+
nextCheckAt: null,
|
|
3049
|
+
status: "inactive",
|
|
3050
|
+
lastVerificationError: message
|
|
3051
|
+
}, secret);
|
|
3052
|
+
await persistLicense(fallbackStored);
|
|
3053
|
+
const fallback = toLicenseStatus(fallbackStored, { error: message });
|
|
3054
|
+
this.cache = fallback;
|
|
3055
|
+
return fallback;
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
this.verificationPromise = verify();
|
|
3059
|
+
try {
|
|
3060
|
+
return await this.verificationPromise;
|
|
3061
|
+
} finally {
|
|
3062
|
+
this.verificationPromise = null;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async activate(licenseKey) {
|
|
3066
|
+
const trimmed = licenseKey.trim();
|
|
3067
|
+
if (!trimmed) {
|
|
3068
|
+
throw new Error("License key is required.");
|
|
3069
|
+
}
|
|
3070
|
+
const secret = await getLicenseSecret();
|
|
3071
|
+
const digest = import_node_crypto2.default.createHash("sha256").update(trimmed).digest("hex");
|
|
3072
|
+
const result = await requestGumroadVerify(trimmed, "activation");
|
|
3073
|
+
ensureWithinUseLimit(result);
|
|
3074
|
+
const normalized = normalizeVerificationResult(result);
|
|
3075
|
+
const stored = {
|
|
3076
|
+
licenseKey: trimmed,
|
|
3077
|
+
purchaseEmail: normalized.email,
|
|
3078
|
+
lastVerifiedAt: nowIso(),
|
|
3079
|
+
nextCheckAt: new Date(Date.now() + LICENSE_REFRESH_INTERVAL_MS).toISOString(),
|
|
3080
|
+
status: "active",
|
|
3081
|
+
lastVerificationError: null
|
|
3082
|
+
};
|
|
3083
|
+
const signed = withSignature(stored, secret);
|
|
3084
|
+
await persistLicense(signed);
|
|
3085
|
+
const status = toLicenseStatus(signed, {
|
|
3086
|
+
message: `License verified (${digest.slice(0, 8)}).`
|
|
3087
|
+
});
|
|
3088
|
+
this.cache = status;
|
|
3089
|
+
return status;
|
|
3090
|
+
}
|
|
3091
|
+
applyProfileCount(status, profileCount) {
|
|
3092
|
+
const profilesRemaining = resolveProfilesRemaining(status.profileLimit, profileCount);
|
|
3093
|
+
return {
|
|
3094
|
+
...status,
|
|
3095
|
+
profilesRemaining
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
refreshStatusInBackground(options = {}) {
|
|
3099
|
+
if (this.refreshPromise) {
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
const forceRefresh = Boolean(options.force);
|
|
3103
|
+
this.refreshPromise = (async () => {
|
|
3104
|
+
try {
|
|
3105
|
+
await this.getStatus({ forceRefresh });
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
console.warn("Background license refresh failed:", error);
|
|
3108
|
+
} finally {
|
|
3109
|
+
this.refreshPromise = null;
|
|
3110
|
+
}
|
|
3111
|
+
})();
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
var licenseService = new LicenseService();
|
|
3115
|
+
|
|
3116
|
+
// ../../lib/license-guard.ts
|
|
3117
|
+
async function assertProfileCreationAllowed(profileManager) {
|
|
3118
|
+
const manager = profileManager ?? new ProfileManager();
|
|
3119
|
+
await manager.initialize();
|
|
3120
|
+
const license = await licenseService.getStatus();
|
|
3121
|
+
const profiles = await manager.listProfiles();
|
|
3122
|
+
const licenseWithCounts = licenseService.applyProfileCount(license, profiles.length);
|
|
3123
|
+
if (typeof licenseWithCounts.profileLimit === "number" && licenseWithCounts.profilesRemaining !== null && licenseWithCounts.profilesRemaining <= 0) {
|
|
3124
|
+
throw new Error("CodexUse Free supports up to 2 profiles. Upgrade to CodexUse Pro for unlimited profiles.");
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/codex-cli.ts
|
|
3129
|
+
var import_node_child_process = require("child_process");
|
|
3130
|
+
var import_node_fs4 = require("fs");
|
|
3131
|
+
var import_node_path4 = __toESM(require("path"));
|
|
3132
|
+
var ENV_HINTS = ["CODEX_BINARY", "CODEX_CLI_PATH", "CODEX_PATH"];
|
|
3133
|
+
var NODE_MODULE_CANDIDATES = [
|
|
3134
|
+
["@openai", "codex"],
|
|
3135
|
+
["@openai", "codex-alpha"]
|
|
3136
|
+
];
|
|
3137
|
+
function fileExists(candidate) {
|
|
3138
|
+
if (!candidate) return null;
|
|
3139
|
+
const resolved = import_node_path4.default.resolve(candidate);
|
|
3140
|
+
try {
|
|
3141
|
+
const stat = (0, import_node_fs4.statSync)(resolved);
|
|
3142
|
+
if (stat.isFile()) return resolved;
|
|
3143
|
+
} catch {
|
|
3144
|
+
return null;
|
|
3145
|
+
}
|
|
3146
|
+
return null;
|
|
3147
|
+
}
|
|
3148
|
+
function resolveFromEnv() {
|
|
3149
|
+
for (const key of ENV_HINTS) {
|
|
3150
|
+
const value = process.env[key];
|
|
3151
|
+
if (!value) continue;
|
|
3152
|
+
const resolved = fileExists(value);
|
|
3153
|
+
if (resolved) return resolved;
|
|
3154
|
+
}
|
|
3155
|
+
return null;
|
|
3156
|
+
}
|
|
3157
|
+
function resolveFromAppResources() {
|
|
3158
|
+
const roots = [
|
|
3159
|
+
import_node_path4.default.resolve(__dirname, ".."),
|
|
3160
|
+
import_node_path4.default.resolve(__dirname, "..", "..")
|
|
3161
|
+
];
|
|
3162
|
+
for (const root of roots) {
|
|
3163
|
+
const base = import_node_path4.default.join(root, "app.asar.unpacked", "node_modules");
|
|
3164
|
+
for (const segments of NODE_MODULE_CANDIDATES) {
|
|
3165
|
+
const candidate = fileExists(import_node_path4.default.join(base, ...segments, "bin", "codex"));
|
|
3166
|
+
if (candidate) return candidate;
|
|
3167
|
+
const jsCandidate = fileExists(import_node_path4.default.join(base, ...segments, "bin", "codex.js"));
|
|
3168
|
+
if (jsCandidate) return jsCandidate;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
function resolveFromNodeModules() {
|
|
3174
|
+
let current = import_node_path4.default.resolve(__dirname, "..");
|
|
3175
|
+
let last = "";
|
|
3176
|
+
while (current !== last) {
|
|
3177
|
+
for (const segments of NODE_MODULE_CANDIDATES) {
|
|
3178
|
+
const candidate = fileExists(import_node_path4.default.join(current, "node_modules", ...segments, "bin", "codex"));
|
|
3179
|
+
if (candidate) return candidate;
|
|
3180
|
+
const jsCandidate = fileExists(import_node_path4.default.join(current, "node_modules", ...segments, "bin", "codex.js"));
|
|
3181
|
+
if (jsCandidate) return jsCandidate;
|
|
3182
|
+
}
|
|
3183
|
+
last = current;
|
|
3184
|
+
current = import_node_path4.default.dirname(current);
|
|
3185
|
+
}
|
|
3186
|
+
return null;
|
|
3187
|
+
}
|
|
3188
|
+
function resolveFromPath() {
|
|
3189
|
+
const pathValue = process.env.PATH ?? "";
|
|
3190
|
+
const entries = pathValue.split(import_node_path4.default.delimiter).filter(Boolean);
|
|
3191
|
+
const names = process.platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
|
|
3192
|
+
for (const entry of entries) {
|
|
3193
|
+
for (const name of names) {
|
|
3194
|
+
const candidate = fileExists(import_node_path4.default.join(entry, name));
|
|
3195
|
+
if (candidate) return candidate;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
function resolveCodexBinary() {
|
|
3201
|
+
return resolveFromEnv() || resolveFromAppResources() || resolveFromNodeModules() || resolveFromPath();
|
|
3202
|
+
}
|
|
3203
|
+
function requireCodexBinary(context) {
|
|
3204
|
+
const resolved = resolveCodexBinary();
|
|
3205
|
+
if (resolved) return resolved;
|
|
3206
|
+
const hint = "Install Codex CLI (npm i -g @openai/codex) or set CODEX_BINARY.";
|
|
3207
|
+
const message = context ? `${context} ${hint}` : hint;
|
|
3208
|
+
throw new Error(message);
|
|
3209
|
+
}
|
|
3210
|
+
function buildCodexCommand(codexPath, args) {
|
|
3211
|
+
const normalized = codexPath.toLowerCase();
|
|
3212
|
+
const isJs = normalized.endsWith(".js");
|
|
3213
|
+
if (isJs) {
|
|
3214
|
+
return { command: process.execPath, args: [codexPath, ...args], shell: false };
|
|
3215
|
+
}
|
|
3216
|
+
const useShell = process.platform === "win32";
|
|
3217
|
+
return { command: codexPath, args, shell: useShell };
|
|
3218
|
+
}
|
|
3219
|
+
async function runCodexLogin() {
|
|
3220
|
+
const codexPath = requireCodexBinary("Codex CLI is required to login.");
|
|
3221
|
+
const { command, args, shell } = buildCodexCommand(codexPath, ["login"]);
|
|
3222
|
+
const child = (0, import_node_child_process.spawn)(command, args, {
|
|
3223
|
+
stdio: "inherit",
|
|
3224
|
+
env: process.env,
|
|
3225
|
+
shell
|
|
3226
|
+
});
|
|
3227
|
+
const exitCode = await new Promise((resolve) => {
|
|
3228
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
3229
|
+
});
|
|
3230
|
+
if (exitCode !== 0) {
|
|
3231
|
+
throw new Error(`Codex CLI login failed (exit code ${exitCode}).`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
// src/index.ts
|
|
3236
|
+
var VERSION = true ? "2.1.0" : "0.0.0";
|
|
3237
|
+
function printHelp() {
|
|
3238
|
+
console.log(`CodexUse CLI v${VERSION}
|
|
3239
|
+
|
|
3240
|
+
Usage:
|
|
3241
|
+
codexuse profile list
|
|
3242
|
+
codexuse profile current
|
|
3243
|
+
codexuse profile add <name> [--skip-login]
|
|
3244
|
+
codexuse profile refresh <name> [--skip-login]
|
|
3245
|
+
codexuse profile switch <name>
|
|
3246
|
+
codexuse profile delete <name>
|
|
3247
|
+
codexuse profile rename <old> <new>
|
|
3248
|
+
|
|
3249
|
+
codexuse license status [--refresh]
|
|
3250
|
+
codexuse license activate <license-key>
|
|
3251
|
+
|
|
3252
|
+
Flags:
|
|
3253
|
+
-h, --help Show help
|
|
3254
|
+
-v, --version Show version
|
|
3255
|
+
`);
|
|
3256
|
+
}
|
|
3257
|
+
function hasFlag(args, flag) {
|
|
3258
|
+
return args.includes(flag);
|
|
3259
|
+
}
|
|
3260
|
+
function stripFlags(args) {
|
|
3261
|
+
return args.filter((arg) => !arg.startsWith("-"));
|
|
3262
|
+
}
|
|
3263
|
+
function formatProfileLabel(name, displayName) {
|
|
3264
|
+
if (displayName && displayName.trim() && displayName !== name) {
|
|
3265
|
+
return `${displayName} (${name})`;
|
|
3266
|
+
}
|
|
3267
|
+
return name;
|
|
3268
|
+
}
|
|
3269
|
+
async function handleProfile(args) {
|
|
3270
|
+
const flags = args.filter((arg) => arg.startsWith("-"));
|
|
3271
|
+
const params = stripFlags(args);
|
|
3272
|
+
const sub = params[0];
|
|
3273
|
+
if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
|
|
3274
|
+
printHelp();
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
const manager = new ProfileManager();
|
|
3278
|
+
switch (sub) {
|
|
3279
|
+
case "list": {
|
|
3280
|
+
const profiles = await manager.listProfiles();
|
|
3281
|
+
const current = await manager.getCurrentProfile();
|
|
3282
|
+
if (!profiles.length) {
|
|
3283
|
+
console.log("No profiles found.");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
for (const profile of profiles) {
|
|
3287
|
+
const marker = current.name === profile.name ? "*" : " ";
|
|
3288
|
+
const label = formatProfileLabel(profile.name, profile.displayName);
|
|
3289
|
+
console.log(`${marker} ${label}`);
|
|
3290
|
+
}
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
case "current": {
|
|
3294
|
+
const current = await manager.getCurrentProfile();
|
|
3295
|
+
if (!current.name) {
|
|
3296
|
+
console.log("No active profile.");
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
console.log(current.name);
|
|
3300
|
+
return;
|
|
3301
|
+
}
|
|
3302
|
+
case "add": {
|
|
3303
|
+
const name = params[1];
|
|
3304
|
+
if (!name) {
|
|
3305
|
+
throw new Error("Profile name is required.");
|
|
3306
|
+
}
|
|
3307
|
+
await assertProfileCreationAllowed(manager);
|
|
3308
|
+
if (!hasFlag(flags, "--skip-login")) {
|
|
3309
|
+
await runCodexLogin();
|
|
3310
|
+
}
|
|
3311
|
+
const profile = await manager.createProfile(name);
|
|
3312
|
+
const label = profile ? formatProfileLabel(profile.name, profile.displayName) : name;
|
|
3313
|
+
console.log(`Profile created: ${label}`);
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
case "refresh": {
|
|
3317
|
+
const name = params[1];
|
|
3318
|
+
if (!name) {
|
|
3319
|
+
throw new Error("Profile name is required.");
|
|
3320
|
+
}
|
|
3321
|
+
if (!hasFlag(flags, "--skip-login")) {
|
|
3322
|
+
await runCodexLogin();
|
|
3323
|
+
}
|
|
3324
|
+
const profile = await manager.refreshProfileAuth(name);
|
|
3325
|
+
const label = formatProfileLabel(profile.name, profile.displayName);
|
|
3326
|
+
console.log(`Profile refreshed: ${label}`);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
case "switch": {
|
|
3330
|
+
const name = params[1];
|
|
3331
|
+
if (!name) {
|
|
3332
|
+
throw new Error("Profile name is required.");
|
|
3333
|
+
}
|
|
3334
|
+
await manager.switchToProfile(name);
|
|
3335
|
+
console.log(`Switched to profile: ${name}`);
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
case "delete": {
|
|
3339
|
+
const name = params[1];
|
|
3340
|
+
if (!name) {
|
|
3341
|
+
throw new Error("Profile name is required.");
|
|
3342
|
+
}
|
|
3343
|
+
await manager.deleteProfile(name);
|
|
3344
|
+
console.log(`Profile deleted: ${name}`);
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
case "rename": {
|
|
3348
|
+
const from = params[1];
|
|
3349
|
+
const to = params[2];
|
|
3350
|
+
if (!from || !to) {
|
|
3351
|
+
throw new Error("Old and new profile names are required.");
|
|
3352
|
+
}
|
|
3353
|
+
await manager.renameProfile(from, to);
|
|
3354
|
+
console.log(`Profile renamed: ${from} -> ${to}`);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
default:
|
|
3358
|
+
printHelp();
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
async function handleLicense(args) {
|
|
3363
|
+
const flags = args.filter((arg) => arg.startsWith("-"));
|
|
3364
|
+
const params = stripFlags(args);
|
|
3365
|
+
const sub = params[0];
|
|
3366
|
+
if (!sub || hasFlag(flags, "--help") || hasFlag(flags, "-h")) {
|
|
3367
|
+
printHelp();
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
switch (sub) {
|
|
3371
|
+
case "status": {
|
|
3372
|
+
const forceRefresh = hasFlag(flags, "--refresh");
|
|
3373
|
+
const status = await licenseService.getStatus({ forceRefresh });
|
|
3374
|
+
console.log(`Tier: ${status.tier}`);
|
|
3375
|
+
console.log(`State: ${status.state}`);
|
|
3376
|
+
if (status.profileLimit !== null) {
|
|
3377
|
+
console.log(`Profile limit: ${status.profileLimit}`);
|
|
3378
|
+
}
|
|
3379
|
+
if (status.profilesRemaining !== null) {
|
|
3380
|
+
console.log(`Profiles remaining: ${status.profilesRemaining}`);
|
|
3381
|
+
}
|
|
3382
|
+
if (status.purchaseEmail) {
|
|
3383
|
+
console.log(`Email: ${status.purchaseEmail}`);
|
|
3384
|
+
}
|
|
3385
|
+
if (status.maskedLicenseKey) {
|
|
3386
|
+
console.log(`License: ${status.maskedLicenseKey}`);
|
|
3387
|
+
}
|
|
3388
|
+
if (status.message) {
|
|
3389
|
+
console.log(`Message: ${status.message}`);
|
|
3390
|
+
}
|
|
3391
|
+
if (status.error) {
|
|
3392
|
+
console.log(`Error: ${status.error}`);
|
|
3393
|
+
}
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
case "activate": {
|
|
3397
|
+
const key = params[1];
|
|
3398
|
+
if (!key) {
|
|
3399
|
+
throw new Error("License key is required.");
|
|
3400
|
+
}
|
|
3401
|
+
const status = await licenseService.activate(key);
|
|
3402
|
+
console.log(status.message ?? "License activated.");
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
default:
|
|
3406
|
+
printHelp();
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
async function main() {
|
|
3411
|
+
const args = process.argv.slice(2);
|
|
3412
|
+
if (args.length === 0 || hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
3413
|
+
printHelp();
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
if (hasFlag(args, "--version") || hasFlag(args, "-v")) {
|
|
3417
|
+
console.log(VERSION);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const command = args[0];
|
|
3421
|
+
const rest = args.slice(1);
|
|
3422
|
+
switch (command) {
|
|
3423
|
+
case "profile":
|
|
3424
|
+
await handleProfile(rest);
|
|
3425
|
+
return;
|
|
3426
|
+
case "license":
|
|
3427
|
+
await handleLicense(rest);
|
|
3428
|
+
return;
|
|
3429
|
+
default:
|
|
3430
|
+
printHelp();
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
main().catch((error) => {
|
|
3435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3436
|
+
console.error(message);
|
|
3437
|
+
process.exitCode = 1;
|
|
3438
|
+
});
|
|
3439
|
+
//# sourceMappingURL=index.js.map
|