codex-team 0.0.2 → 0.0.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 +2 -1
- package/dist/cli.cjs +137 -1
- package/dist/main.cjs +137 -1
- package/dist/main.js +137 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ codexm list [name]
|
|
|
20
20
|
codexm save <name>
|
|
21
21
|
codexm update
|
|
22
22
|
codexm switch <name>
|
|
23
|
+
codexm switch --auto --dry-run
|
|
23
24
|
codexm remove <name> --yes
|
|
24
25
|
codexm rename <old> <new>
|
|
25
26
|
codexm quota refresh [name]
|
|
@@ -33,7 +34,7 @@ Use `--json` on query and mutation commands when you need machine-readable outpu
|
|
|
33
34
|
1. Log into a target account with the native Codex CLI.
|
|
34
35
|
2. Save the current auth snapshot with `codexm save <name>`.
|
|
35
36
|
3. Repeat for other accounts.
|
|
36
|
-
4. Switch between saved accounts with `codexm switch <name
|
|
37
|
+
4. Switch between saved accounts with `codexm switch <name>` or let the tool choose with `codexm switch --auto`.
|
|
37
38
|
5. Refresh and inspect quota usage with `codexm list` or `codexm quota refresh`.
|
|
38
39
|
|
|
39
40
|
## Development
|
package/dist/cli.cjs
CHANGED
|
@@ -910,6 +910,7 @@ Usage:
|
|
|
910
910
|
codexm update [--json]
|
|
911
911
|
codexm quota refresh [name] [--json]
|
|
912
912
|
codexm switch <name> [--json]
|
|
913
|
+
codexm switch --auto [--dry-run] [--json]
|
|
913
914
|
codexm remove <name> [--yes] [--json]
|
|
914
915
|
codexm rename <old> <new> [--json]
|
|
915
916
|
codexm doctor [--json]
|
|
@@ -973,6 +974,69 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
973
974
|
failures: result.failures
|
|
974
975
|
};
|
|
975
976
|
}
|
|
977
|
+
function computeRemainingPercent(usedPercent) {
|
|
978
|
+
if ("number" != typeof usedPercent) return null;
|
|
979
|
+
return Math.max(0, 100 - usedPercent);
|
|
980
|
+
}
|
|
981
|
+
function toAutoSwitchCandidate(account) {
|
|
982
|
+
if ("ok" !== account.status) return null;
|
|
983
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
984
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
985
|
+
if (null === remain5h || null === remain1w) return null;
|
|
986
|
+
return {
|
|
987
|
+
name: account.name,
|
|
988
|
+
account_id: account.account_id,
|
|
989
|
+
plan_type: account.plan_type,
|
|
990
|
+
available: computeAvailability(account),
|
|
991
|
+
refresh_status: "ok",
|
|
992
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
993
|
+
remain_5h: remain5h,
|
|
994
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
995
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
996
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
997
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
998
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function compareNullableDateAscending(left, right) {
|
|
1002
|
+
if (left === right) return 0;
|
|
1003
|
+
if (null === left) return 1;
|
|
1004
|
+
if (null === right) return -1;
|
|
1005
|
+
return left.localeCompare(right);
|
|
1006
|
+
}
|
|
1007
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
1008
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
1009
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1010
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1011
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1012
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1013
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1014
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1015
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1016
|
+
return left.name.localeCompare(right.name);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1020
|
+
const lines = [
|
|
1021
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1022
|
+
`Score: ${candidate.effective_score}`,
|
|
1023
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1024
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1025
|
+
];
|
|
1026
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1027
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
1030
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1031
|
+
const lines = [
|
|
1032
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1033
|
+
`Score: ${candidate.effective_score}`,
|
|
1034
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1035
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1036
|
+
];
|
|
1037
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1038
|
+
return lines.join("\n");
|
|
1039
|
+
}
|
|
976
1040
|
function describeQuotaAccounts(accounts, warnings) {
|
|
977
1041
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
978
1042
|
const table = formatTable(accounts.map((account)=>({
|
|
@@ -1043,12 +1107,17 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1043
1107
|
if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
|
|
1044
1108
|
streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
|
|
1045
1109
|
return await new Promise((resolve)=>{
|
|
1110
|
+
const cleanup = ()=>{
|
|
1111
|
+
streams.stdin.off("data", onData);
|
|
1112
|
+
streams.stdin.pause();
|
|
1113
|
+
};
|
|
1046
1114
|
const onData = (buffer)=>{
|
|
1047
1115
|
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1048
|
-
|
|
1116
|
+
cleanup();
|
|
1049
1117
|
streams.stdout.write("\n");
|
|
1050
1118
|
resolve("y" === answer || "yes" === answer);
|
|
1051
1119
|
};
|
|
1120
|
+
streams.stdin.resume();
|
|
1052
1121
|
streams.stdin.on("data", onData);
|
|
1053
1122
|
});
|
|
1054
1123
|
}
|
|
@@ -1145,7 +1214,74 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1145
1214
|
}
|
|
1146
1215
|
case "switch":
|
|
1147
1216
|
{
|
|
1217
|
+
const auto = parsed.flags.has("--auto");
|
|
1218
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1148
1219
|
const name = parsed.positionals[0];
|
|
1220
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1221
|
+
if (auto) {
|
|
1222
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1223
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1224
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1225
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1226
|
+
const selected = candidates[0];
|
|
1227
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1228
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1229
|
+
if (dryRun) {
|
|
1230
|
+
const payload = {
|
|
1231
|
+
ok: true,
|
|
1232
|
+
action: "switch",
|
|
1233
|
+
mode: "auto",
|
|
1234
|
+
dry_run: true,
|
|
1235
|
+
selected,
|
|
1236
|
+
candidates,
|
|
1237
|
+
warnings
|
|
1238
|
+
};
|
|
1239
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1240
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1241
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1242
|
+
}
|
|
1243
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1244
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1245
|
+
const payload = {
|
|
1246
|
+
ok: true,
|
|
1247
|
+
action: "switch",
|
|
1248
|
+
mode: "auto",
|
|
1249
|
+
skipped: true,
|
|
1250
|
+
reason: "already_current_best",
|
|
1251
|
+
account: {
|
|
1252
|
+
name: selected.name,
|
|
1253
|
+
account_id: selected.account_id
|
|
1254
|
+
},
|
|
1255
|
+
selected,
|
|
1256
|
+
candidates,
|
|
1257
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1258
|
+
warnings
|
|
1259
|
+
};
|
|
1260
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1261
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1262
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1263
|
+
}
|
|
1264
|
+
const result = await store.switchAccount(selected.name);
|
|
1265
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1266
|
+
const payload = {
|
|
1267
|
+
ok: true,
|
|
1268
|
+
action: "switch",
|
|
1269
|
+
mode: "auto",
|
|
1270
|
+
account: {
|
|
1271
|
+
name: result.account.name,
|
|
1272
|
+
account_id: result.account.account_id,
|
|
1273
|
+
auth_mode: result.account.auth_mode
|
|
1274
|
+
},
|
|
1275
|
+
selected,
|
|
1276
|
+
candidates,
|
|
1277
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1278
|
+
backup_path: result.backup_path,
|
|
1279
|
+
warnings: result.warnings
|
|
1280
|
+
};
|
|
1281
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1282
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1283
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1284
|
+
}
|
|
1149
1285
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1150
1286
|
const result = await store.switchAccount(name);
|
|
1151
1287
|
let quota = null;
|
package/dist/main.cjs
CHANGED
|
@@ -940,6 +940,7 @@ Usage:
|
|
|
940
940
|
codexm update [--json]
|
|
941
941
|
codexm quota refresh [name] [--json]
|
|
942
942
|
codexm switch <name> [--json]
|
|
943
|
+
codexm switch --auto [--dry-run] [--json]
|
|
943
944
|
codexm remove <name> [--yes] [--json]
|
|
944
945
|
codexm rename <old> <new> [--json]
|
|
945
946
|
codexm doctor [--json]
|
|
@@ -1003,6 +1004,69 @@ function toCliQuotaRefreshResult(result) {
|
|
|
1003
1004
|
failures: result.failures
|
|
1004
1005
|
};
|
|
1005
1006
|
}
|
|
1007
|
+
function computeRemainingPercent(usedPercent) {
|
|
1008
|
+
if ("number" != typeof usedPercent) return null;
|
|
1009
|
+
return Math.max(0, 100 - usedPercent);
|
|
1010
|
+
}
|
|
1011
|
+
function toAutoSwitchCandidate(account) {
|
|
1012
|
+
if ("ok" !== account.status) return null;
|
|
1013
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
1014
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
1015
|
+
if (null === remain5h || null === remain1w) return null;
|
|
1016
|
+
return {
|
|
1017
|
+
name: account.name,
|
|
1018
|
+
account_id: account.account_id,
|
|
1019
|
+
plan_type: account.plan_type,
|
|
1020
|
+
available: computeAvailability(account),
|
|
1021
|
+
refresh_status: "ok",
|
|
1022
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
1023
|
+
remain_5h: remain5h,
|
|
1024
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
1025
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
1026
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
1027
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
1028
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function compareNullableDateAscending(left, right) {
|
|
1032
|
+
if (left === right) return 0;
|
|
1033
|
+
if (null === left) return 1;
|
|
1034
|
+
if (null === right) return -1;
|
|
1035
|
+
return left.localeCompare(right);
|
|
1036
|
+
}
|
|
1037
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
1038
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
1039
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1040
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1041
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1042
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1043
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1044
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1045
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1046
|
+
return left.name.localeCompare(right.name);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1050
|
+
const lines = [
|
|
1051
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1052
|
+
`Score: ${candidate.effective_score}`,
|
|
1053
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1054
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1055
|
+
];
|
|
1056
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1057
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1058
|
+
return lines.join("\n");
|
|
1059
|
+
}
|
|
1060
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1061
|
+
const lines = [
|
|
1062
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1063
|
+
`Score: ${candidate.effective_score}`,
|
|
1064
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1065
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1066
|
+
];
|
|
1067
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1068
|
+
return lines.join("\n");
|
|
1069
|
+
}
|
|
1006
1070
|
function describeQuotaAccounts(accounts, warnings) {
|
|
1007
1071
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
1008
1072
|
const table = formatTable(accounts.map((account)=>({
|
|
@@ -1073,12 +1137,17 @@ async function confirmRemoval(name, streams) {
|
|
|
1073
1137
|
if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
|
|
1074
1138
|
streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
|
|
1075
1139
|
return await new Promise((resolve)=>{
|
|
1140
|
+
const cleanup = ()=>{
|
|
1141
|
+
streams.stdin.off("data", onData);
|
|
1142
|
+
streams.stdin.pause();
|
|
1143
|
+
};
|
|
1076
1144
|
const onData = (buffer)=>{
|
|
1077
1145
|
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1078
|
-
|
|
1146
|
+
cleanup();
|
|
1079
1147
|
streams.stdout.write("\n");
|
|
1080
1148
|
resolve("y" === answer || "yes" === answer);
|
|
1081
1149
|
};
|
|
1150
|
+
streams.stdin.resume();
|
|
1082
1151
|
streams.stdin.on("data", onData);
|
|
1083
1152
|
});
|
|
1084
1153
|
}
|
|
@@ -1175,7 +1244,74 @@ async function runCli(argv, options = {}) {
|
|
|
1175
1244
|
}
|
|
1176
1245
|
case "switch":
|
|
1177
1246
|
{
|
|
1247
|
+
const auto = parsed.flags.has("--auto");
|
|
1248
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1178
1249
|
const name = parsed.positionals[0];
|
|
1250
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1251
|
+
if (auto) {
|
|
1252
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1253
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1254
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1255
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1256
|
+
const selected = candidates[0];
|
|
1257
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1258
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1259
|
+
if (dryRun) {
|
|
1260
|
+
const payload = {
|
|
1261
|
+
ok: true,
|
|
1262
|
+
action: "switch",
|
|
1263
|
+
mode: "auto",
|
|
1264
|
+
dry_run: true,
|
|
1265
|
+
selected,
|
|
1266
|
+
candidates,
|
|
1267
|
+
warnings
|
|
1268
|
+
};
|
|
1269
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1270
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1271
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1272
|
+
}
|
|
1273
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1274
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1275
|
+
const payload = {
|
|
1276
|
+
ok: true,
|
|
1277
|
+
action: "switch",
|
|
1278
|
+
mode: "auto",
|
|
1279
|
+
skipped: true,
|
|
1280
|
+
reason: "already_current_best",
|
|
1281
|
+
account: {
|
|
1282
|
+
name: selected.name,
|
|
1283
|
+
account_id: selected.account_id
|
|
1284
|
+
},
|
|
1285
|
+
selected,
|
|
1286
|
+
candidates,
|
|
1287
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1288
|
+
warnings
|
|
1289
|
+
};
|
|
1290
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1291
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1292
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1293
|
+
}
|
|
1294
|
+
const result = await store.switchAccount(selected.name);
|
|
1295
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1296
|
+
const payload = {
|
|
1297
|
+
ok: true,
|
|
1298
|
+
action: "switch",
|
|
1299
|
+
mode: "auto",
|
|
1300
|
+
account: {
|
|
1301
|
+
name: result.account.name,
|
|
1302
|
+
account_id: result.account.account_id,
|
|
1303
|
+
auth_mode: result.account.auth_mode
|
|
1304
|
+
},
|
|
1305
|
+
selected,
|
|
1306
|
+
candidates,
|
|
1307
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1308
|
+
backup_path: result.backup_path,
|
|
1309
|
+
warnings: result.warnings
|
|
1310
|
+
};
|
|
1311
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1312
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1313
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1314
|
+
}
|
|
1179
1315
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1180
1316
|
const result = await store.switchAccount(name);
|
|
1181
1317
|
let quota = null;
|
package/dist/main.js
CHANGED
|
@@ -900,6 +900,7 @@ Usage:
|
|
|
900
900
|
codexm update [--json]
|
|
901
901
|
codexm quota refresh [name] [--json]
|
|
902
902
|
codexm switch <name> [--json]
|
|
903
|
+
codexm switch --auto [--dry-run] [--json]
|
|
903
904
|
codexm remove <name> [--yes] [--json]
|
|
904
905
|
codexm rename <old> <new> [--json]
|
|
905
906
|
codexm doctor [--json]
|
|
@@ -963,6 +964,69 @@ function toCliQuotaRefreshResult(result) {
|
|
|
963
964
|
failures: result.failures
|
|
964
965
|
};
|
|
965
966
|
}
|
|
967
|
+
function computeRemainingPercent(usedPercent) {
|
|
968
|
+
if ("number" != typeof usedPercent) return null;
|
|
969
|
+
return Math.max(0, 100 - usedPercent);
|
|
970
|
+
}
|
|
971
|
+
function toAutoSwitchCandidate(account) {
|
|
972
|
+
if ("ok" !== account.status) return null;
|
|
973
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
974
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
975
|
+
if (null === remain5h || null === remain1w) return null;
|
|
976
|
+
return {
|
|
977
|
+
name: account.name,
|
|
978
|
+
account_id: account.account_id,
|
|
979
|
+
plan_type: account.plan_type,
|
|
980
|
+
available: computeAvailability(account),
|
|
981
|
+
refresh_status: "ok",
|
|
982
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
983
|
+
remain_5h: remain5h,
|
|
984
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
985
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
986
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
987
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
988
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function compareNullableDateAscending(left, right) {
|
|
992
|
+
if (left === right) return 0;
|
|
993
|
+
if (null === left) return 1;
|
|
994
|
+
if (null === right) return -1;
|
|
995
|
+
return left.localeCompare(right);
|
|
996
|
+
}
|
|
997
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
998
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
999
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1000
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1001
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1002
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1003
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1004
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1005
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1006
|
+
return left.name.localeCompare(right.name);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1010
|
+
const lines = [
|
|
1011
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1012
|
+
`Score: ${candidate.effective_score}`,
|
|
1013
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1014
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1015
|
+
];
|
|
1016
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1017
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1018
|
+
return lines.join("\n");
|
|
1019
|
+
}
|
|
1020
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1021
|
+
const lines = [
|
|
1022
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1023
|
+
`Score: ${candidate.effective_score}`,
|
|
1024
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1025
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1026
|
+
];
|
|
1027
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
966
1030
|
function describeQuotaAccounts(accounts, warnings) {
|
|
967
1031
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
968
1032
|
const table = formatTable(accounts.map((account)=>({
|
|
@@ -1033,12 +1097,17 @@ async function confirmRemoval(name, streams) {
|
|
|
1033
1097
|
if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
|
|
1034
1098
|
streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
|
|
1035
1099
|
return await new Promise((resolve)=>{
|
|
1100
|
+
const cleanup = ()=>{
|
|
1101
|
+
streams.stdin.off("data", onData);
|
|
1102
|
+
streams.stdin.pause();
|
|
1103
|
+
};
|
|
1036
1104
|
const onData = (buffer)=>{
|
|
1037
1105
|
const answer = buffer.toString("utf8").trim().toLowerCase();
|
|
1038
|
-
|
|
1106
|
+
cleanup();
|
|
1039
1107
|
streams.stdout.write("\n");
|
|
1040
1108
|
resolve("y" === answer || "yes" === answer);
|
|
1041
1109
|
};
|
|
1110
|
+
streams.stdin.resume();
|
|
1042
1111
|
streams.stdin.on("data", onData);
|
|
1043
1112
|
});
|
|
1044
1113
|
}
|
|
@@ -1135,7 +1204,74 @@ async function runCli(argv, options = {}) {
|
|
|
1135
1204
|
}
|
|
1136
1205
|
case "switch":
|
|
1137
1206
|
{
|
|
1207
|
+
const auto = parsed.flags.has("--auto");
|
|
1208
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1138
1209
|
const name = parsed.positionals[0];
|
|
1210
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1211
|
+
if (auto) {
|
|
1212
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1213
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1214
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1215
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1216
|
+
const selected = candidates[0];
|
|
1217
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1218
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1219
|
+
if (dryRun) {
|
|
1220
|
+
const payload = {
|
|
1221
|
+
ok: true,
|
|
1222
|
+
action: "switch",
|
|
1223
|
+
mode: "auto",
|
|
1224
|
+
dry_run: true,
|
|
1225
|
+
selected,
|
|
1226
|
+
candidates,
|
|
1227
|
+
warnings
|
|
1228
|
+
};
|
|
1229
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1230
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1231
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1232
|
+
}
|
|
1233
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1234
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1235
|
+
const payload = {
|
|
1236
|
+
ok: true,
|
|
1237
|
+
action: "switch",
|
|
1238
|
+
mode: "auto",
|
|
1239
|
+
skipped: true,
|
|
1240
|
+
reason: "already_current_best",
|
|
1241
|
+
account: {
|
|
1242
|
+
name: selected.name,
|
|
1243
|
+
account_id: selected.account_id
|
|
1244
|
+
},
|
|
1245
|
+
selected,
|
|
1246
|
+
candidates,
|
|
1247
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1248
|
+
warnings
|
|
1249
|
+
};
|
|
1250
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1251
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1252
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1253
|
+
}
|
|
1254
|
+
const result = await store.switchAccount(selected.name);
|
|
1255
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1256
|
+
const payload = {
|
|
1257
|
+
ok: true,
|
|
1258
|
+
action: "switch",
|
|
1259
|
+
mode: "auto",
|
|
1260
|
+
account: {
|
|
1261
|
+
name: result.account.name,
|
|
1262
|
+
account_id: result.account.account_id,
|
|
1263
|
+
auth_mode: result.account.auth_mode
|
|
1264
|
+
},
|
|
1265
|
+
selected,
|
|
1266
|
+
candidates,
|
|
1267
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1268
|
+
backup_path: result.backup_path,
|
|
1269
|
+
warnings: result.warnings
|
|
1270
|
+
};
|
|
1271
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1272
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1273
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1274
|
+
}
|
|
1139
1275
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1140
1276
|
const result = await store.switchAccount(name);
|
|
1141
1277
|
let quota = null;
|