sneakoscope 0.9.2 → 0.9.4
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 +27 -0
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +394 -11
- package/src/cli/main.mjs +68 -5
- package/src/core/fsx.mjs +1 -1
package/README.md
CHANGED
|
@@ -191,6 +191,33 @@ Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/
|
|
|
191
191
|
|
|
192
192
|
If codex-lb provider auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
|
|
193
193
|
|
|
194
|
+
### Switching back to ChatGPT OAuth (releasing codex-lb)
|
|
195
|
+
|
|
196
|
+
If you want to hand control back to your official ChatGPT account login after codex-lb has been reconciled, use `sks codex-lb release`:
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
sks codex-lb release
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
This restores `~/.codex/auth.chatgpt-backup.json` (written by the 0.9.3 auto-reconcile) to `~/.codex/auth.json` and unsets `model_provider = "codex-lb"` so Codex CLI/App falls back to ChatGPT OAuth. To re-engage codex-lb afterward, run `sks codex-lb repair`.
|
|
203
|
+
|
|
204
|
+
Flags:
|
|
205
|
+
|
|
206
|
+
- `--keep-provider` — restore `auth.json` only; leave `model_provider = "codex-lb"` selected (advanced use).
|
|
207
|
+
- `--delete-backup` — remove `~/.codex/auth.chatgpt-backup.json` after a successful restore. Default is to keep it so a subsequent re-reconcile still has a source backup.
|
|
208
|
+
- `--force` — restore even when the current `auth.json` does not look like the codex-lb apikey shape (e.g. if you hand-edited it after reconcile).
|
|
209
|
+
- `--json` — machine-readable output with `status` ∈ {`released`, `no_backup`, `already_chatgpt`, `auth_in_use`, `failed`} plus `auth_path`, `backup_path`, `provider_unselected`, `backup_removed`.
|
|
210
|
+
|
|
211
|
+
`sks codex-lb status` reports whether a ChatGPT OAuth backup is present and shows the `sks codex-lb release` hint when applicable. `sks doctor` surfaces the same hint.
|
|
212
|
+
|
|
213
|
+
If you only want to stop routing through codex-lb without touching `auth.json`, use the lighter `sks codex-lb unselect` instead:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
sks codex-lb unselect
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This flips `model_provider` away from `codex-lb` in the top-level Codex App config while leaving your `sks-codex-lb.env` and `auth.json` untouched, so you can re-engage codex-lb later with `sks codex-lb repair` without re-running setup.
|
|
220
|
+
|
|
194
221
|
### MAD tmux Launch
|
|
195
222
|
|
|
196
223
|
```sh
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.4",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -87,6 +87,14 @@ async function reportPostinstallCodexLbAuth() {
|
|
|
87
87
|
else if (codexLbAuth.status === 'missing_env_key') console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
|
|
88
88
|
else if (codexLbAuth.status === 'missing_base_url') console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
|
|
89
89
|
else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
|
|
90
|
+
const reconcile = codexLbAuth.auth_reconcile;
|
|
91
|
+
if (reconcile?.status === 'reconciled') {
|
|
92
|
+
console.log(`codex-lb auth: resolved ChatGPT OAuth conflict by switching auth.json to apikey mode (OAuth backup at ${reconcile.backup_path}). Set SKS_CODEX_LB_NO_AUTH_RECONCILE=1 to opt out.`);
|
|
93
|
+
} else if (reconcile?.status === 'backup_only') {
|
|
94
|
+
console.log(`codex-lb auth: detected ChatGPT OAuth tokens in auth.json. OAuth backup written to ${reconcile.backup_path}; auth.json left untouched because SKS_CODEX_LB_NO_AUTH_RECONCILE=1.`);
|
|
95
|
+
} else if (reconcile?.status === 'failed') {
|
|
96
|
+
console.log(`codex-lb auth: ChatGPT OAuth reconciliation could not complete (${reconcile.reason || 'unknown'}${reconcile.error ? `: ${reconcile.error}` : ''}). Run \`sks codex-lb repair\` to retry.`);
|
|
97
|
+
}
|
|
90
98
|
return codexLbAuth;
|
|
91
99
|
}
|
|
92
100
|
|
|
@@ -160,26 +168,65 @@ function codexAuthPath(home = process.env.HOME || os.homedir()) {
|
|
|
160
168
|
return path.join(home, '.codex', 'auth.json');
|
|
161
169
|
}
|
|
162
170
|
|
|
171
|
+
function codexAuthChatgptBackupPath(home = process.env.HOME || os.homedir()) {
|
|
172
|
+
return path.join(home, '.codex', 'auth.chatgpt-backup.json');
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME || os.homedir()) {
|
|
164
176
|
const configPath = codexLbConfigPath(home);
|
|
165
177
|
const envPath = codexLbEnvPath(home);
|
|
178
|
+
const authPath = codexAuthPath(home);
|
|
166
179
|
const config = await readText(configPath, '');
|
|
167
180
|
const envText = await readText(envPath, '');
|
|
168
|
-
|
|
181
|
+
const authExisted = await exists(authPath);
|
|
182
|
+
const authText = authExisted ? await readText(authPath, '') : '';
|
|
183
|
+
const envKey = parseCodexLbEnvKey(envText);
|
|
184
|
+
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
169
185
|
const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText);
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
// Snapshot any codex-lb-related state so the upgrade-time bootstrap can't silently undo it.
|
|
187
|
+
if (!envKey && !providerConfigured && !authExisted) return null;
|
|
188
|
+
return {
|
|
189
|
+
config_path: configPath,
|
|
190
|
+
env_path: envPath,
|
|
191
|
+
auth_path: authPath,
|
|
192
|
+
base_url: baseUrl ? normalizeCodexLbBaseUrl(baseUrl) : null,
|
|
193
|
+
auth_existed: authExisted,
|
|
194
|
+
auth_text: authText
|
|
195
|
+
};
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
|
|
175
|
-
if (!snapshot
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
if (!snapshot) return { status: 'skipped', reason: 'no_snapshot' };
|
|
200
|
+
let configRestored = false;
|
|
201
|
+
if (snapshot.base_url) {
|
|
202
|
+
const current = await readText(snapshot.config_path, '');
|
|
203
|
+
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
|
|
204
|
+
const alreadyOk = next === ensureTrailingNewline(current) && codexLbProviderBaseUrl(current);
|
|
205
|
+
if (!alreadyOk) {
|
|
206
|
+
await writeTextAtomic(snapshot.config_path, next);
|
|
207
|
+
configRestored = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Restore auth.json only if bootstrap accidentally wiped or emptied a pre-existing auth.json.
|
|
211
|
+
// We do NOT clobber a legitimately rewritten auth.json; we just heal the disappearing-auth regression.
|
|
212
|
+
let authRestored = false;
|
|
213
|
+
if (snapshot.auth_existed && snapshot.auth_text && snapshot.auth_text.trim()) {
|
|
214
|
+
const currentAuthExists = await exists(snapshot.auth_path);
|
|
215
|
+
const currentAuthText = currentAuthExists ? await readText(snapshot.auth_path, '') : '';
|
|
216
|
+
if (!currentAuthExists || !currentAuthText.trim()) {
|
|
217
|
+
await ensureDir(path.dirname(snapshot.auth_path));
|
|
218
|
+
await writeTextAtomic(snapshot.auth_path, snapshot.auth_text);
|
|
219
|
+
await fsp.chmod(snapshot.auth_path, 0o600).catch(() => {});
|
|
220
|
+
authRestored = true;
|
|
221
|
+
}
|
|
180
222
|
}
|
|
181
|
-
|
|
182
|
-
|
|
223
|
+
return {
|
|
224
|
+
status: configRestored || authRestored ? 'restored' : 'present',
|
|
225
|
+
config_path: snapshot.config_path,
|
|
226
|
+
auth_path: snapshot.auth_path,
|
|
227
|
+
config_restored: configRestored,
|
|
228
|
+
auth_restored: authRestored
|
|
229
|
+
};
|
|
183
230
|
}
|
|
184
231
|
|
|
185
232
|
export function normalizeCodexLbBaseUrl(input = '') {
|
|
@@ -413,6 +460,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
413
460
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
414
461
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
415
462
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
463
|
+
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
416
464
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
417
465
|
return {
|
|
418
466
|
ok,
|
|
@@ -423,6 +471,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
423
471
|
config_repaired: configRepaired,
|
|
424
472
|
legacy_auth_migrated: legacyAuthMigrated,
|
|
425
473
|
legacy_auth_path: legacyAuthPath,
|
|
474
|
+
auth_reconcile: authReconcile,
|
|
426
475
|
codex_lb: status,
|
|
427
476
|
codex_environment: codexEnvironment,
|
|
428
477
|
codex_login: codexLogin
|
|
@@ -441,6 +490,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
441
490
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
442
491
|
const apiKey = parseCodexLbEnvKey(await readText(status.env_path, ''));
|
|
443
492
|
const codexLogin = await maybeSyncCodexLbSharedLogin(apiKey, { ...opts, home: opts.home || process.env.HOME || os.homedir(), force: true });
|
|
493
|
+
const authReconcile = await reconcileCodexLbAuthConflict({ ...opts, status }).catch((err) => ({ status: 'failed', reason: 'exception', error: err.message }));
|
|
444
494
|
const ok = Boolean(codexEnvironment.ok && codexLogin.ok);
|
|
445
495
|
return {
|
|
446
496
|
ok,
|
|
@@ -451,6 +501,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
451
501
|
codex_lb: status,
|
|
452
502
|
codex_environment: codexEnvironment,
|
|
453
503
|
codex_login: codexLogin,
|
|
504
|
+
auth_reconcile: authReconcile,
|
|
454
505
|
error: codexEnvironment.error || codexLogin.error || null
|
|
455
506
|
};
|
|
456
507
|
}
|
|
@@ -470,6 +521,253 @@ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
|
|
|
470
521
|
return { ok: true, status: 'migrated_login_cache', auth_path: authPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
|
|
471
522
|
}
|
|
472
523
|
|
|
524
|
+
// Detects a real ChatGPT OAuth token blob in auth.json.
|
|
525
|
+
// A bare {"auth_mode":"browser"} marker is NOT considered an OAuth token blob — we preserve it.
|
|
526
|
+
function hasChatgptOAuthTokens(text = '') {
|
|
527
|
+
try {
|
|
528
|
+
const parsed = JSON.parse(String(text || ''));
|
|
529
|
+
if (!parsed || typeof parsed !== 'object') return false;
|
|
530
|
+
const authMode = String(parsed.auth_mode || parsed.authMode || parsed.mode || '').toLowerCase();
|
|
531
|
+
const tokens = parsed.tokens || parsed.oauth || parsed.oauth_tokens;
|
|
532
|
+
if (tokens && typeof tokens === 'object') {
|
|
533
|
+
if (tokens.id_token || tokens.access_token || tokens.refresh_token) return true;
|
|
534
|
+
}
|
|
535
|
+
if (authMode && /chatgpt|oauth|browser/.test(authMode)) {
|
|
536
|
+
// Only treat as an OAuth blob when real tokens or a refresh metadata trail are also present.
|
|
537
|
+
if (parsed.last_refresh || parsed.expires_at || parsed.refresh_token || parsed.access_token || parsed.id_token) return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseCodexAuthApiKey(text = '') {
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(String(text || ''));
|
|
548
|
+
const key = parsed?.key || parsed?.api_key || parsed?.apiKey || parsed?.openai_api_key || parsed?.OPENAI_API_KEY;
|
|
549
|
+
return typeof key === 'string' ? key.trim() : '';
|
|
550
|
+
} catch {
|
|
551
|
+
return '';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// When codex-lb is selected with env_key auth AND auth.json also carries a real ChatGPT OAuth
|
|
556
|
+
// token blob, Codex CLI/App can pick the OAuth bearer over the env_key bearer and fail against
|
|
557
|
+
// the load balancer. We back the OAuth blob up to ~/.codex/auth.chatgpt-backup.json and replace
|
|
558
|
+
// auth.json with an apikey-mode payload that matches the stored CODEX_LB_API_KEY.
|
|
559
|
+
// Opt out with SKS_CODEX_LB_NO_AUTH_RECONCILE=1 (the backup is still produced so nothing is lost).
|
|
560
|
+
export async function reconcileCodexLbAuthConflict(opts = {}) {
|
|
561
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
562
|
+
const status = opts.status || await codexLbStatus({ ...opts, home });
|
|
563
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
564
|
+
const backupPath = opts.backupPath || codexAuthChatgptBackupPath(home);
|
|
565
|
+
if (!status.env_key_configured || !status.base_url) {
|
|
566
|
+
return { status: 'skipped', reason: 'codex_lb_not_ready', auth_path: authPath };
|
|
567
|
+
}
|
|
568
|
+
if (!(await exists(authPath))) {
|
|
569
|
+
return { status: 'skipped', reason: 'auth_missing', auth_path: authPath };
|
|
570
|
+
}
|
|
571
|
+
const authText = await readText(authPath, '');
|
|
572
|
+
if (!authText.trim()) {
|
|
573
|
+
return { status: 'skipped', reason: 'auth_empty', auth_path: authPath };
|
|
574
|
+
}
|
|
575
|
+
if (!hasChatgptOAuthTokens(authText)) {
|
|
576
|
+
return { status: 'no_oauth_conflict', auth_path: authPath };
|
|
577
|
+
}
|
|
578
|
+
const envText = await readText(status.env_path, '');
|
|
579
|
+
const apiKey = parseCodexLbEnvKey(envText);
|
|
580
|
+
if (!apiKey) {
|
|
581
|
+
return { status: 'skipped', reason: 'missing_env_key', auth_path: authPath };
|
|
582
|
+
}
|
|
583
|
+
// Always back up the OAuth blob — even if reconciliation is opted out — so the data survives.
|
|
584
|
+
try {
|
|
585
|
+
await ensureDir(path.dirname(backupPath));
|
|
586
|
+
await writeTextAtomic(backupPath, authText);
|
|
587
|
+
await fsp.chmod(backupPath, 0o600).catch(() => {});
|
|
588
|
+
} catch (err) {
|
|
589
|
+
return { status: 'failed', reason: 'backup_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
590
|
+
}
|
|
591
|
+
if (process.env.SKS_CODEX_LB_NO_AUTH_RECONCILE === '1' && !opts.force) {
|
|
592
|
+
return {
|
|
593
|
+
status: 'backup_only',
|
|
594
|
+
reason: 'SKS_CODEX_LB_NO_AUTH_RECONCILE=1',
|
|
595
|
+
auth_path: authPath,
|
|
596
|
+
backup_path: backupPath
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const replacement = `${JSON.stringify({ auth_mode: 'apikey', key: apiKey }, null, 2)}\n`;
|
|
601
|
+
await writeTextAtomic(authPath, replacement);
|
|
602
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return { status: 'failed', reason: 'write_failed', auth_path: authPath, backup_path: backupPath, error: err.message };
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
status: 'reconciled',
|
|
608
|
+
auth_path: authPath,
|
|
609
|
+
backup_path: backupPath
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Expose the ChatGPT OAuth backup path so the CLI can surface it in status / release output.
|
|
614
|
+
export function codexLbChatgptBackupPath(home = process.env.HOME || os.homedir()) {
|
|
615
|
+
return codexAuthChatgptBackupPath(home);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Remove a top-level TOML key (only above the first table header). Returns the original text
|
|
619
|
+
// unchanged when the key isn't present.
|
|
620
|
+
function removeTopLevelTomlString(text, key) {
|
|
621
|
+
const lines = String(text || '').split('\n');
|
|
622
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
623
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
624
|
+
let removed = false;
|
|
625
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
626
|
+
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
|
|
627
|
+
lines.splice(i, 1);
|
|
628
|
+
removed = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!removed) return text;
|
|
632
|
+
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Unselect codex-lb at the top-level model_provider setting. Leaves [model_providers.codex-lb]
|
|
636
|
+
// and the env file alone so the user can re-engage with `sks codex-lb repair`.
|
|
637
|
+
export async function unselectCodexLbProvider(opts = {}) {
|
|
638
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
639
|
+
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
640
|
+
const current = await readText(configPath, '');
|
|
641
|
+
if (!current.trim()) return { status: 'not_selected', reason: 'no_config', config_path: configPath };
|
|
642
|
+
if (!hasTopLevelCodexLbSelected(current)) return { status: 'not_selected', config_path: configPath };
|
|
643
|
+
try {
|
|
644
|
+
const next = ensureTrailingNewline(removeTopLevelTomlString(current, 'model_provider'));
|
|
645
|
+
await writeTextAtomic(configPath, next);
|
|
646
|
+
return { status: 'unselected', config_path: configPath };
|
|
647
|
+
} catch (err) {
|
|
648
|
+
return { status: 'failed', reason: 'write_failed', config_path: configPath, error: err.message };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Reverse of reconcileCodexLbAuthConflict: restore the ChatGPT OAuth blob from the backup file
|
|
653
|
+
// so the user can return to the official ChatGPT account login. Also deselects codex-lb at the
|
|
654
|
+
// model_provider level by default so the restored OAuth blob actually wins; pass keepProvider
|
|
655
|
+
// to skip that.
|
|
656
|
+
//
|
|
657
|
+
// Options:
|
|
658
|
+
// home - HOME override (selftest)
|
|
659
|
+
// keepProvider - leave `model_provider = "codex-lb"` selected (default: deselect)
|
|
660
|
+
// deleteBackup - remove ~/.codex/auth.chatgpt-backup.json after a successful restore
|
|
661
|
+
// (default: false; keeping it makes the next reconcile cycle a no-op clobber risk)
|
|
662
|
+
// force - restore even if the current auth.json shape isn't recognized
|
|
663
|
+
export async function releaseCodexLbAuthHold(opts = {}) {
|
|
664
|
+
const home = opts.home || process.env.HOME || os.homedir();
|
|
665
|
+
const authPath = opts.authPath || codexAuthPath(home);
|
|
666
|
+
const backupPath = opts.backupPath || codexAuthChatgptBackupPath(home);
|
|
667
|
+
const configPath = opts.configPath || codexLbConfigPath(home);
|
|
668
|
+
|
|
669
|
+
const backupExists = await exists(backupPath);
|
|
670
|
+
const backupText = backupExists ? await readText(backupPath, '') : '';
|
|
671
|
+
if (!backupExists || !backupText.trim()) {
|
|
672
|
+
return {
|
|
673
|
+
status: 'no_backup',
|
|
674
|
+
auth_path: authPath,
|
|
675
|
+
backup_path: backupPath,
|
|
676
|
+
provider_unselected: false
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (!hasChatgptOAuthTokens(backupText)) {
|
|
680
|
+
return {
|
|
681
|
+
status: 'no_backup',
|
|
682
|
+
reason: 'backup_not_oauth',
|
|
683
|
+
auth_path: authPath,
|
|
684
|
+
backup_path: backupPath,
|
|
685
|
+
provider_unselected: false
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const currentAuthText = await readText(authPath, '');
|
|
690
|
+
const trimmedCurrent = currentAuthText.trim();
|
|
691
|
+
|
|
692
|
+
// If auth.json already looks like ChatGPT OAuth (user re-logged in some other way), don't
|
|
693
|
+
// clobber it — but still honor the deselect request so the OAuth blob takes effect.
|
|
694
|
+
if (trimmedCurrent && hasChatgptOAuthTokens(currentAuthText) && !opts.force) {
|
|
695
|
+
let providerUnselected = false;
|
|
696
|
+
let providerError = null;
|
|
697
|
+
if (!opts.keepProvider) {
|
|
698
|
+
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
699
|
+
if (unselected.status === 'unselected') providerUnselected = true;
|
|
700
|
+
else if (unselected.status === 'failed') providerError = unselected.error || unselected.reason || 'unselect_failed';
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
status: 'already_chatgpt',
|
|
704
|
+
auth_path: authPath,
|
|
705
|
+
backup_path: backupPath,
|
|
706
|
+
provider_unselected: providerUnselected,
|
|
707
|
+
provider_error: providerError
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Refuse to clobber unfamiliar auth.json shapes unless forced. We expect either an empty file,
|
|
712
|
+
// the apikey shape we wrote during reconcile, or a stray `{"auth_mode":"browser"}` marker.
|
|
713
|
+
if (!opts.force && trimmedCurrent) {
|
|
714
|
+
const looksApikey = /"auth_mode"\s*:\s*"apikey"/.test(currentAuthText) && Boolean(parseCodexAuthApiKey(currentAuthText));
|
|
715
|
+
const looksBrowserMarker = /^\{\s*"auth_mode"\s*:\s*"browser"\s*\}\s*$/.test(currentAuthText);
|
|
716
|
+
if (!looksApikey && !looksBrowserMarker) {
|
|
717
|
+
return {
|
|
718
|
+
status: 'auth_in_use',
|
|
719
|
+
reason: 'unfamiliar_auth_json',
|
|
720
|
+
auth_path: authPath,
|
|
721
|
+
backup_path: backupPath,
|
|
722
|
+
provider_unselected: false
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
await ensureDir(path.dirname(authPath));
|
|
729
|
+
const restored = backupText.endsWith('\n') ? backupText : `${backupText}\n`;
|
|
730
|
+
await writeTextAtomic(authPath, restored);
|
|
731
|
+
await fsp.chmod(authPath, 0o600).catch(() => {});
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return {
|
|
734
|
+
status: 'failed',
|
|
735
|
+
reason: 'restore_failed',
|
|
736
|
+
auth_path: authPath,
|
|
737
|
+
backup_path: backupPath,
|
|
738
|
+
error: err.message,
|
|
739
|
+
provider_unselected: false
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let backupRemoved = false;
|
|
744
|
+
if (opts.deleteBackup) {
|
|
745
|
+
try {
|
|
746
|
+
await fsp.rm(backupPath, { force: true });
|
|
747
|
+
backupRemoved = true;
|
|
748
|
+
} catch {
|
|
749
|
+
// Non-fatal: the restore already landed.
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let providerUnselected = false;
|
|
754
|
+
let providerError = null;
|
|
755
|
+
if (!opts.keepProvider) {
|
|
756
|
+
const unselected = await unselectCodexLbProvider({ ...opts, home, configPath });
|
|
757
|
+
if (unselected.status === 'unselected') providerUnselected = true;
|
|
758
|
+
else if (unselected.status === 'failed') providerError = unselected.error || unselected.reason || 'unselect_failed';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
status: 'released',
|
|
763
|
+
auth_path: authPath,
|
|
764
|
+
backup_path: backupPath,
|
|
765
|
+
backup_removed: backupRemoved,
|
|
766
|
+
provider_unselected: providerUnselected,
|
|
767
|
+
provider_error: providerError
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
473
771
|
export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
474
772
|
if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
|
|
475
773
|
let status = await codexLbStatus(opts);
|
|
@@ -1240,7 +1538,10 @@ export async function selftestCodexLb(tmp) {
|
|
|
1240
1538
|
const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
|
|
1241
1539
|
await ensureDir(codexLbFakeBin);
|
|
1242
1540
|
const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
|
|
1243
|
-
|
|
1541
|
+
// NOTE: printf format uses literal double-quotes inside single-quoted shell strings so the
|
|
1542
|
+
// fake login writes proper JSON in both bash and dash (where `\"` is a non-standard printf
|
|
1543
|
+
// escape that dash emits literally and bash collapses to `"`).
|
|
1544
|
+
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\"auth_mode\":\"apikey\",\"key\":\"%s\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
|
|
1244
1545
|
await fsp.chmod(codexLbFakeCodex, 0o755);
|
|
1245
1546
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
|
|
1246
1547
|
const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}`, SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1' };
|
|
@@ -1353,6 +1654,88 @@ export async function selftestCodexLb(tmp) {
|
|
|
1353
1654
|
const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1354
1655
|
if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"browser"') || codexLbDoctorAuth.includes('sk-test') || !hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
|
|
1355
1656
|
if (!codexLbDoctorConfig.includes('requires_openai_auth = true')) throw new Error('selftest: doctor codex-lb did not restore upstream-required requires_openai_auth');
|
|
1657
|
+
// codex-lb auth: ChatGPT OAuth ↔ codex-lb env_key conflict reconciliation.
|
|
1658
|
+
const oauthAuthJson = JSON.stringify({
|
|
1659
|
+
auth_mode: 'chatgpt',
|
|
1660
|
+
tokens: { id_token: 'oauth-id', access_token: 'oauth-access', refresh_token: 'oauth-refresh' },
|
|
1661
|
+
last_refresh: '2026-01-01T00:00:00Z'
|
|
1662
|
+
});
|
|
1663
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), `${oauthAuthJson}\n`);
|
|
1664
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
|
|
1665
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = true\n');
|
|
1666
|
+
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
1667
|
+
const codexLbReconcileRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1668
|
+
if (codexLbReconcileRepair.code !== 0) throw new Error(`selftest: codex-lb oauth reconcile repair exited ${codexLbReconcileRepair.code}: ${codexLbReconcileRepair.stderr}`);
|
|
1669
|
+
const codexLbReconcileJson = JSON.parse(codexLbReconcileRepair.stdout);
|
|
1670
|
+
const codexLbReconcileAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1671
|
+
const codexLbReconcileBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1672
|
+
if (codexLbReconcileJson.auth_reconcile?.status !== 'reconciled' || !codexLbReconcileAuth.includes('"auth_mode": "apikey"') || !codexLbReconcileAuth.includes('sk-test') || codexLbReconcileAuth.includes('oauth-id') || !codexLbReconcileBackup.includes('oauth-id') || !codexLbReconcileBackup.includes('oauth-refresh')) throw new Error('selftest: codex-lb oauth reconcile did not back up ChatGPT tokens and switch auth.json to apikey mode');
|
|
1673
|
+
// Opt-out path: SKS_CODEX_LB_NO_AUTH_RECONCILE=1 keeps auth.json untouched but still backs up the OAuth blob.
|
|
1674
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), `${oauthAuthJson}\n`);
|
|
1675
|
+
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
1676
|
+
const codexLbReconcileOptOutRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: { ...codexLbEnvForSelftest, SKS_CODEX_LB_NO_AUTH_RECONCILE: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1677
|
+
if (codexLbReconcileOptOutRepair.code !== 0) throw new Error(`selftest: codex-lb oauth reconcile opt-out repair exited ${codexLbReconcileOptOutRepair.code}: ${codexLbReconcileOptOutRepair.stderr}`);
|
|
1678
|
+
const codexLbReconcileOptOutJson = JSON.parse(codexLbReconcileOptOutRepair.stdout);
|
|
1679
|
+
const codexLbReconcileOptOutAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1680
|
+
const codexLbReconcileOptOutBackup = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1681
|
+
if (codexLbReconcileOptOutJson.auth_reconcile?.status !== 'backup_only' || !codexLbReconcileOptOutAuth.includes('oauth-id') || !codexLbReconcileOptOutBackup.includes('oauth-id')) throw new Error('selftest: codex-lb oauth reconcile opt-out should back up but not rewrite auth.json');
|
|
1682
|
+
// codex-lb auth: release flow — restore ChatGPT OAuth from backup so the user can return to
|
|
1683
|
+
// the official ChatGPT account login. Default deselects model_provider; flags control whether
|
|
1684
|
+
// the provider stays selected and whether the backup file is removed after restore.
|
|
1685
|
+
const codexLbReleaseConfig = 'model_provider = "codex-lb"\n\n[model_providers.codex-lb]\nname = "OpenAI"\nbase_url = "https://lb.example.test/backend-api/codex"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = true\n';
|
|
1686
|
+
const codexLbReleaseEnv = "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n";
|
|
1687
|
+
const codexLbReleaseApikeyAuth = '{"auth_mode":"apikey","key":"sk-test"}\n';
|
|
1688
|
+
const codexLbReleaseOauthBackup = `${oauthAuthJson}\n`;
|
|
1689
|
+
// Happy path: deselect model_provider and preserve backup file.
|
|
1690
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
1691
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), codexLbReleaseOauthBackup);
|
|
1692
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), codexLbReleaseEnv);
|
|
1693
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), codexLbReleaseConfig);
|
|
1694
|
+
const codexLbReleaseRun = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'release', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1695
|
+
if (codexLbReleaseRun.code !== 0) throw new Error(`selftest: codex-lb release exited ${codexLbReleaseRun.code}: ${codexLbReleaseRun.stderr}`);
|
|
1696
|
+
const codexLbReleaseJson = JSON.parse(codexLbReleaseRun.stdout);
|
|
1697
|
+
const codexLbReleaseAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1698
|
+
const codexLbReleaseBackupAfter = await safeReadText(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1699
|
+
const codexLbReleaseConfigAfter = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1700
|
+
if (codexLbReleaseJson.status !== 'released' || codexLbReleaseJson.provider_unselected !== true || codexLbReleaseJson.backup_removed !== false || !codexLbReleaseAuth.includes('oauth-id') || !codexLbReleaseAuth.includes('oauth-refresh') || codexLbReleaseAuth.includes('apikey') || !codexLbReleaseBackupAfter.includes('oauth-id') || hasTopLevelCodexLbSelected(codexLbReleaseConfigAfter)) throw new Error('selftest: codex-lb release happy path did not restore OAuth, preserve backup, and deselect model_provider');
|
|
1701
|
+
// --keep-provider: restore auth.json but leave model_provider = "codex-lb" alone.
|
|
1702
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
1703
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), codexLbReleaseOauthBackup);
|
|
1704
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), codexLbReleaseConfig);
|
|
1705
|
+
const codexLbReleaseKeepRun = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'release', '--keep-provider', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1706
|
+
if (codexLbReleaseKeepRun.code !== 0) throw new Error(`selftest: codex-lb release --keep-provider exited ${codexLbReleaseKeepRun.code}: ${codexLbReleaseKeepRun.stderr}`);
|
|
1707
|
+
const codexLbReleaseKeepJson = JSON.parse(codexLbReleaseKeepRun.stdout);
|
|
1708
|
+
const codexLbReleaseKeepConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1709
|
+
if (codexLbReleaseKeepJson.status !== 'released' || codexLbReleaseKeepJson.provider_unselected !== false || !hasTopLevelCodexLbSelected(codexLbReleaseKeepConfig)) throw new Error('selftest: codex-lb release --keep-provider should leave model_provider = "codex-lb" intact');
|
|
1710
|
+
// --delete-backup: restore auth.json and remove the backup file.
|
|
1711
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
1712
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), codexLbReleaseOauthBackup);
|
|
1713
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), codexLbReleaseConfig);
|
|
1714
|
+
const codexLbReleaseDeleteRun = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'release', '--delete-backup', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1715
|
+
if (codexLbReleaseDeleteRun.code !== 0) throw new Error(`selftest: codex-lb release --delete-backup exited ${codexLbReleaseDeleteRun.code}: ${codexLbReleaseDeleteRun.stderr}`);
|
|
1716
|
+
const codexLbReleaseDeleteJson = JSON.parse(codexLbReleaseDeleteRun.stdout);
|
|
1717
|
+
const codexLbReleaseDeleteBackupExists = await exists(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'));
|
|
1718
|
+
if (codexLbReleaseDeleteJson.status !== 'released' || codexLbReleaseDeleteJson.backup_removed !== true || codexLbReleaseDeleteBackupExists) throw new Error('selftest: codex-lb release --delete-backup should remove the backup file after restore');
|
|
1719
|
+
// No backup: release should refuse and exit 1.
|
|
1720
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
1721
|
+
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
1722
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), codexLbReleaseConfig);
|
|
1723
|
+
const codexLbReleaseMissingRun = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'release', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1724
|
+
const codexLbReleaseMissingJson = JSON.parse(codexLbReleaseMissingRun.stdout || '{}');
|
|
1725
|
+
const codexLbReleaseMissingAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1726
|
+
if (codexLbReleaseMissingRun.code === 0 || codexLbReleaseMissingJson.status !== 'no_backup' || !codexLbReleaseMissingAuth.includes('apikey')) throw new Error('selftest: codex-lb release with no backup should exit non-zero and report no_backup without touching auth.json');
|
|
1727
|
+
// unselect: flip model_provider off without touching auth.json or env file.
|
|
1728
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), codexLbReleaseApikeyAuth);
|
|
1729
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), codexLbReleaseConfig);
|
|
1730
|
+
const codexLbUnselectRun = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'unselect', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1731
|
+
if (codexLbUnselectRun.code !== 0) throw new Error(`selftest: codex-lb unselect exited ${codexLbUnselectRun.code}: ${codexLbUnselectRun.stderr}`);
|
|
1732
|
+
const codexLbUnselectJson = JSON.parse(codexLbUnselectRun.stdout);
|
|
1733
|
+
const codexLbUnselectConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1734
|
+
const codexLbUnselectAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1735
|
+
if (codexLbUnselectJson.status !== 'unselected' || hasTopLevelCodexLbSelected(codexLbUnselectConfig) || !codexLbUnselectConfig.includes('[model_providers.codex-lb]') || !codexLbUnselectAuth.includes('apikey')) throw new Error('selftest: codex-lb unselect should drop model_provider but preserve [model_providers.codex-lb] and auth.json');
|
|
1736
|
+
// Restore the doctor-test auth.json shape so downstream selftest assertions still hold.
|
|
1737
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
1738
|
+
await fsp.rm(path.join(codexLbHome, '.codex', 'auth.chatgpt-backup.json'), { force: true });
|
|
1356
1739
|
const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
|
|
1357
1740
|
await ensureDir(codexLbContext7Bin);
|
|
1358
1741
|
await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
package/src/cli/main.mjs
CHANGED
|
@@ -79,7 +79,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
|
|
|
79
79
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
80
80
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
81
81
|
import { context7Command } from './context7-command.mjs';
|
|
82
|
-
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
82
|
+
import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
|
|
83
83
|
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
84
84
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
85
85
|
import { recallPulseCommand } from './recallpulse-command.mjs';
|
|
@@ -188,8 +188,8 @@ Usage:
|
|
|
188
188
|
sks bootstrap [--install-scope global|project] [--local-only] [--json]
|
|
189
189
|
sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
|
|
190
190
|
sks codex-app
|
|
191
|
-
sks codex-lb status|health|repair|setup --host <domain> --api-key <key>
|
|
192
|
-
sks auth status|health|repair|setup --host <domain> --api-key <key>
|
|
191
|
+
sks codex-lb status|health|repair|release|unselect|setup --host <domain> --api-key <key>
|
|
192
|
+
sks auth status|health|repair|release|unselect|setup --host <domain> --api-key <key>
|
|
193
193
|
sks openclaw install|path|print [--dir path] [--force] [--json]
|
|
194
194
|
sks --mad [--high]
|
|
195
195
|
sks auto-review status|enable|start [--high]
|
|
@@ -1141,7 +1141,9 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1141
1141
|
const json = flag(args, '--json');
|
|
1142
1142
|
if (sub === 'status' || sub === 'check') {
|
|
1143
1143
|
const status = await codexLbStatus();
|
|
1144
|
-
|
|
1144
|
+
const backupPath = codexLbChatgptBackupPath();
|
|
1145
|
+
const backupPresent = await exists(backupPath);
|
|
1146
|
+
if (json) return console.log(JSON.stringify({ ...status, chatgpt_backup_present: backupPresent, chatgpt_backup_path: backupPath }, null, 2));
|
|
1145
1147
|
console.log('SKS codex-lb\n');
|
|
1146
1148
|
console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
|
|
1147
1149
|
console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
|
|
@@ -1149,10 +1151,71 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1149
1151
|
console.log(`Codex App auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`);
|
|
1150
1152
|
console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
|
|
1151
1153
|
if (status.base_url) console.log(`Base URL: ${status.base_url}`);
|
|
1154
|
+
console.log(`ChatGPT backup: ${backupPresent ? `yes (${backupPath})` : 'no'}`);
|
|
1152
1155
|
if (status.ok && !status.selected) console.log('\nRun: sks codex-lb repair to activate codex-lb for Codex App.');
|
|
1153
1156
|
else if (!status.ok && status.base_url && status.env_key_configured) console.log('\nRun: sks codex-lb repair to restore the upstream codex-lb provider block.');
|
|
1154
1157
|
else if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
|
|
1155
1158
|
else console.log('\nRepair provider auth: sks codex-lb repair');
|
|
1159
|
+
if (backupPresent) console.log('Switch back to ChatGPT OAuth login: sks codex-lb release');
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (sub === 'release') {
|
|
1163
|
+
const result = await releaseCodexLbAuthHold({
|
|
1164
|
+
keepProvider: flag(args, '--keep-provider'),
|
|
1165
|
+
deleteBackup: flag(args, '--delete-backup'),
|
|
1166
|
+
force: flag(args, '--force')
|
|
1167
|
+
});
|
|
1168
|
+
if (result.status === 'no_backup' || result.status === 'auth_in_use' || result.status === 'failed') process.exitCode = 1;
|
|
1169
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1170
|
+
if (result.status === 'released') {
|
|
1171
|
+
console.log('codex-lb auth released: ChatGPT OAuth blob restored.');
|
|
1172
|
+
console.log(`Auth: ${result.auth_path}`);
|
|
1173
|
+
console.log(`Backup: ${result.backup_removed ? 'removed' : result.backup_path}`);
|
|
1174
|
+
console.log(`Provider unselected: ${result.provider_unselected ? 'yes' : 'no'}`);
|
|
1175
|
+
if (result.provider_error) console.log(`Provider unselect warning: ${result.provider_error}`);
|
|
1176
|
+
console.log('\nLaunch Codex App / `codex` and complete the ChatGPT browser login if prompted.');
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (result.status === 'already_chatgpt') {
|
|
1180
|
+
console.log('codex-lb auth release: auth.json already carries ChatGPT OAuth tokens — nothing to restore.');
|
|
1181
|
+
console.log(`Auth: ${result.auth_path}`);
|
|
1182
|
+
console.log(`Backup: ${result.backup_path}`);
|
|
1183
|
+
console.log(`Provider unselected: ${result.provider_unselected ? 'yes' : 'no'}`);
|
|
1184
|
+
if (result.provider_error) console.log(`Provider unselect warning: ${result.provider_error}`);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (result.status === 'no_backup') {
|
|
1188
|
+
console.error(`codex-lb auth release: no ChatGPT OAuth backup found at ${result.backup_path}.`);
|
|
1189
|
+
if (result.reason === 'backup_not_oauth') console.error('The backup file is present but does not contain a ChatGPT OAuth token blob — refusing to clobber auth.json.');
|
|
1190
|
+
else console.error('Run `sks codex-lb repair` after a fresh ChatGPT login to recreate a backup, or `sks codex-lb unselect` to leave codex-lb off without touching auth.json.');
|
|
1191
|
+
process.exitCode = 1;
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (result.status === 'auth_in_use') {
|
|
1195
|
+
console.error(`codex-lb auth release refused: ${result.auth_path} does not look like the codex-lb apikey shape. Re-run with --force to overwrite, or back up auth.json yourself first.`);
|
|
1196
|
+
process.exitCode = 1;
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
console.error(`codex-lb auth release failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
1200
|
+
process.exitCode = 1;
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (sub === 'unselect') {
|
|
1204
|
+
const result = await unselectCodexLbProvider();
|
|
1205
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
1206
|
+
if (json) return console.log(JSON.stringify(result, null, 2));
|
|
1207
|
+
if (result.status === 'unselected') {
|
|
1208
|
+
console.log('codex-lb unselected. Codex CLI/App will fall back to the default OpenAI provider.');
|
|
1209
|
+
console.log(`Config: ${result.config_path}`);
|
|
1210
|
+
console.log('Re-engage codex-lb with: sks codex-lb repair');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (result.status === 'not_selected') {
|
|
1214
|
+
console.log('codex-lb is not selected — nothing to do.');
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
console.error(`codex-lb unselect failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
|
|
1218
|
+
process.exitCode = 1;
|
|
1156
1219
|
return;
|
|
1157
1220
|
}
|
|
1158
1221
|
if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
|
|
@@ -1205,7 +1268,7 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1205
1268
|
console.log(`Key env: ${result.env_path}`);
|
|
1206
1269
|
return;
|
|
1207
1270
|
}
|
|
1208
|
-
console.error('Usage: sks codex-lb status|health|repair|setup --host <domain> --api-key <key> [--json]');
|
|
1271
|
+
console.error('Usage: sks codex-lb status|health|repair|release [--keep-provider] [--delete-backup] [--force]|unselect|setup --host <domain> --api-key <key> [--json]');
|
|
1209
1272
|
process.exitCode = 1;
|
|
1210
1273
|
}
|
|
1211
1274
|
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.9.
|
|
8
|
+
export const PACKAGE_VERSION = '0.9.4';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|