@nervmor/codexui 1.0.3 → 1.0.5
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/dist/assets/index-BQpi2jbn.css +1 -0
- package/dist/assets/index-CNfDYn0C.js +1455 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +462 -46
- package/dist-cli/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/assets/index-BRHjQaZ8.css +0 -1
- package/dist/assets/index-C2tfimEi.js +0 -1441
package/dist-cli/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
5
|
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
6
|
+
import { readFile as readFile5, stat as stat6, writeFile as writeFile5 } from "fs/promises";
|
|
7
7
|
import { homedir as homedir4, networkInterfaces } from "os";
|
|
8
|
-
import { join as join6 } from "path";
|
|
8
|
+
import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
|
|
9
9
|
import { spawn as spawn4, spawnSync } from "child_process";
|
|
10
10
|
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -58,6 +58,9 @@ function readNumber(value) {
|
|
|
58
58
|
function readBoolean(value) {
|
|
59
59
|
return typeof value === "boolean" ? value : null;
|
|
60
60
|
}
|
|
61
|
+
function normalizeAccountUnavailableReason(value) {
|
|
62
|
+
return value === "payment_required" ? value : null;
|
|
63
|
+
}
|
|
61
64
|
function setJson(res, statusCode, payload) {
|
|
62
65
|
res.statusCode = statusCode;
|
|
63
66
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -77,6 +80,14 @@ function getErrorMessage(payload, fallback) {
|
|
|
77
80
|
}
|
|
78
81
|
return fallback;
|
|
79
82
|
}
|
|
83
|
+
function isPaymentRequiredErrorMessage(value) {
|
|
84
|
+
if (!value) return false;
|
|
85
|
+
const normalized = value.toLowerCase();
|
|
86
|
+
return normalized.includes("payment required") || /\b402\b/.test(normalized);
|
|
87
|
+
}
|
|
88
|
+
function detectAccountUnavailableReason(error) {
|
|
89
|
+
return isPaymentRequiredErrorMessage(getErrorMessage(error, "")) ? "payment_required" : null;
|
|
90
|
+
}
|
|
80
91
|
function getCodexHomeDir() {
|
|
81
92
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
82
93
|
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
@@ -159,7 +170,8 @@ function normalizeStoredAccountEntry(value) {
|
|
|
159
170
|
quotaSnapshot: normalizeRateLimitSnapshot(record?.quotaSnapshot),
|
|
160
171
|
quotaUpdatedAtIso: readString(record?.quotaUpdatedAtIso),
|
|
161
172
|
quotaStatus,
|
|
162
|
-
quotaError: readString(record?.quotaError)
|
|
173
|
+
quotaError: readString(record?.quotaError),
|
|
174
|
+
unavailableReason: normalizeAccountUnavailableReason(record?.unavailableReason) ?? (isPaymentRequiredErrorMessage(readString(record?.quotaError)) ? "payment_required" : null)
|
|
163
175
|
};
|
|
164
176
|
}
|
|
165
177
|
async function readStoredAccountsState() {
|
|
@@ -248,6 +260,9 @@ async function writeSnapshot(storageId, raw) {
|
|
|
248
260
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
249
261
|
await writeFile(getSnapshotPath(storageId), raw, { encoding: "utf8", mode: 384 });
|
|
250
262
|
}
|
|
263
|
+
async function removeSnapshot(storageId) {
|
|
264
|
+
await rm(join(getAccountsSnapshotRoot(), storageId), { recursive: true, force: true });
|
|
265
|
+
}
|
|
251
266
|
async function readRuntimeAccountMetadata(appServer) {
|
|
252
267
|
const payload = asRecord(await appServer.rpc("account/read", { refreshToken: false }));
|
|
253
268
|
const account = asRecord(payload?.account);
|
|
@@ -345,8 +360,8 @@ async function withTemporaryCodexAppServer(authRaw, run) {
|
|
|
345
360
|
};
|
|
346
361
|
const call = async (method, params) => {
|
|
347
362
|
const id = nextId++;
|
|
348
|
-
return await new Promise((
|
|
349
|
-
pending.set(id, { resolve:
|
|
363
|
+
return await new Promise((resolve3, reject) => {
|
|
364
|
+
pending.set(id, { resolve: resolve3, reject });
|
|
350
365
|
sendLine({
|
|
351
366
|
jsonrpc: "2.0",
|
|
352
367
|
id,
|
|
@@ -435,6 +450,16 @@ async function replaceStoredAccount(nextEntry, activeAccountId) {
|
|
|
435
450
|
accounts: nextState.accounts
|
|
436
451
|
});
|
|
437
452
|
}
|
|
453
|
+
async function pickReplacementActiveAccount(accounts) {
|
|
454
|
+
const sorted = sortAccounts(accounts, null);
|
|
455
|
+
for (const entry of sorted) {
|
|
456
|
+
if (entry.unavailableReason === "payment_required") continue;
|
|
457
|
+
if (await fileExists(getSnapshotPath(entry.storageId))) {
|
|
458
|
+
return entry;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
438
463
|
async function refreshAccountsInBackground(accountIds, activeAccountId) {
|
|
439
464
|
for (const accountId of accountIds) {
|
|
440
465
|
const state = await readStoredAccountsState();
|
|
@@ -449,14 +474,16 @@ async function refreshAccountsInBackground(accountIds, activeAccountId) {
|
|
|
449
474
|
quotaSnapshot: inspected.quotaSnapshot ?? entry.quotaSnapshot,
|
|
450
475
|
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
451
476
|
quotaStatus: "ready",
|
|
452
|
-
quotaError: null
|
|
477
|
+
quotaError: null,
|
|
478
|
+
unavailableReason: null
|
|
453
479
|
}, activeAccountId);
|
|
454
480
|
} catch (error) {
|
|
455
481
|
await replaceStoredAccount({
|
|
456
482
|
...entry,
|
|
457
483
|
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
458
484
|
quotaStatus: "error",
|
|
459
|
-
quotaError: getErrorMessage(error, "Failed to refresh account quota")
|
|
485
|
+
quotaError: getErrorMessage(error, "Failed to refresh account quota"),
|
|
486
|
+
unavailableReason: detectAccountUnavailableReason(error)
|
|
460
487
|
}, activeAccountId);
|
|
461
488
|
}
|
|
462
489
|
}
|
|
@@ -509,7 +536,8 @@ async function importAccountFromAuthPath(path) {
|
|
|
509
536
|
quotaSnapshot: existing?.quotaSnapshot ?? null,
|
|
510
537
|
quotaUpdatedAtIso: existing?.quotaUpdatedAtIso ?? null,
|
|
511
538
|
quotaStatus: existing?.quotaStatus ?? "idle",
|
|
512
|
-
quotaError: existing?.quotaError ?? null
|
|
539
|
+
quotaError: existing?.quotaError ?? null,
|
|
540
|
+
unavailableReason: existing?.unavailableReason ?? null
|
|
513
541
|
};
|
|
514
542
|
const nextState = withUpsertedAccount(state, nextEntry);
|
|
515
543
|
await writeStoredAccountsState(nextState);
|
|
@@ -559,7 +587,8 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
559
587
|
quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
|
|
560
588
|
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
561
589
|
quotaStatus: "ready",
|
|
562
|
-
quotaError: null
|
|
590
|
+
quotaError: null,
|
|
591
|
+
unavailableReason: null
|
|
563
592
|
};
|
|
564
593
|
const nextState = withUpsertedAccount({
|
|
565
594
|
activeAccountId: importedAccountId,
|
|
@@ -606,13 +635,13 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
606
635
|
});
|
|
607
636
|
return true;
|
|
608
637
|
}
|
|
609
|
-
const rawBody = await new Promise((
|
|
638
|
+
const rawBody = await new Promise((resolve3, reject) => {
|
|
610
639
|
let body = "";
|
|
611
640
|
req.setEncoding("utf8");
|
|
612
641
|
req.on("data", (chunk) => {
|
|
613
642
|
body += chunk;
|
|
614
643
|
});
|
|
615
|
-
req.on("end", () =>
|
|
644
|
+
req.on("end", () => resolve3(body));
|
|
616
645
|
req.on("error", reject);
|
|
617
646
|
});
|
|
618
647
|
const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
@@ -651,7 +680,8 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
651
680
|
quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
|
|
652
681
|
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
653
682
|
quotaStatus: "ready",
|
|
654
|
-
quotaError: null
|
|
683
|
+
quotaError: null,
|
|
684
|
+
unavailableReason: null
|
|
655
685
|
};
|
|
656
686
|
const nextState = withUpsertedAccount({
|
|
657
687
|
activeAccountId: accountId,
|
|
@@ -676,6 +706,13 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
676
706
|
} catch (error) {
|
|
677
707
|
await restoreActiveAuth(previousRaw);
|
|
678
708
|
appServer.dispose();
|
|
709
|
+
await replaceStoredAccount({
|
|
710
|
+
...target,
|
|
711
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
712
|
+
quotaStatus: "error",
|
|
713
|
+
quotaError: getErrorMessage(error, "Failed to switch account"),
|
|
714
|
+
unavailableReason: detectAccountUnavailableReason(error)
|
|
715
|
+
}, state.activeAccountId);
|
|
679
716
|
setJson(res, 502, {
|
|
680
717
|
error: "account_switch_failed",
|
|
681
718
|
message: getErrorMessage(error, "Failed to switch account")
|
|
@@ -689,6 +726,145 @@ async function handleAccountRoutes(req, res, url, context) {
|
|
|
689
726
|
}
|
|
690
727
|
return true;
|
|
691
728
|
}
|
|
729
|
+
if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
|
|
730
|
+
try {
|
|
731
|
+
const rawBody = await new Promise((resolve3, reject) => {
|
|
732
|
+
let body = "";
|
|
733
|
+
req.setEncoding("utf8");
|
|
734
|
+
req.on("data", (chunk) => {
|
|
735
|
+
body += chunk;
|
|
736
|
+
});
|
|
737
|
+
req.on("end", () => resolve3(body));
|
|
738
|
+
req.on("error", reject);
|
|
739
|
+
});
|
|
740
|
+
const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
741
|
+
const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
|
|
742
|
+
if (!accountId) {
|
|
743
|
+
setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
const state = await readStoredAccountsState();
|
|
747
|
+
const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
|
|
748
|
+
if (!target) {
|
|
749
|
+
setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
|
|
753
|
+
if (state.activeAccountId !== accountId) {
|
|
754
|
+
await removeSnapshot(target.storageId);
|
|
755
|
+
await writeStoredAccountsState({
|
|
756
|
+
activeAccountId: state.activeAccountId,
|
|
757
|
+
accounts: remainingAccounts
|
|
758
|
+
});
|
|
759
|
+
setJson(res, 200, {
|
|
760
|
+
ok: true,
|
|
761
|
+
data: {
|
|
762
|
+
activeAccountId: state.activeAccountId,
|
|
763
|
+
accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
if (appServer.listPendingServerRequests().length > 0) {
|
|
769
|
+
setJson(res, 409, {
|
|
770
|
+
error: "account_remove_blocked",
|
|
771
|
+
message: "Finish pending approval requests before removing the active account."
|
|
772
|
+
});
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
let previousRaw = null;
|
|
776
|
+
try {
|
|
777
|
+
previousRaw = await readFile(getActiveAuthPath(), "utf8");
|
|
778
|
+
} catch {
|
|
779
|
+
previousRaw = null;
|
|
780
|
+
}
|
|
781
|
+
const replacement = await pickReplacementActiveAccount(remainingAccounts);
|
|
782
|
+
if (!replacement) {
|
|
783
|
+
await restoreActiveAuth(null);
|
|
784
|
+
appServer.dispose();
|
|
785
|
+
await removeSnapshot(target.storageId);
|
|
786
|
+
await writeStoredAccountsState({
|
|
787
|
+
activeAccountId: null,
|
|
788
|
+
accounts: remainingAccounts
|
|
789
|
+
});
|
|
790
|
+
void scheduleAccountsBackgroundRefresh({
|
|
791
|
+
force: true,
|
|
792
|
+
accountIds: remainingAccounts.map((entry) => entry.accountId)
|
|
793
|
+
});
|
|
794
|
+
setJson(res, 200, {
|
|
795
|
+
ok: true,
|
|
796
|
+
data: {
|
|
797
|
+
activeAccountId: null,
|
|
798
|
+
accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
|
|
804
|
+
if (!await fileExists(replacementSnapshotPath)) {
|
|
805
|
+
setJson(res, 404, {
|
|
806
|
+
error: "account_not_found",
|
|
807
|
+
message: "The replacement account snapshot is missing."
|
|
808
|
+
});
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
|
|
812
|
+
await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
|
|
813
|
+
try {
|
|
814
|
+
appServer.dispose();
|
|
815
|
+
const inspection = await validateSwitchedAccount(appServer);
|
|
816
|
+
const activatedReplacement = {
|
|
817
|
+
...replacement,
|
|
818
|
+
email: inspection.metadata.email ?? replacement.email,
|
|
819
|
+
planType: inspection.metadata.planType ?? replacement.planType,
|
|
820
|
+
lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
821
|
+
quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
|
|
822
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
823
|
+
quotaStatus: "ready",
|
|
824
|
+
quotaError: null,
|
|
825
|
+
unavailableReason: null
|
|
826
|
+
};
|
|
827
|
+
const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
|
|
828
|
+
await removeSnapshot(target.storageId);
|
|
829
|
+
await writeStoredAccountsState({
|
|
830
|
+
activeAccountId: activatedReplacement.accountId,
|
|
831
|
+
accounts: nextAccounts
|
|
832
|
+
});
|
|
833
|
+
void scheduleAccountsBackgroundRefresh({
|
|
834
|
+
force: true,
|
|
835
|
+
prioritizeAccountId: activatedReplacement.accountId,
|
|
836
|
+
accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
|
|
837
|
+
});
|
|
838
|
+
setJson(res, 200, {
|
|
839
|
+
ok: true,
|
|
840
|
+
data: {
|
|
841
|
+
activeAccountId: activatedReplacement.accountId,
|
|
842
|
+
accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
} catch (error) {
|
|
846
|
+
await restoreActiveAuth(previousRaw);
|
|
847
|
+
appServer.dispose();
|
|
848
|
+
await replaceStoredAccount({
|
|
849
|
+
...replacement,
|
|
850
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
851
|
+
quotaStatus: "error",
|
|
852
|
+
quotaError: getErrorMessage(error, "Failed to switch account"),
|
|
853
|
+
unavailableReason: detectAccountUnavailableReason(error)
|
|
854
|
+
}, state.activeAccountId);
|
|
855
|
+
setJson(res, 502, {
|
|
856
|
+
error: "account_remove_failed",
|
|
857
|
+
message: getErrorMessage(error, "Failed to remove account")
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
} catch (error) {
|
|
861
|
+
setJson(res, 400, {
|
|
862
|
+
error: "invalid_auth_json",
|
|
863
|
+
message: getErrorMessage(error, "Failed to remove account")
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
692
868
|
return false;
|
|
693
869
|
}
|
|
694
870
|
|
|
@@ -729,7 +905,7 @@ function getSkillsInstallDir() {
|
|
|
729
905
|
return join2(getCodexHomeDir2(), "skills");
|
|
730
906
|
}
|
|
731
907
|
async function runCommand(command, args, options = {}) {
|
|
732
|
-
await new Promise((
|
|
908
|
+
await new Promise((resolve3, reject) => {
|
|
733
909
|
const proc = spawn2(command, args, {
|
|
734
910
|
cwd: options.cwd,
|
|
735
911
|
env: process.env,
|
|
@@ -746,7 +922,7 @@ async function runCommand(command, args, options = {}) {
|
|
|
746
922
|
proc.on("error", reject);
|
|
747
923
|
proc.on("close", (code) => {
|
|
748
924
|
if (code === 0) {
|
|
749
|
-
|
|
925
|
+
resolve3();
|
|
750
926
|
return;
|
|
751
927
|
}
|
|
752
928
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -756,7 +932,7 @@ async function runCommand(command, args, options = {}) {
|
|
|
756
932
|
});
|
|
757
933
|
}
|
|
758
934
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
759
|
-
return await new Promise((
|
|
935
|
+
return await new Promise((resolve3, reject) => {
|
|
760
936
|
const proc = spawn2(command, args, {
|
|
761
937
|
cwd: options.cwd,
|
|
762
938
|
env: process.env,
|
|
@@ -773,7 +949,7 @@ async function runCommandWithOutput(command, args, options = {}) {
|
|
|
773
949
|
proc.on("error", reject);
|
|
774
950
|
proc.on("close", (code) => {
|
|
775
951
|
if (code === 0) {
|
|
776
|
-
|
|
952
|
+
resolve3(stdout.trim());
|
|
777
953
|
return;
|
|
778
954
|
}
|
|
779
955
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -818,9 +994,9 @@ async function getGhToken() {
|
|
|
818
994
|
proc.stdout.on("data", (d) => {
|
|
819
995
|
out += d.toString();
|
|
820
996
|
});
|
|
821
|
-
return new Promise((
|
|
822
|
-
proc.on("close", (code) =>
|
|
823
|
-
proc.on("error", () =>
|
|
997
|
+
return new Promise((resolve3) => {
|
|
998
|
+
proc.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
|
|
999
|
+
proc.on("error", () => resolve3(null));
|
|
824
1000
|
});
|
|
825
1001
|
} catch {
|
|
826
1002
|
return null;
|
|
@@ -1059,7 +1235,7 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
|
1059
1235
|
ready = true;
|
|
1060
1236
|
break;
|
|
1061
1237
|
}
|
|
1062
|
-
await new Promise((
|
|
1238
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
1063
1239
|
}
|
|
1064
1240
|
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
1065
1241
|
if (!created) return;
|
|
@@ -1854,7 +2030,7 @@ function scoreFileCandidate(path, query) {
|
|
|
1854
2030
|
return 10;
|
|
1855
2031
|
}
|
|
1856
2032
|
async function listFilesWithRipgrep(cwd) {
|
|
1857
|
-
return await new Promise((
|
|
2033
|
+
return await new Promise((resolve3, reject) => {
|
|
1858
2034
|
const proc = spawn3("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1859
2035
|
cwd,
|
|
1860
2036
|
env: process.env,
|
|
@@ -1872,7 +2048,7 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1872
2048
|
proc.on("close", (code) => {
|
|
1873
2049
|
if (code === 0) {
|
|
1874
2050
|
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1875
|
-
|
|
2051
|
+
resolve3(rows);
|
|
1876
2052
|
return;
|
|
1877
2053
|
}
|
|
1878
2054
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -1885,7 +2061,7 @@ function getCodexHomeDir3() {
|
|
|
1885
2061
|
return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
|
|
1886
2062
|
}
|
|
1887
2063
|
async function runCommand2(command, args, options = {}) {
|
|
1888
|
-
await new Promise((
|
|
2064
|
+
await new Promise((resolve3, reject) => {
|
|
1889
2065
|
const proc = spawn3(command, args, {
|
|
1890
2066
|
cwd: options.cwd,
|
|
1891
2067
|
env: process.env,
|
|
@@ -1902,7 +2078,7 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1902
2078
|
proc.on("error", reject);
|
|
1903
2079
|
proc.on("close", (code) => {
|
|
1904
2080
|
if (code === 0) {
|
|
1905
|
-
|
|
2081
|
+
resolve3();
|
|
1906
2082
|
return;
|
|
1907
2083
|
}
|
|
1908
2084
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -1934,7 +2110,7 @@ async function ensureRepoHasInitialCommit(repoRoot) {
|
|
|
1934
2110
|
);
|
|
1935
2111
|
}
|
|
1936
2112
|
async function runCommandCapture(command, args, options = {}) {
|
|
1937
|
-
return await new Promise((
|
|
2113
|
+
return await new Promise((resolve3, reject) => {
|
|
1938
2114
|
const proc = spawn3(command, args, {
|
|
1939
2115
|
cwd: options.cwd,
|
|
1940
2116
|
env: process.env,
|
|
@@ -1951,7 +2127,7 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
1951
2127
|
proc.on("error", reject);
|
|
1952
2128
|
proc.on("close", (code) => {
|
|
1953
2129
|
if (code === 0) {
|
|
1954
|
-
|
|
2130
|
+
resolve3(stdout.trim());
|
|
1955
2131
|
return;
|
|
1956
2132
|
}
|
|
1957
2133
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -1997,6 +2173,9 @@ async function readCodexAuth() {
|
|
|
1997
2173
|
function getCodexGlobalStatePath() {
|
|
1998
2174
|
return join3(getCodexHomeDir3(), ".codex-global-state.json");
|
|
1999
2175
|
}
|
|
2176
|
+
function getCodexSessionIndexPath() {
|
|
2177
|
+
return join3(getCodexHomeDir3(), "session_index.jsonl");
|
|
2178
|
+
}
|
|
2000
2179
|
var MAX_THREAD_TITLES = 500;
|
|
2001
2180
|
function normalizeThreadTitleCache(value) {
|
|
2002
2181
|
const record = asRecord3(value);
|
|
@@ -2024,6 +2203,47 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
2024
2203
|
const { [id]: _, ...titles } = cache.titles;
|
|
2025
2204
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
2026
2205
|
}
|
|
2206
|
+
function normalizeSessionIndexThreadTitle(value) {
|
|
2207
|
+
const record = asRecord3(value);
|
|
2208
|
+
if (!record) return null;
|
|
2209
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
2210
|
+
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
2211
|
+
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
2212
|
+
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
2213
|
+
if (!id || !title) return null;
|
|
2214
|
+
return {
|
|
2215
|
+
id,
|
|
2216
|
+
title,
|
|
2217
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function trimThreadTitleCache(cache) {
|
|
2221
|
+
const titles = { ...cache.titles };
|
|
2222
|
+
const order = cache.order.filter((id) => {
|
|
2223
|
+
if (!titles[id]) return false;
|
|
2224
|
+
return true;
|
|
2225
|
+
}).slice(0, MAX_THREAD_TITLES);
|
|
2226
|
+
for (const id of Object.keys(titles)) {
|
|
2227
|
+
if (!order.includes(id)) {
|
|
2228
|
+
delete titles[id];
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return { titles, order };
|
|
2232
|
+
}
|
|
2233
|
+
function mergeThreadTitleCaches(base, overlay) {
|
|
2234
|
+
const titles = { ...base.titles, ...overlay.titles };
|
|
2235
|
+
const order = [];
|
|
2236
|
+
for (const id of [...overlay.order, ...base.order]) {
|
|
2237
|
+
if (!titles[id] || order.includes(id)) continue;
|
|
2238
|
+
order.push(id);
|
|
2239
|
+
}
|
|
2240
|
+
for (const id of Object.keys(titles)) {
|
|
2241
|
+
if (!order.includes(id)) {
|
|
2242
|
+
order.push(id);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return trimThreadTitleCache({ titles, order });
|
|
2246
|
+
}
|
|
2027
2247
|
async function readThreadTitleCache() {
|
|
2028
2248
|
const statePath = getCodexGlobalStatePath();
|
|
2029
2249
|
try {
|
|
@@ -2046,6 +2266,42 @@ async function writeThreadTitleCache(cache) {
|
|
|
2046
2266
|
payload["thread-titles"] = cache;
|
|
2047
2267
|
await writeFile3(statePath, JSON.stringify(payload), "utf8");
|
|
2048
2268
|
}
|
|
2269
|
+
async function readThreadTitlesFromSessionIndex() {
|
|
2270
|
+
try {
|
|
2271
|
+
const raw = await readFile3(getCodexSessionIndexPath(), "utf8");
|
|
2272
|
+
const latestById = /* @__PURE__ */ new Map();
|
|
2273
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
2274
|
+
const trimmed = line.trim();
|
|
2275
|
+
if (!trimmed) continue;
|
|
2276
|
+
try {
|
|
2277
|
+
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
2278
|
+
if (!entry) continue;
|
|
2279
|
+
const previous = latestById.get(entry.id);
|
|
2280
|
+
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
2281
|
+
latestById.set(entry.id, entry);
|
|
2282
|
+
}
|
|
2283
|
+
} catch {
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
2287
|
+
const titles = {};
|
|
2288
|
+
const order = [];
|
|
2289
|
+
for (const entry of entries) {
|
|
2290
|
+
titles[entry.id] = entry.title;
|
|
2291
|
+
order.push(entry.id);
|
|
2292
|
+
}
|
|
2293
|
+
return trimThreadTitleCache({ titles, order });
|
|
2294
|
+
} catch {
|
|
2295
|
+
return { titles: {}, order: [] };
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
async function readMergedThreadTitleCache() {
|
|
2299
|
+
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
2300
|
+
readThreadTitlesFromSessionIndex(),
|
|
2301
|
+
readThreadTitleCache()
|
|
2302
|
+
]);
|
|
2303
|
+
return mergeThreadTitleCaches(sessionIndexCache, persistedCache);
|
|
2304
|
+
}
|
|
2049
2305
|
async function readWorkspaceRootsState() {
|
|
2050
2306
|
const statePath = getCodexGlobalStatePath();
|
|
2051
2307
|
let payload = {};
|
|
@@ -2170,14 +2426,14 @@ async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
|
2170
2426
|
if (accountId) {
|
|
2171
2427
|
headers["ChatGPT-Account-Id"] = accountId;
|
|
2172
2428
|
}
|
|
2173
|
-
return new Promise((
|
|
2429
|
+
return new Promise((resolve3, reject) => {
|
|
2174
2430
|
const req = httpsRequest(
|
|
2175
2431
|
"https://chatgpt.com/backend-api/transcribe",
|
|
2176
2432
|
{ method: "POST", headers },
|
|
2177
2433
|
(res) => {
|
|
2178
2434
|
const chunks = [];
|
|
2179
2435
|
res.on("data", (c) => chunks.push(c));
|
|
2180
|
-
res.on("end", () =>
|
|
2436
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
2181
2437
|
res.on("error", reject);
|
|
2182
2438
|
}
|
|
2183
2439
|
);
|
|
@@ -2334,8 +2590,8 @@ var AppServerProcess = class {
|
|
|
2334
2590
|
async call(method, params) {
|
|
2335
2591
|
this.start();
|
|
2336
2592
|
const id = this.nextId++;
|
|
2337
|
-
return new Promise((
|
|
2338
|
-
this.pending.set(id, { resolve:
|
|
2593
|
+
return new Promise((resolve3, reject) => {
|
|
2594
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
2339
2595
|
this.sendLine({
|
|
2340
2596
|
jsonrpc: "2.0",
|
|
2341
2597
|
id,
|
|
@@ -2443,7 +2699,7 @@ var MethodCatalog = class {
|
|
|
2443
2699
|
this.notificationCache = null;
|
|
2444
2700
|
}
|
|
2445
2701
|
async runGenerateSchemaCommand(outDir) {
|
|
2446
|
-
await new Promise((
|
|
2702
|
+
await new Promise((resolve3, reject) => {
|
|
2447
2703
|
const process2 = spawn3("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
2448
2704
|
stdio: ["ignore", "ignore", "pipe"]
|
|
2449
2705
|
});
|
|
@@ -2455,7 +2711,7 @@ var MethodCatalog = class {
|
|
|
2455
2711
|
process2.on("error", reject);
|
|
2456
2712
|
process2.on("exit", (code) => {
|
|
2457
2713
|
if (code === 0) {
|
|
2458
|
-
|
|
2714
|
+
resolve3();
|
|
2459
2715
|
return;
|
|
2460
2716
|
}
|
|
2461
2717
|
reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
|
|
@@ -2881,7 +3137,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2881
3137
|
return;
|
|
2882
3138
|
}
|
|
2883
3139
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
2884
|
-
const cache = await
|
|
3140
|
+
const cache = await readMergedThreadTitleCache();
|
|
2885
3141
|
setJson3(res, 200, { data: cache });
|
|
2886
3142
|
return;
|
|
2887
3143
|
}
|
|
@@ -3245,9 +3501,9 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3245
3501
|
const rows = items.map((item) => {
|
|
3246
3502
|
const suffix = item.isDirectory ? "/" : "";
|
|
3247
3503
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
3248
|
-
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
3504
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
|
|
3249
3505
|
}).join("\n");
|
|
3250
|
-
const parentLink = localPath !== parentPath ? `<
|
|
3506
|
+
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
3251
3507
|
return `<!doctype html>
|
|
3252
3508
|
<html lang="en">
|
|
3253
3509
|
<head>
|
|
@@ -3261,8 +3517,27 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3261
3517
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
3262
3518
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
3263
3519
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
3264
|
-
.
|
|
3520
|
+
.header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
|
|
3521
|
+
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
3522
|
+
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
3523
|
+
.header-open-btn {
|
|
3524
|
+
height: 42px;
|
|
3525
|
+
padding: 0 14px;
|
|
3526
|
+
border: 1px solid #4f8de0;
|
|
3527
|
+
border-radius: 10px;
|
|
3528
|
+
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
3529
|
+
color: #eef6ff;
|
|
3530
|
+
font-weight: 700;
|
|
3531
|
+
letter-spacing: 0.01em;
|
|
3532
|
+
cursor: pointer;
|
|
3533
|
+
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
3534
|
+
}
|
|
3535
|
+
.header-open-btn:hover { filter: brightness(1.08); }
|
|
3536
|
+
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
3537
|
+
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
3538
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
|
|
3265
3539
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
3540
|
+
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
3266
3541
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
3267
3542
|
@media (max-width: 640px) {
|
|
3268
3543
|
body { margin: 12px; }
|
|
@@ -3274,8 +3549,46 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
3274
3549
|
</head>
|
|
3275
3550
|
<body>
|
|
3276
3551
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
3277
|
-
|
|
3552
|
+
<div class="header-actions">
|
|
3553
|
+
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
3554
|
+
<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
|
|
3555
|
+
</div>
|
|
3556
|
+
<p id="status" class="status"></p>
|
|
3278
3557
|
<ul>${rows}</ul>
|
|
3558
|
+
<script>
|
|
3559
|
+
const status = document.getElementById('status');
|
|
3560
|
+
document.addEventListener('click', async (event) => {
|
|
3561
|
+
const target = event.target;
|
|
3562
|
+
if (!(target instanceof Element)) return;
|
|
3563
|
+
const button = target.closest('.open-folder-btn');
|
|
3564
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
3565
|
+
|
|
3566
|
+
const path = button.getAttribute('data-path') || '';
|
|
3567
|
+
if (!path) return;
|
|
3568
|
+
button.disabled = true;
|
|
3569
|
+
status.textContent = 'Opening folder in Codex...';
|
|
3570
|
+
try {
|
|
3571
|
+
const response = await fetch('/codex-api/project-root', {
|
|
3572
|
+
method: 'POST',
|
|
3573
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3574
|
+
body: JSON.stringify({
|
|
3575
|
+
path,
|
|
3576
|
+
createIfMissing: false,
|
|
3577
|
+
label: '',
|
|
3578
|
+
}),
|
|
3579
|
+
});
|
|
3580
|
+
if (!response.ok) {
|
|
3581
|
+
status.textContent = 'Failed to open folder.';
|
|
3582
|
+
button.disabled = false;
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
window.location.assign('/#/');
|
|
3586
|
+
} catch {
|
|
3587
|
+
status.textContent = 'Failed to open folder.';
|
|
3588
|
+
button.disabled = false;
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
</script>
|
|
3279
3592
|
</body>
|
|
3280
3593
|
</html>`;
|
|
3281
3594
|
}
|
|
@@ -3554,6 +3867,22 @@ function generatePassword() {
|
|
|
3554
3867
|
// src/cli/index.ts
|
|
3555
3868
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
3556
3869
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3870
|
+
var hasPromptedCloudflaredInstall = false;
|
|
3871
|
+
function getCodexHomePath() {
|
|
3872
|
+
return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
3873
|
+
}
|
|
3874
|
+
function getCloudflaredPromptMarkerPath() {
|
|
3875
|
+
return join6(getCodexHomePath(), ".cloudflared-install-prompted");
|
|
3876
|
+
}
|
|
3877
|
+
function hasPromptedCloudflaredInstallPersisted() {
|
|
3878
|
+
return existsSync3(getCloudflaredPromptMarkerPath());
|
|
3879
|
+
}
|
|
3880
|
+
async function persistCloudflaredInstallPrompted() {
|
|
3881
|
+
const codexHome = getCodexHomePath();
|
|
3882
|
+
mkdirSync(codexHome, { recursive: true });
|
|
3883
|
+
await writeFile5(getCloudflaredPromptMarkerPath(), `${Date.now()}
|
|
3884
|
+
`, "utf8");
|
|
3885
|
+
}
|
|
3557
3886
|
async function readCliVersion() {
|
|
3558
3887
|
try {
|
|
3559
3888
|
const packageJsonPath = join6(__dirname2, "..", "package.json");
|
|
@@ -3622,7 +3951,7 @@ function mapCloudflaredLinuxArch(arch) {
|
|
|
3622
3951
|
return null;
|
|
3623
3952
|
}
|
|
3624
3953
|
function downloadFile(url, destination) {
|
|
3625
|
-
return new Promise((
|
|
3954
|
+
return new Promise((resolve3, reject) => {
|
|
3626
3955
|
const request = (currentUrl) => {
|
|
3627
3956
|
httpsGet(currentUrl, (response) => {
|
|
3628
3957
|
const code = response.statusCode ?? 0;
|
|
@@ -3640,7 +3969,7 @@ function downloadFile(url, destination) {
|
|
|
3640
3969
|
response.pipe(file);
|
|
3641
3970
|
file.on("finish", () => {
|
|
3642
3971
|
file.close();
|
|
3643
|
-
|
|
3972
|
+
resolve3();
|
|
3644
3973
|
});
|
|
3645
3974
|
file.on("error", reject);
|
|
3646
3975
|
}).on("error", reject);
|
|
@@ -3676,6 +4005,14 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
3676
4005
|
return installed;
|
|
3677
4006
|
}
|
|
3678
4007
|
async function shouldInstallCloudflaredInteractively() {
|
|
4008
|
+
if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
|
|
4009
|
+
return false;
|
|
4010
|
+
}
|
|
4011
|
+
hasPromptedCloudflaredInstall = true;
|
|
4012
|
+
await persistCloudflaredInstallPrompted();
|
|
4013
|
+
if (process.platform === "win32") {
|
|
4014
|
+
return false;
|
|
4015
|
+
}
|
|
3679
4016
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3680
4017
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3681
4018
|
return false;
|
|
@@ -3694,6 +4031,9 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3694
4031
|
if (current) {
|
|
3695
4032
|
return current;
|
|
3696
4033
|
}
|
|
4034
|
+
if (process.platform === "win32") {
|
|
4035
|
+
return null;
|
|
4036
|
+
}
|
|
3697
4037
|
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3698
4038
|
if (!installApproved) {
|
|
3699
4039
|
return null;
|
|
@@ -3701,7 +4041,7 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3701
4041
|
return ensureCloudflaredInstalledLinux();
|
|
3702
4042
|
}
|
|
3703
4043
|
function hasCodexAuth() {
|
|
3704
|
-
const codexHome =
|
|
4044
|
+
const codexHome = getCodexHomePath();
|
|
3705
4045
|
return existsSync3(join6(codexHome, "auth.json"));
|
|
3706
4046
|
}
|
|
3707
4047
|
function ensureCodexInstalled() {
|
|
@@ -3800,7 +4140,7 @@ function getAccessibleUrls(port) {
|
|
|
3800
4140
|
return Array.from(urls);
|
|
3801
4141
|
}
|
|
3802
4142
|
async function startCloudflaredTunnel(command, localPort) {
|
|
3803
|
-
return new Promise((
|
|
4143
|
+
return new Promise((resolve3, reject) => {
|
|
3804
4144
|
const child = spawn4(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
3805
4145
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3806
4146
|
});
|
|
@@ -3817,7 +4157,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
3817
4157
|
clearTimeout(timeout);
|
|
3818
4158
|
child.stdout?.off("data", handleData);
|
|
3819
4159
|
child.stderr?.off("data", handleData);
|
|
3820
|
-
|
|
4160
|
+
resolve3({ process: child, url: parsedUrl });
|
|
3821
4161
|
};
|
|
3822
4162
|
const onError = (error) => {
|
|
3823
4163
|
clearTimeout(timeout);
|
|
@@ -3836,7 +4176,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
3836
4176
|
});
|
|
3837
4177
|
}
|
|
3838
4178
|
function listenWithFallback(server, startPort) {
|
|
3839
|
-
return new Promise((
|
|
4179
|
+
return new Promise((resolve3, reject) => {
|
|
3840
4180
|
const attempt = (port) => {
|
|
3841
4181
|
const onError = (error) => {
|
|
3842
4182
|
server.off("listening", onListening);
|
|
@@ -3848,7 +4188,7 @@ function listenWithFallback(server, startPort) {
|
|
|
3848
4188
|
};
|
|
3849
4189
|
const onListening = () => {
|
|
3850
4190
|
server.off("error", onError);
|
|
3851
|
-
|
|
4191
|
+
resolve3(port);
|
|
3852
4192
|
};
|
|
3853
4193
|
server.once("error", onError);
|
|
3854
4194
|
server.once("listening", onListening);
|
|
@@ -3857,8 +4197,72 @@ function listenWithFallback(server, startPort) {
|
|
|
3857
4197
|
attempt(startPort);
|
|
3858
4198
|
});
|
|
3859
4199
|
}
|
|
4200
|
+
function getCodexGlobalStatePath2() {
|
|
4201
|
+
const codexHome = getCodexHomePath();
|
|
4202
|
+
return join6(codexHome, ".codex-global-state.json");
|
|
4203
|
+
}
|
|
4204
|
+
function normalizeUniqueStrings(value) {
|
|
4205
|
+
if (!Array.isArray(value)) return [];
|
|
4206
|
+
const next = [];
|
|
4207
|
+
for (const item of value) {
|
|
4208
|
+
if (typeof item !== "string") continue;
|
|
4209
|
+
const trimmed = item.trim();
|
|
4210
|
+
if (!trimmed || next.includes(trimmed)) continue;
|
|
4211
|
+
next.push(trimmed);
|
|
4212
|
+
}
|
|
4213
|
+
return next;
|
|
4214
|
+
}
|
|
4215
|
+
async function persistLaunchProject(projectPath) {
|
|
4216
|
+
const trimmed = projectPath.trim();
|
|
4217
|
+
if (!trimmed) return;
|
|
4218
|
+
const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
|
|
4219
|
+
const directoryInfo = await stat6(normalizedPath);
|
|
4220
|
+
if (!directoryInfo.isDirectory()) {
|
|
4221
|
+
throw new Error(`Not a directory: ${normalizedPath}`);
|
|
4222
|
+
}
|
|
4223
|
+
const statePath = getCodexGlobalStatePath2();
|
|
4224
|
+
let payload = {};
|
|
4225
|
+
try {
|
|
4226
|
+
const raw = await readFile5(statePath, "utf8");
|
|
4227
|
+
const parsed = JSON.parse(raw);
|
|
4228
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4229
|
+
payload = parsed;
|
|
4230
|
+
}
|
|
4231
|
+
} catch {
|
|
4232
|
+
payload = {};
|
|
4233
|
+
}
|
|
4234
|
+
const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
|
|
4235
|
+
const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
|
|
4236
|
+
payload["electron-saved-workspace-roots"] = [
|
|
4237
|
+
normalizedPath,
|
|
4238
|
+
...roots.filter((value) => value !== normalizedPath)
|
|
4239
|
+
];
|
|
4240
|
+
payload["active-workspace-roots"] = [
|
|
4241
|
+
normalizedPath,
|
|
4242
|
+
...activeRoots.filter((value) => value !== normalizedPath)
|
|
4243
|
+
];
|
|
4244
|
+
await writeFile5(statePath, JSON.stringify(payload), "utf8");
|
|
4245
|
+
}
|
|
4246
|
+
async function addProjectOnly(projectPath) {
|
|
4247
|
+
const trimmed = projectPath.trim();
|
|
4248
|
+
if (!trimmed) {
|
|
4249
|
+
throw new Error("Missing project path");
|
|
4250
|
+
}
|
|
4251
|
+
await persistLaunchProject(trimmed);
|
|
4252
|
+
}
|
|
3860
4253
|
async function startServer(options) {
|
|
3861
4254
|
const version = await readCliVersion();
|
|
4255
|
+
const projectPath = options.projectPath?.trim() ?? "";
|
|
4256
|
+
if (projectPath.length > 0) {
|
|
4257
|
+
try {
|
|
4258
|
+
await persistLaunchProject(projectPath);
|
|
4259
|
+
} catch (error) {
|
|
4260
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4261
|
+
console.warn(`
|
|
4262
|
+
[project] Could not open launch project: ${message}
|
|
4263
|
+
`);
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
3862
4266
|
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
3863
4267
|
if (!hasCodexAuth() && codexCommand) {
|
|
3864
4268
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
@@ -3942,8 +4346,20 @@ async function runLogin() {
|
|
|
3942
4346
|
console.log("\nStarting `codex login`...\n");
|
|
3943
4347
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
3944
4348
|
}
|
|
3945
|
-
program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
|
|
3946
|
-
|
|
4349
|
+
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
|
|
4350
|
+
const rawArgv = process.argv.slice(2);
|
|
4351
|
+
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
4352
|
+
let openProjectOnly = (opts.openProject ?? "").trim();
|
|
4353
|
+
if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
|
|
4354
|
+
openProjectOnly = projectPath.trim();
|
|
4355
|
+
}
|
|
4356
|
+
if (openProjectOnly.length > 0) {
|
|
4357
|
+
await addProjectOnly(openProjectOnly);
|
|
4358
|
+
console.log(`Added project: ${openProjectOnly}`);
|
|
4359
|
+
return;
|
|
4360
|
+
}
|
|
4361
|
+
const launchProject = (projectPath ?? "").trim();
|
|
4362
|
+
await startServer({ ...opts, projectPath: launchProject });
|
|
3947
4363
|
});
|
|
3948
4364
|
program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
|
|
3949
4365
|
program.command("help").description("Show codexui command help").action(() => {
|