codex-team 0.0.2 → 0.0.3
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 +131 -0
- package/dist/main.cjs +131 -0
- package/dist/main.js +131 -0
- 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)=>({
|
|
@@ -1145,7 +1209,74 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1145
1209
|
}
|
|
1146
1210
|
case "switch":
|
|
1147
1211
|
{
|
|
1212
|
+
const auto = parsed.flags.has("--auto");
|
|
1213
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1148
1214
|
const name = parsed.positionals[0];
|
|
1215
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1216
|
+
if (auto) {
|
|
1217
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1218
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1219
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1220
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1221
|
+
const selected = candidates[0];
|
|
1222
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1223
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1224
|
+
if (dryRun) {
|
|
1225
|
+
const payload = {
|
|
1226
|
+
ok: true,
|
|
1227
|
+
action: "switch",
|
|
1228
|
+
mode: "auto",
|
|
1229
|
+
dry_run: true,
|
|
1230
|
+
selected,
|
|
1231
|
+
candidates,
|
|
1232
|
+
warnings
|
|
1233
|
+
};
|
|
1234
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1235
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1236
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1237
|
+
}
|
|
1238
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1239
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1240
|
+
const payload = {
|
|
1241
|
+
ok: true,
|
|
1242
|
+
action: "switch",
|
|
1243
|
+
mode: "auto",
|
|
1244
|
+
skipped: true,
|
|
1245
|
+
reason: "already_current_best",
|
|
1246
|
+
account: {
|
|
1247
|
+
name: selected.name,
|
|
1248
|
+
account_id: selected.account_id
|
|
1249
|
+
},
|
|
1250
|
+
selected,
|
|
1251
|
+
candidates,
|
|
1252
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1253
|
+
warnings
|
|
1254
|
+
};
|
|
1255
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1256
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1257
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1258
|
+
}
|
|
1259
|
+
const result = await store.switchAccount(selected.name);
|
|
1260
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1261
|
+
const payload = {
|
|
1262
|
+
ok: true,
|
|
1263
|
+
action: "switch",
|
|
1264
|
+
mode: "auto",
|
|
1265
|
+
account: {
|
|
1266
|
+
name: result.account.name,
|
|
1267
|
+
account_id: result.account.account_id,
|
|
1268
|
+
auth_mode: result.account.auth_mode
|
|
1269
|
+
},
|
|
1270
|
+
selected,
|
|
1271
|
+
candidates,
|
|
1272
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1273
|
+
backup_path: result.backup_path,
|
|
1274
|
+
warnings: result.warnings
|
|
1275
|
+
};
|
|
1276
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1277
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1278
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1279
|
+
}
|
|
1149
1280
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1150
1281
|
const result = await store.switchAccount(name);
|
|
1151
1282
|
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)=>({
|
|
@@ -1175,7 +1239,74 @@ async function runCli(argv, options = {}) {
|
|
|
1175
1239
|
}
|
|
1176
1240
|
case "switch":
|
|
1177
1241
|
{
|
|
1242
|
+
const auto = parsed.flags.has("--auto");
|
|
1243
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1178
1244
|
const name = parsed.positionals[0];
|
|
1245
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1246
|
+
if (auto) {
|
|
1247
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1248
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1249
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1250
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1251
|
+
const selected = candidates[0];
|
|
1252
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1253
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1254
|
+
if (dryRun) {
|
|
1255
|
+
const payload = {
|
|
1256
|
+
ok: true,
|
|
1257
|
+
action: "switch",
|
|
1258
|
+
mode: "auto",
|
|
1259
|
+
dry_run: true,
|
|
1260
|
+
selected,
|
|
1261
|
+
candidates,
|
|
1262
|
+
warnings
|
|
1263
|
+
};
|
|
1264
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1265
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1266
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1267
|
+
}
|
|
1268
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1269
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1270
|
+
const payload = {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
action: "switch",
|
|
1273
|
+
mode: "auto",
|
|
1274
|
+
skipped: true,
|
|
1275
|
+
reason: "already_current_best",
|
|
1276
|
+
account: {
|
|
1277
|
+
name: selected.name,
|
|
1278
|
+
account_id: selected.account_id
|
|
1279
|
+
},
|
|
1280
|
+
selected,
|
|
1281
|
+
candidates,
|
|
1282
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1283
|
+
warnings
|
|
1284
|
+
};
|
|
1285
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1286
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1287
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1288
|
+
}
|
|
1289
|
+
const result = await store.switchAccount(selected.name);
|
|
1290
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1291
|
+
const payload = {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
action: "switch",
|
|
1294
|
+
mode: "auto",
|
|
1295
|
+
account: {
|
|
1296
|
+
name: result.account.name,
|
|
1297
|
+
account_id: result.account.account_id,
|
|
1298
|
+
auth_mode: result.account.auth_mode
|
|
1299
|
+
},
|
|
1300
|
+
selected,
|
|
1301
|
+
candidates,
|
|
1302
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1303
|
+
backup_path: result.backup_path,
|
|
1304
|
+
warnings: result.warnings
|
|
1305
|
+
};
|
|
1306
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1307
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1308
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1309
|
+
}
|
|
1179
1310
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1180
1311
|
const result = await store.switchAccount(name);
|
|
1181
1312
|
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)=>({
|
|
@@ -1135,7 +1199,74 @@ async function runCli(argv, options = {}) {
|
|
|
1135
1199
|
}
|
|
1136
1200
|
case "switch":
|
|
1137
1201
|
{
|
|
1202
|
+
const auto = parsed.flags.has("--auto");
|
|
1203
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1138
1204
|
const name = parsed.positionals[0];
|
|
1205
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1206
|
+
if (auto) {
|
|
1207
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1208
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1209
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1210
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1211
|
+
const selected = candidates[0];
|
|
1212
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1213
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1214
|
+
if (dryRun) {
|
|
1215
|
+
const payload = {
|
|
1216
|
+
ok: true,
|
|
1217
|
+
action: "switch",
|
|
1218
|
+
mode: "auto",
|
|
1219
|
+
dry_run: true,
|
|
1220
|
+
selected,
|
|
1221
|
+
candidates,
|
|
1222
|
+
warnings
|
|
1223
|
+
};
|
|
1224
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1225
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1226
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1227
|
+
}
|
|
1228
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1229
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1230
|
+
const payload = {
|
|
1231
|
+
ok: true,
|
|
1232
|
+
action: "switch",
|
|
1233
|
+
mode: "auto",
|
|
1234
|
+
skipped: true,
|
|
1235
|
+
reason: "already_current_best",
|
|
1236
|
+
account: {
|
|
1237
|
+
name: selected.name,
|
|
1238
|
+
account_id: selected.account_id
|
|
1239
|
+
},
|
|
1240
|
+
selected,
|
|
1241
|
+
candidates,
|
|
1242
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1243
|
+
warnings
|
|
1244
|
+
};
|
|
1245
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1246
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1247
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1248
|
+
}
|
|
1249
|
+
const result = await store.switchAccount(selected.name);
|
|
1250
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1251
|
+
const payload = {
|
|
1252
|
+
ok: true,
|
|
1253
|
+
action: "switch",
|
|
1254
|
+
mode: "auto",
|
|
1255
|
+
account: {
|
|
1256
|
+
name: result.account.name,
|
|
1257
|
+
account_id: result.account.account_id,
|
|
1258
|
+
auth_mode: result.account.auth_mode
|
|
1259
|
+
},
|
|
1260
|
+
selected,
|
|
1261
|
+
candidates,
|
|
1262
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1263
|
+
backup_path: result.backup_path,
|
|
1264
|
+
warnings: result.warnings
|
|
1265
|
+
};
|
|
1266
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1267
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1268
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1269
|
+
}
|
|
1139
1270
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1140
1271
|
const result = await store.switchAccount(name);
|
|
1141
1272
|
let quota = null;
|