codexuse-cli 3.9.2 → 3.9.7

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/index.js CHANGED
@@ -2094,6 +2094,117 @@ function normalizeCommitMessagePrompt(value) {
2094
2094
  return normalized.length > 0 ? normalized : null;
2095
2095
  }
2096
2096
 
2097
+ // ../../packages/contracts/src/settings/auto-roll.ts
2098
+ var DEFAULT_AUTO_ROLL_ENABLED = false;
2099
+ var DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD = 15;
2100
+ var DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD = 5;
2101
+ var DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL = false;
2102
+ var DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL = false;
2103
+ var DEFAULT_AUTO_ROLL_PRIORITY_ORDER = [];
2104
+ var DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED = false;
2105
+ var DEFAULT_LOW_REMAINING_NOTIFICATION_THRESHOLD = 1;
2106
+ var AUTO_ROLL_SWITCH_REMAINING_MIN = 0;
2107
+ var AUTO_ROLL_SWITCH_REMAINING_MAX = 50;
2108
+ var AUTO_ROLL_REARM_REMAINING_MAX = 100;
2109
+ var LOW_REMAINING_NOTIFICATION_MIN = 1;
2110
+ var LOW_REMAINING_NOTIFICATION_MAX = 50;
2111
+ function clampNumber(value, min, max) {
2112
+ return Math.min(max, Math.max(min, value));
2113
+ }
2114
+ function resolveFiniteNumber(value, fallback) {
2115
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2116
+ }
2117
+ function legacyUsedThresholdToRemaining(value, fallbackUsed) {
2118
+ const used = clampNumber(resolveFiniteNumber(value, fallbackUsed), 0, 100);
2119
+ return 100 - used;
2120
+ }
2121
+ function sanitizeAutoRollSwitchRemainingThreshold(value) {
2122
+ const numeric = resolveFiniteNumber(
2123
+ value,
2124
+ DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD
2125
+ );
2126
+ return clampNumber(
2127
+ numeric,
2128
+ AUTO_ROLL_SWITCH_REMAINING_MIN,
2129
+ AUTO_ROLL_SWITCH_REMAINING_MAX
2130
+ );
2131
+ }
2132
+ function sanitizeAutoRollRearmRemainingThreshold(value, switchRemainingThreshold) {
2133
+ const sanitizedSwitch = sanitizeAutoRollSwitchRemainingThreshold(
2134
+ switchRemainingThreshold
2135
+ );
2136
+ const numeric = resolveFiniteNumber(
2137
+ value,
2138
+ DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD
2139
+ );
2140
+ return clampNumber(
2141
+ numeric,
2142
+ sanitizedSwitch + 1,
2143
+ AUTO_ROLL_REARM_REMAINING_MAX
2144
+ );
2145
+ }
2146
+ function sanitizeAutoRollThresholds(rearmRemainingThreshold, switchRemainingThreshold) {
2147
+ const sanitizedSwitch = sanitizeAutoRollSwitchRemainingThreshold(
2148
+ switchRemainingThreshold
2149
+ );
2150
+ const sanitizedRearm = sanitizeAutoRollRearmRemainingThreshold(
2151
+ rearmRemainingThreshold,
2152
+ sanitizedSwitch
2153
+ );
2154
+ return {
2155
+ rearmRemainingThreshold: sanitizedRearm,
2156
+ switchRemainingThreshold: sanitizedSwitch
2157
+ };
2158
+ }
2159
+ function sanitizeAutoRollPriorityOrder(value) {
2160
+ if (!Array.isArray(value)) {
2161
+ return [...DEFAULT_AUTO_ROLL_PRIORITY_ORDER];
2162
+ }
2163
+ const seen = /* @__PURE__ */ new Set();
2164
+ const normalized = [];
2165
+ for (const item of value) {
2166
+ if (typeof item !== "string") {
2167
+ continue;
2168
+ }
2169
+ const trimmed = item.trim();
2170
+ if (!trimmed || seen.has(trimmed)) {
2171
+ continue;
2172
+ }
2173
+ seen.add(trimmed);
2174
+ normalized.push(trimmed);
2175
+ }
2176
+ return normalized;
2177
+ }
2178
+ function sanitizeLowRemainingNotificationThreshold(value) {
2179
+ const numeric = resolveFiniteNumber(value, DEFAULT_LOW_REMAINING_NOTIFICATION_THRESHOLD);
2180
+ return clampNumber(
2181
+ numeric,
2182
+ LOW_REMAINING_NOTIFICATION_MIN,
2183
+ LOW_REMAINING_NOTIFICATION_MAX
2184
+ );
2185
+ }
2186
+ function normalizeAutoRollSettings(raw) {
2187
+ const enabled = typeof raw?.enabled === "boolean" ? raw.enabled : DEFAULT_AUTO_ROLL_ENABLED;
2188
+ const rawSwitchRemaining = typeof raw?.switchRemainingThreshold === "number" ? raw.switchRemainingThreshold : legacyUsedThresholdToRemaining(raw?.switchThreshold, 95);
2189
+ const rawRearmRemaining = typeof raw?.rearmRemainingThreshold === "number" ? raw.rearmRemainingThreshold : legacyUsedThresholdToRemaining(raw?.warningThreshold, 85);
2190
+ const {
2191
+ rearmRemainingThreshold: normalizedRearm,
2192
+ switchRemainingThreshold: normalizedSwitch
2193
+ } = sanitizeAutoRollThresholds(rawRearmRemaining, rawSwitchRemaining);
2194
+ return {
2195
+ enabled,
2196
+ rearmRemainingThreshold: normalizedRearm,
2197
+ switchRemainingThreshold: normalizedSwitch,
2198
+ restartOfficialCodexOnAutoRoll: raw?.restartOfficialCodexOnAutoRoll === true ? true : DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL,
2199
+ launchOfficialCodexWhenClosedOnAutoRoll: raw?.launchOfficialCodexWhenClosedOnAutoRoll === true ? true : DEFAULT_LAUNCH_OFFICIAL_CODEX_WHEN_CLOSED_ON_AUTO_ROLL,
2200
+ priorityOrder: sanitizeAutoRollPriorityOrder(raw?.priorityOrder),
2201
+ lowRemainingNotificationEnabled: raw?.lowRemainingNotificationEnabled === true ? true : DEFAULT_LOW_REMAINING_NOTIFICATION_ENABLED,
2202
+ lowRemainingNotificationThreshold: sanitizeLowRemainingNotificationThreshold(
2203
+ typeof raw?.lowRemainingNotificationThreshold === "number" ? raw.lowRemainingNotificationThreshold : Number.NaN
2204
+ )
2205
+ };
2206
+ }
2207
+
2097
2208
  // ../../packages/runtime-app-state/src/storage/documents.ts
2098
2209
  var import_node_fs = require("fs");
2099
2210
  var import_node_path = __toESM(require("path"), 1);
@@ -2351,9 +2462,23 @@ function createDefaultAppState() {
2351
2462
  schemaVersion: 1,
2352
2463
  autoRoll: {
2353
2464
  enabled: false,
2354
- warningThreshold: 85,
2355
- switchThreshold: 95,
2356
- restartOfficialCodexOnAutoRoll: false
2465
+ rearmRemainingThreshold: 15,
2466
+ switchRemainingThreshold: 5,
2467
+ restartOfficialCodexOnAutoRoll: false,
2468
+ launchOfficialCodexWhenClosedOnAutoRoll: false,
2469
+ priorityOrder: [],
2470
+ lowRemainingNotificationEnabled: false,
2471
+ lowRemainingNotificationThreshold: 1
2472
+ },
2473
+ officialCodex: {
2474
+ lastProfileSwitchAt: null,
2475
+ lastProfileSwitchProfileKey: null,
2476
+ lastVerifiedLaunchAt: null,
2477
+ lastVerifiedLaunchProfileKey: null,
2478
+ lastObservedPid: null,
2479
+ lastRestartStatus: null,
2480
+ lastRestartReason: null,
2481
+ instancesByProfileName: {}
2357
2482
  },
2358
2483
  app: {
2359
2484
  lastAppVersion: null,
@@ -2454,34 +2579,72 @@ function asString(value) {
2454
2579
  const trimmed = value.trim();
2455
2580
  return trimmed.length > 0 ? trimmed : null;
2456
2581
  }
2582
+ function asNumberOrNull(value) {
2583
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2584
+ }
2457
2585
  function normalizeAppState(raw) {
2458
2586
  const defaults = createDefaultAppState();
2459
2587
  if (!isRecord2(raw)) {
2460
2588
  return defaults;
2461
2589
  }
2590
+ const rawAutoRoll = isRecord2(raw.autoRoll) ? raw.autoRoll : void 0;
2462
2591
  const next = deepMerge(defaults, raw);
2463
2592
  const merged = clone2(next);
2464
2593
  merged.schemaVersion = 1;
2465
- if (typeof merged.autoRoll.enabled !== "boolean") {
2466
- merged.autoRoll.enabled = false;
2594
+ merged.autoRoll = normalizeAutoRollSettings(rawAutoRoll);
2595
+ if (!isRecord2(merged.officialCodex)) {
2596
+ merged.officialCodex = clone2(defaults.officialCodex);
2467
2597
  }
2468
- if (!Number.isFinite(merged.autoRoll.warningThreshold)) {
2469
- merged.autoRoll.warningThreshold = 85;
2470
- }
2471
- if (!Number.isFinite(merged.autoRoll.switchThreshold)) {
2472
- merged.autoRoll.switchThreshold = 95;
2473
- }
2474
- if (merged.autoRoll.warningThreshold < 50 || merged.autoRoll.warningThreshold > 99) {
2475
- merged.autoRoll.warningThreshold = 85;
2476
- }
2477
- if (merged.autoRoll.switchThreshold <= merged.autoRoll.warningThreshold || merged.autoRoll.switchThreshold > 100) {
2478
- merged.autoRoll.switchThreshold = Math.min(
2479
- 100,
2480
- Math.max(merged.autoRoll.warningThreshold + 1, 95)
2481
- );
2482
- }
2483
- if (typeof merged.autoRoll.restartOfficialCodexOnAutoRoll !== "boolean") {
2484
- merged.autoRoll.restartOfficialCodexOnAutoRoll = false;
2598
+ merged.officialCodex.lastProfileSwitchAt = asNumberOrNull(
2599
+ merged.officialCodex.lastProfileSwitchAt
2600
+ );
2601
+ merged.officialCodex.lastProfileSwitchProfileKey = asString(
2602
+ merged.officialCodex.lastProfileSwitchProfileKey
2603
+ );
2604
+ merged.officialCodex.lastVerifiedLaunchAt = asNumberOrNull(
2605
+ merged.officialCodex.lastVerifiedLaunchAt
2606
+ );
2607
+ merged.officialCodex.lastVerifiedLaunchProfileKey = asString(
2608
+ merged.officialCodex.lastVerifiedLaunchProfileKey
2609
+ );
2610
+ merged.officialCodex.lastObservedPid = asNumberOrNull(
2611
+ merged.officialCodex.lastObservedPid
2612
+ );
2613
+ merged.officialCodex.lastRestartStatus = asString(
2614
+ merged.officialCodex.lastRestartStatus
2615
+ );
2616
+ merged.officialCodex.lastRestartReason = asString(
2617
+ merged.officialCodex.lastRestartReason
2618
+ );
2619
+ if (!isRecord2(merged.officialCodex.instancesByProfileName)) {
2620
+ merged.officialCodex.instancesByProfileName = {};
2621
+ } else {
2622
+ const nextInstances = {};
2623
+ for (const [key, value] of Object.entries(
2624
+ merged.officialCodex.instancesByProfileName
2625
+ )) {
2626
+ if (!isRecord2(value)) {
2627
+ continue;
2628
+ }
2629
+ const profileName = asString(value.profileName) ?? asString(key);
2630
+ if (!profileName) {
2631
+ continue;
2632
+ }
2633
+ nextInstances[profileName] = {
2634
+ profileName,
2635
+ profileKey: asString(value.profileKey),
2636
+ profileHome: asString(value.profileHome),
2637
+ appPath: asString(value.appPath),
2638
+ bundleId: asString(value.bundleId),
2639
+ pid: asNumberOrNull(value.pid),
2640
+ appServerPid: asNumberOrNull(value.appServerPid),
2641
+ launchedAt: asNumberOrNull(value.launchedAt),
2642
+ lastVerifiedAt: asNumberOrNull(value.lastVerifiedAt),
2643
+ lastStatus: asString(value.lastStatus),
2644
+ lastError: asString(value.lastError)
2645
+ };
2646
+ }
2647
+ merged.officialCodex.instancesByProfileName = nextInstances;
2485
2648
  }
2486
2649
  merged.app.lastAppVersion = asString(merged.app.lastAppVersion);
2487
2650
  merged.app.pendingUpdateVersion = asString(merged.app.pendingUpdateVersion);
@@ -2977,59 +3140,6 @@ async function writeAppSettings(settings, onUpdated) {
2977
3140
  // ../../packages/runtime-profiles/src/license/service.ts
2978
3141
  var import_node_crypto2 = __toESM(require("crypto"), 1);
2979
3142
 
2980
- // ../../packages/contracts/src/settings/auto-roll.ts
2981
- var DEFAULT_AUTO_ROLL_ENABLED = false;
2982
- var DEFAULT_AUTO_ROLL_WARNING_THRESHOLD = 85;
2983
- var DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD = 95;
2984
- var DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL = false;
2985
- var AUTO_ROLL_WARNING_MIN = 50;
2986
- var AUTO_ROLL_WARNING_MAX = 99;
2987
- var AUTO_ROLL_SWITCH_MAX = 100;
2988
- function clampNumber(value, min, max) {
2989
- return Math.min(max, Math.max(min, value));
2990
- }
2991
- function resolveFiniteNumber(value, fallback) {
2992
- return Number.isFinite(value) ? value : fallback;
2993
- }
2994
- function sanitizeAutoRollWarningThreshold(value) {
2995
- const numeric = resolveFiniteNumber(value, DEFAULT_AUTO_ROLL_WARNING_THRESHOLD);
2996
- return clampNumber(numeric, AUTO_ROLL_WARNING_MIN, AUTO_ROLL_WARNING_MAX);
2997
- }
2998
- function sanitizeAutoRollSwitchThreshold(value, warningThreshold) {
2999
- const sanitizedWarning = sanitizeAutoRollWarningThreshold(warningThreshold);
3000
- const numeric = resolveFiniteNumber(value, DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD);
3001
- return clampNumber(numeric, sanitizedWarning + 1, AUTO_ROLL_SWITCH_MAX);
3002
- }
3003
- function sanitizeAutoRollThresholds(warningThreshold, switchThreshold) {
3004
- const sanitizedWarning = sanitizeAutoRollWarningThreshold(warningThreshold);
3005
- const sanitizedSwitch = sanitizeAutoRollSwitchThreshold(switchThreshold, sanitizedWarning);
3006
- return {
3007
- warningThreshold: sanitizedWarning,
3008
- switchThreshold: sanitizedSwitch
3009
- };
3010
- }
3011
- function normalizeAutoRollSettings(raw) {
3012
- const enabled = typeof raw?.enabled === "boolean" ? raw.enabled : DEFAULT_AUTO_ROLL_ENABLED;
3013
- const rawWarning = resolveFiniteNumber(
3014
- typeof raw?.warningThreshold === "number" ? raw.warningThreshold : Number.NaN,
3015
- DEFAULT_AUTO_ROLL_WARNING_THRESHOLD
3016
- );
3017
- const rawSwitch = resolveFiniteNumber(
3018
- typeof raw?.switchThreshold === "number" ? raw.switchThreshold : Number.NaN,
3019
- DEFAULT_AUTO_ROLL_SWITCH_THRESHOLD
3020
- );
3021
- const {
3022
- warningThreshold: normalizedWarning,
3023
- switchThreshold: normalizedSwitch
3024
- } = sanitizeAutoRollThresholds(rawWarning, rawSwitch);
3025
- return {
3026
- enabled,
3027
- warningThreshold: normalizedWarning,
3028
- switchThreshold: normalizedSwitch,
3029
- restartOfficialCodexOnAutoRoll: raw?.restartOfficialCodexOnAutoRoll === true ? true : DEFAULT_RESTART_OFFICIAL_CODEX_ON_AUTO_ROLL
3030
- };
3031
- }
3032
-
3033
3143
  // ../../packages/runtime-codex/src/codex/settings.ts
3034
3144
  function asString2(value) {
3035
3145
  if (typeof value !== "string") {
@@ -3071,17 +3181,6 @@ function parseStoredLicense(raw) {
3071
3181
  );
3072
3182
  return hasValue ? license : null;
3073
3183
  }
3074
- async function getLastProfileName() {
3075
- const state = await getAppState();
3076
- return asString2(state.app.lastProfileName);
3077
- }
3078
- async function persistLastProfileName(profileName) {
3079
- await patchAppState({
3080
- app: {
3081
- lastProfileName: asString2(profileName)
3082
- }
3083
- });
3084
- }
3085
3184
  async function getStoredLicense() {
3086
3185
  const state = await getAppState();
3087
3186
  return parseStoredLicense(state.license);
@@ -3150,9 +3249,13 @@ async function writeCodexSettingsJsonRaw(payload) {
3150
3249
  },
3151
3250
  autoRoll: autoRoll ? {
3152
3251
  enabled: autoRoll.enabled,
3153
- warningThreshold: autoRoll.warningThreshold,
3154
- switchThreshold: autoRoll.switchThreshold,
3155
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
3252
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
3253
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
3254
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
3255
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
3256
+ priorityOrder: autoRoll.priorityOrder,
3257
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
3258
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
3156
3259
  } : void 0,
3157
3260
  license: license ? {
3158
3261
  licenseKey: license.licenseKey ?? null,
@@ -4245,21 +4348,6 @@ var ProfileManager = class {
4245
4348
  error && typeof error === "object" && "code" in error && error.code === "ENOENT"
4246
4349
  );
4247
4350
  }
4248
- async readPreferredProfileName() {
4249
- try {
4250
- return await getLastProfileName();
4251
- } catch (error) {
4252
- logWarn("Failed to read preferred profile name:", error);
4253
- return null;
4254
- }
4255
- }
4256
- async persistPreferredProfileName(name) {
4257
- try {
4258
- await persistLastProfileName(name);
4259
- } catch (error) {
4260
- logWarn("Failed to persist preferred profile name:", error);
4261
- }
4262
- }
4263
4351
  toStateRecord(record) {
4264
4352
  return {
4265
4353
  name: record.name,
@@ -4379,47 +4467,36 @@ var ProfileManager = class {
4379
4467
  return null;
4380
4468
  }
4381
4469
  }
4382
- async captureActiveAuthSnapshot() {
4383
- await this.initialize();
4470
+ emptyAuthSnapshot(fingerprint = null) {
4471
+ return {
4472
+ fingerprint,
4473
+ email: null,
4474
+ accountId: null,
4475
+ userId: null,
4476
+ chatgptUserId: null,
4477
+ chatgptAccountUserId: null,
4478
+ workspaceId: null
4479
+ };
4480
+ }
4481
+ async captureAuthSnapshotAtPath(authPath) {
4384
4482
  let raw;
4385
4483
  try {
4386
- raw = await import_fs.promises.readFile(this.activeAuth, "utf8");
4484
+ raw = await import_fs.promises.readFile(authPath, "utf8");
4387
4485
  } catch (error) {
4388
4486
  if (this.isNotFoundError(error)) {
4389
- return {
4390
- fingerprint: null,
4391
- email: null,
4392
- accountId: null,
4393
- userId: null,
4394
- chatgptUserId: null,
4395
- workspaceId: null
4396
- };
4487
+ return this.emptyAuthSnapshot();
4397
4488
  }
4398
4489
  const message = error instanceof Error ? error.message : "unknown error";
4399
- const signature = typeof message === "string" ? `${message}:${this.activeAuth}` : this.activeAuth;
4490
+ const signature = typeof message === "string" ? `${message}:${authPath}` : authPath;
4400
4491
  if (this.lastActiveAuthErrorSignature !== signature) {
4401
- logWarn("Failed to snapshot active auth file:", error);
4492
+ logWarn("Failed to snapshot auth file:", error);
4402
4493
  this.lastActiveAuthErrorSignature = signature;
4403
4494
  }
4404
- return {
4405
- fingerprint: null,
4406
- email: null,
4407
- accountId: null,
4408
- userId: null,
4409
- chatgptUserId: null,
4410
- workspaceId: null
4411
- };
4495
+ return this.emptyAuthSnapshot();
4412
4496
  }
4413
4497
  const trimmed = raw.trim();
4414
4498
  if (!trimmed) {
4415
- return {
4416
- fingerprint: null,
4417
- email: null,
4418
- accountId: null,
4419
- userId: null,
4420
- chatgptUserId: null,
4421
- workspaceId: null
4422
- };
4499
+ return this.emptyAuthSnapshot();
4423
4500
  }
4424
4501
  const fingerprint = (0, import_node_crypto3.createHash)("sha256").update(trimmed).digest("hex");
4425
4502
  try {
@@ -4433,19 +4510,27 @@ var ProfileManager = class {
4433
4510
  accountId: this.getAccountIdFromData(normalized) ?? null,
4434
4511
  userId: metadata?.userId ?? null,
4435
4512
  chatgptUserId: metadata?.chatgptUserId ?? null,
4513
+ chatgptAccountUserId: metadata?.chatgptAccountUserId ?? null,
4436
4514
  workspaceId: workspace.id ?? null
4437
4515
  };
4438
4516
  } catch {
4439
- return {
4440
- fingerprint,
4441
- email: null,
4442
- accountId: null,
4443
- userId: null,
4444
- chatgptUserId: null,
4445
- workspaceId: null
4446
- };
4517
+ return this.emptyAuthSnapshot(fingerprint);
4447
4518
  }
4448
4519
  }
4520
+ async captureAuthSnapshot(authPath) {
4521
+ const targetPath = authPath ?? this.activeAuth;
4522
+ if (targetPath === this.activeAuth) {
4523
+ return this.enqueueAuthSwap(async () => {
4524
+ await this.initialize();
4525
+ return this.captureAuthSnapshotAtPath(targetPath);
4526
+ });
4527
+ }
4528
+ await this.initialize();
4529
+ return this.captureAuthSnapshotAtPath(targetPath);
4530
+ }
4531
+ async captureActiveAuthSnapshot() {
4532
+ return this.captureAuthSnapshot(this.activeAuth);
4533
+ }
4449
4534
  /**
4450
4535
  * Decode a JWT payload into an object without validating the signature.
4451
4536
  */
@@ -4842,6 +4927,130 @@ var ProfileManager = class {
4842
4927
  metadata: record.metadata
4843
4928
  }) ?? resolveFallbackAccountKey(record.name);
4844
4929
  }
4930
+ describeActiveAuth(auth) {
4931
+ const normalized = this.normalizeProfileData(auth);
4932
+ if (typeof normalized.auth_method === "string") {
4933
+ normalized.auth_method = normalized.auth_method.trim().toLowerCase();
4934
+ }
4935
+ normalized.auth_method = normalized.auth_method ?? "codex-cli";
4936
+ const metadata = this.extractProfileMetadata(normalized);
4937
+ const workspace = this.resolveWorkspaceIdentity(normalized, metadata);
4938
+ const workspaceId = workspace.id || DEFAULT_WORKSPACE_ID;
4939
+ const workspaceName = workspace.name ?? null;
4940
+ normalized.workspace_id = normalized.workspace_id ?? workspaceId;
4941
+ if (workspaceName) {
4942
+ normalized.workspace_name = normalized.workspace_name ?? workspaceName;
4943
+ }
4944
+ const email = this.resolveProfileEmail(normalized, metadata) ?? null;
4945
+ if (email) {
4946
+ normalized.email = email;
4947
+ }
4948
+ const accountId = this.getAccountIdFromData(normalized) ?? null;
4949
+ const identityKey = this.resolveProfileIdentityFromData(null, normalized, metadata);
4950
+ const hasTokenPayload = Boolean(
4951
+ normalized.id_token || normalized.access_token || normalized.refresh_token || accountId || email
4952
+ );
4953
+ return {
4954
+ normalized,
4955
+ metadata,
4956
+ workspaceId,
4957
+ workspaceName,
4958
+ email,
4959
+ accountId,
4960
+ identityKey,
4961
+ hasTokenPayload
4962
+ };
4963
+ }
4964
+ async findProfileForActiveAuth(identityKey, workspaceId) {
4965
+ return this.getProfileRowByIdentityAndWorkspace(identityKey, workspaceId, {
4966
+ preferAuthMethod: "codex-cli"
4967
+ });
4968
+ }
4969
+ async syncMatchedProfileFromActiveAuth(record, description) {
4970
+ if (!this.hasTokenChanges(record.data, description.normalized)) {
4971
+ return;
4972
+ }
4973
+ const { profile: merged, metadata } = this.mergeProfileRecords(record.data, description.normalized);
4974
+ delete merged.tokenAlert;
4975
+ await this.persistProfileRecord(record.name, merged, metadata);
4976
+ }
4977
+ async assertActiveAuthMatchesRecord(record, message) {
4978
+ const activeAuth = await this.readActiveAuthFile();
4979
+ if (!activeAuth) {
4980
+ throw new Error("Active Codex auth is missing. Refresh profiles and try again.");
4981
+ }
4982
+ const activeDescription = this.describeActiveAuth(activeAuth);
4983
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
4984
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? record.data.workspace_id);
4985
+ if (activeDescription.identityKey !== targetIdentity || this.normalizeWorkspaceId(activeDescription.workspaceId) !== targetWorkspaceId) {
4986
+ throw new Error(message);
4987
+ }
4988
+ }
4989
+ async writeActiveAuthForRecord(record) {
4990
+ const profileData = record.data;
4991
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
4992
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? profileData.workspace_id);
4993
+ const codexFormat = this.convertToCodexFormat(profileData);
4994
+ await this.writeAtomic(this.activeAuth, JSON.stringify(codexFormat, null, 2));
4995
+ const written = await this.readActiveAuthFile();
4996
+ if (!written) {
4997
+ throw new Error("Codex auth file was not written.");
4998
+ }
4999
+ const writtenDescription = this.describeActiveAuth(written);
5000
+ if (writtenDescription.identityKey !== targetIdentity) {
5001
+ throw new Error("Codex auth verification failed after profile switch.");
5002
+ }
5003
+ if (this.normalizeWorkspaceId(writtenDescription.workspaceId) !== targetWorkspaceId) {
5004
+ throw new Error("Codex auth workspace verification failed after profile switch.");
5005
+ }
5006
+ }
5007
+ async syncActiveAuthFromProfileIfCurrent(name) {
5008
+ const profileName = this.normalizeProfileName(name);
5009
+ const record = await this.getProfileRowByName(profileName);
5010
+ if (!record) {
5011
+ return;
5012
+ }
5013
+ const activeAuth = await this.readActiveAuthFile();
5014
+ if (!activeAuth) {
5015
+ return;
5016
+ }
5017
+ const activeDescription = this.describeActiveAuth(activeAuth);
5018
+ const targetIdentity = this.resolveProfileIdentityFromRecord(record);
5019
+ const targetWorkspaceId = this.normalizeWorkspaceId(record.workspaceId ?? record.data.workspace_id);
5020
+ if (activeDescription.identityKey !== targetIdentity || this.normalizeWorkspaceId(activeDescription.workspaceId) !== targetWorkspaceId) {
5021
+ return;
5022
+ }
5023
+ await this.writeActiveAuthForRecord(record);
5024
+ }
5025
+ async removeActiveAuthFiles() {
5026
+ await import_fs.promises.rm(this.activeAuth, { force: true });
5027
+ await import_fs.promises.rm(this.activeAuthBackup, { force: true });
5028
+ await import_fs.promises.rm(`${this.activeAuth}.tmp`, { force: true });
5029
+ }
5030
+ resolveAutoImportedProfileName(email, existingRecords) {
5031
+ const source = email ? email.split("@")[0] : "codex-account";
5032
+ const base = source.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "codex-account";
5033
+ const used = new Set(existingRecords.map((record) => record.name));
5034
+ let candidate = this.normalizeProfileName(base);
5035
+ let suffix = 2;
5036
+ while (used.has(candidate)) {
5037
+ candidate = this.normalizeProfileName(`${base}-${suffix}`);
5038
+ suffix += 1;
5039
+ }
5040
+ return candidate;
5041
+ }
5042
+ buildActiveCodexAuthStatus(state, description, options) {
5043
+ return {
5044
+ state,
5045
+ profileName: options?.profileName ?? null,
5046
+ email: description?.email ?? null,
5047
+ accountId: description?.accountId ?? null,
5048
+ workspaceId: description?.workspaceId ?? null,
5049
+ workspaceName: description?.workspaceName ?? null,
5050
+ importedProfileName: options?.importedProfileName ?? null,
5051
+ importBlockedReason: options?.importBlockedReason ?? null
5052
+ };
5053
+ }
4845
5054
  resolveWorkspaceIdentity(data, metadata) {
4846
5055
  const directId = "workspace_id" in data && typeof data.workspace_id === "string" ? data.workspace_id.trim() : void 0;
4847
5056
  const directName = "workspace_name" in data && typeof data.workspace_name === "string" ? data.workspace_name.trim() : void 0;
@@ -4862,22 +5071,6 @@ var ProfileManager = class {
4862
5071
  const record = await this.readProfileRecord(normalized);
4863
5072
  return record ?? void 0;
4864
5073
  }
4865
- async getProfileRowByIdentityKey(identityKey, options) {
4866
- const records = await this.listProfileRecords();
4867
- const matches = records.filter((record) => this.resolveProfileIdentityFromRecord(record) === identityKey);
4868
- if (matches.length === 0) {
4869
- return void 0;
4870
- }
4871
- const mustMatch = typeof options?.authMethod === "string" ? options.authMethod.trim().toLowerCase() : null;
4872
- if (mustMatch) {
4873
- return matches.find((record) => this.resolveAuthMethod(record.data) === mustMatch);
4874
- }
4875
- const prefer = typeof options?.preferAuthMethod === "string" ? options.preferAuthMethod.trim().toLowerCase() : null;
4876
- if (prefer) {
4877
- return matches.find((record) => this.resolveAuthMethod(record.data) === prefer) ?? matches[0];
4878
- }
4879
- return matches[0];
4880
- }
4881
5074
  async getProfileRowByIdentityAndWorkspace(identityKey, workspaceId, options) {
4882
5075
  const workspaceKey = workspaceId && workspaceId.trim().length > 0 ? workspaceId.trim() : DEFAULT_WORKSPACE_ID;
4883
5076
  const records = await this.listProfileRecords();
@@ -4901,6 +5094,9 @@ var ProfileManager = class {
4901
5094
  }
4902
5095
  return matches[0];
4903
5096
  }
5097
+ normalizeWorkspaceId(value) {
5098
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : DEFAULT_WORKSPACE_ID;
5099
+ }
4904
5100
  async persistProfileRecord(name, data, metadata, options) {
4905
5101
  const resolvedName = this.normalizeProfileName(name);
4906
5102
  const existingRecord = await this.readProfileRecord(resolvedName);
@@ -4960,6 +5156,22 @@ var ProfileManager = class {
4960
5156
  });
4961
5157
  return profile;
4962
5158
  }
5159
+ async deleteProfileRecordAndHome(record) {
5160
+ const profileName = this.normalizeProfileName(record.name);
5161
+ if (!record.accountId) {
5162
+ const dashboardState = await this.readProfileDashboardState();
5163
+ delete dashboardState.customGroupsByAccountKey[resolveFallbackAccountKey(profileName)];
5164
+ await this.writeProfileDashboardState(dashboardState);
5165
+ }
5166
+ try {
5167
+ await import_fs.promises.rm(this.getProfileHomePath(profileName), { recursive: true, force: true });
5168
+ } catch (error) {
5169
+ if (!this.isNotFoundError(error)) {
5170
+ logWarn(`Failed to remove profile home for '${profileName}':`, error);
5171
+ }
5172
+ }
5173
+ await this.deleteProfileRecord(profileName);
5174
+ }
4963
5175
  buildProfileFromData(name, data, fallback) {
4964
5176
  const metadata = fallback?.metadata ?? this.extractProfileMetadata(data);
4965
5177
  const workspace = this.resolveWorkspaceIdentity(data, metadata);
@@ -5019,6 +5231,18 @@ var ProfileManager = class {
5019
5231
  if (normalizedLastRefresh) {
5020
5232
  codexAuth.last_refresh = normalizedLastRefresh;
5021
5233
  }
5234
+ const email = typeof data.email === "string" ? data.email.trim() : "";
5235
+ if (email) {
5236
+ codexAuth.email = email;
5237
+ }
5238
+ const workspaceId = typeof data.workspace_id === "string" ? data.workspace_id.trim() : "";
5239
+ if (workspaceId) {
5240
+ codexAuth.workspace_id = workspaceId;
5241
+ }
5242
+ const workspaceName = typeof data.workspace_name === "string" ? data.workspace_name.trim() : "";
5243
+ if (workspaceName) {
5244
+ codexAuth.workspace_name = workspaceName;
5245
+ }
5022
5246
  return codexAuth;
5023
5247
  }
5024
5248
  /**
@@ -5041,8 +5265,17 @@ var ProfileManager = class {
5041
5265
  if (normalizedLastRefresh) {
5042
5266
  profile.last_refresh = normalizedLastRefresh;
5043
5267
  }
5044
- if (data.email) {
5045
- profile.email = data.email;
5268
+ const email = typeof data.email === "string" ? data.email.trim() : "";
5269
+ if (email) {
5270
+ profile.email = email;
5271
+ }
5272
+ const workspaceId = typeof data.workspace_id === "string" ? data.workspace_id.trim() : "";
5273
+ if (workspaceId) {
5274
+ profile.workspace_id = workspaceId;
5275
+ }
5276
+ const workspaceName = typeof data.workspace_name === "string" ? data.workspace_name.trim() : "";
5277
+ if (workspaceName) {
5278
+ profile.workspace_name = workspaceName;
5046
5279
  }
5047
5280
  return profile;
5048
5281
  }
@@ -5174,6 +5407,12 @@ var ProfileManager = class {
5174
5407
  );
5175
5408
  return;
5176
5409
  }
5410
+ if (this.normalizeWorkspaceId(existing.workspace_id) !== this.normalizeWorkspaceId(normalized.workspace_id)) {
5411
+ logWarn(
5412
+ `Skipped syncing tokens for profile '${profileName}' because Codex auth switched to a different workspace.`
5413
+ );
5414
+ return;
5415
+ }
5177
5416
  if (!this.hasTokenChanges(existing, normalized)) {
5178
5417
  return;
5179
5418
  }
@@ -5371,94 +5610,80 @@ var ProfileManager = class {
5371
5610
  }
5372
5611
  if (finalAuthContent) {
5373
5612
  await this.syncProfileTokensFromAuthContent(profileName, record.data, finalAuthContent);
5613
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5374
5614
  return;
5375
5615
  }
5376
5616
  await this.syncProfileTokensFromActiveAuth(profileName, record.data, authPath);
5617
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5377
5618
  })
5378
5619
  );
5379
5620
  }
5380
5621
  /**
5381
5622
  * Create a new profile by running codex login
5382
5623
  */
5383
- async createProfile(name) {
5384
- await this.initialize();
5385
- const profileName = this.normalizeProfileName(name);
5386
- if (await this.getProfileRowByName(profileName)) {
5387
- throw new Error(`Profile '${profileName}' already exists!`);
5388
- }
5389
- let authData;
5624
+ async readAuthFileForImport(authPath, actionLabel) {
5625
+ let authRaw;
5390
5626
  try {
5391
- const authRaw = await import_fs.promises.readFile(this.activeAuth, "utf8");
5392
- authData = JSON.parse(authRaw);
5627
+ authRaw = await import_fs.promises.readFile(authPath, "utf8");
5393
5628
  } catch (error) {
5394
- logError("Error creating profile:", error);
5395
- throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5396
- }
5397
- const normalizedProfile = this.normalizeProfileData(authData);
5398
- normalizedProfile.created_at = normalizedProfile.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
5399
- if (typeof normalizedProfile.auth_method === "string") {
5400
- normalizedProfile.auth_method = normalizedProfile.auth_method.trim().toLowerCase();
5401
- }
5402
- normalizedProfile.auth_method = normalizedProfile.auth_method ?? "codex-cli";
5403
- const metadata = this.extractProfileMetadata(normalizedProfile);
5404
- const workspace = this.resolveWorkspaceIdentity(normalizedProfile, metadata);
5405
- normalizedProfile.workspace_id = normalizedProfile.workspace_id ?? workspace.id ?? DEFAULT_WORKSPACE_ID;
5406
- if (workspace.name) {
5407
- normalizedProfile.workspace_name = normalizedProfile.workspace_name ?? workspace.name;
5408
- }
5409
- const resolvedEmail = this.resolveProfileEmail(normalizedProfile, metadata);
5410
- if (resolvedEmail) {
5411
- normalizedProfile.email = resolvedEmail;
5629
+ if (this.isNotFoundError(error)) {
5630
+ throw new Error(`Codex CLI did not produce an auth file. Complete the login before ${actionLabel}.`);
5631
+ }
5632
+ logError(`Failed to read Codex auth file during ${actionLabel}:`, error);
5633
+ throw new Error("Failed to read Codex auth file. Complete the login and try again.");
5412
5634
  }
5413
5635
  try {
5414
- await this.persistProfileRecord(profileName, normalizedProfile, metadata);
5415
- const stored = await this.getProfileRowByName(profileName);
5416
- return stored ? this.buildProfileFromRow(stored) : null;
5636
+ return JSON.parse(authRaw);
5417
5637
  } catch (error) {
5418
- logError("Error creating profile:", error);
5419
- throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5638
+ logError(`Failed to parse Codex auth file during ${actionLabel}:`, error);
5639
+ throw new Error("Failed to parse Codex auth file. Complete the login and try again.");
5420
5640
  }
5421
5641
  }
5422
- /**
5423
- * Switch to a different profile
5424
- */
5425
- async switchToProfile(name) {
5426
- await this.initialize();
5642
+ async persistNewProfileFromAuth(profileName, authData) {
5643
+ const description = this.describeActiveAuth(authData);
5644
+ const normalizedProfile = description.normalized;
5645
+ normalizedProfile.created_at = normalizedProfile.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
5646
+ await this.persistProfileRecord(profileName, normalizedProfile, description.metadata);
5647
+ const stored = await this.getProfileRowByName(profileName);
5648
+ return stored ? this.buildProfileFromRow(stored) : null;
5649
+ }
5650
+ async createProfile(name) {
5427
5651
  const profileName = this.normalizeProfileName(name);
5428
- const record = await this.getProfileRowByName(profileName);
5429
- if (!record) {
5430
- throw new Error(`Profile '${profileName}' not found!`);
5431
- }
5432
- try {
5433
- const profileData = record.data;
5434
- const codexFormat = this.convertToCodexFormat(profileData);
5435
- await this.writeAtomic(this.activeAuth, JSON.stringify(codexFormat, null, 2));
5436
- await this.persistPreferredProfileName(profileName);
5437
- return true;
5438
- } catch (error) {
5439
- logError("Error switching profile:", error);
5440
- throw new Error("Failed to switch profile");
5441
- }
5652
+ return this.enqueueProfileOperation(
5653
+ profileName,
5654
+ () => this.enqueueAuthSwap(async () => {
5655
+ await this.initialize();
5656
+ if (await this.getProfileRowByName(profileName)) {
5657
+ throw new Error(`Profile '${profileName}' already exists!`);
5658
+ }
5659
+ try {
5660
+ const authData = await this.readAuthFileForImport(this.activeAuth, "profile creation");
5661
+ return await this.persistNewProfileFromAuth(profileName, authData);
5662
+ } catch (error) {
5663
+ logError("Error creating profile:", error);
5664
+ throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5665
+ }
5666
+ })
5667
+ );
5442
5668
  }
5443
- async refreshProfileAuth(name) {
5444
- await this.initialize();
5669
+ async createProfileFromAuthFile(name, authPath) {
5445
5670
  const profileName = this.normalizeProfileName(name);
5446
- const record = await this.getProfileRowByName(profileName);
5447
- if (!record) {
5448
- throw new Error(`Profile '${profileName}' not found!`);
5449
- }
5450
- const existing = record.data;
5451
- let activeAuth;
5452
- try {
5453
- const authContent = await import_fs.promises.readFile(this.activeAuth, "utf8");
5454
- activeAuth = JSON.parse(authContent);
5455
- } catch (error) {
5456
- if (this.isNotFoundError(error)) {
5457
- throw new Error("Codex CLI did not produce an auth file. Complete the login before refreshing this profile.");
5671
+ return this.enqueueProfileOperation(profileName, async () => {
5672
+ await this.initialize();
5673
+ if (await this.getProfileRowByName(profileName)) {
5674
+ throw new Error(`Profile '${profileName}' already exists!`);
5458
5675
  }
5459
- logError("Failed to read Codex auth file during refresh:", error);
5460
- throw new Error("Failed to read Codex auth file. Complete the login and try again.");
5461
- }
5676
+ try {
5677
+ const authData = await this.readAuthFileForImport(authPath, "profile creation");
5678
+ return await this.persistNewProfileFromAuth(profileName, authData);
5679
+ } catch (error) {
5680
+ logError("Error creating profile:", error);
5681
+ throw new Error("Failed to create profile. Make sure Codex CLI is installed and you are logged in.");
5682
+ }
5683
+ });
5684
+ }
5685
+ async refreshProfileAuthWithData(profileName, record, activeAuth) {
5686
+ const existing = record.data;
5462
5687
  const normalized = this.normalizeProfileData(activeAuth);
5463
5688
  const existingMetadata = record.metadata ?? this.extractProfileMetadata(existing);
5464
5689
  const currentWorkspace = this.resolveWorkspaceIdentity(existing, existingMetadata);
@@ -5507,6 +5732,60 @@ var ProfileManager = class {
5507
5732
  }
5508
5733
  return profile;
5509
5734
  }
5735
+ /**
5736
+ * Switch to a different profile
5737
+ */
5738
+ async switchToProfile(name) {
5739
+ const profileName = this.normalizeProfileName(name);
5740
+ return this.enqueueProfileOperation(
5741
+ profileName,
5742
+ () => this.enqueueAuthSwap(async () => {
5743
+ await this.initialize();
5744
+ const record = await this.getProfileRowByName(profileName);
5745
+ if (!record) {
5746
+ throw new Error(`Profile '${profileName}' not found!`);
5747
+ }
5748
+ try {
5749
+ await this.writeActiveAuthForRecord(record);
5750
+ return true;
5751
+ } catch (error) {
5752
+ logError("Error switching profile:", error);
5753
+ throw new Error("Failed to switch profile");
5754
+ }
5755
+ })
5756
+ );
5757
+ }
5758
+ async refreshProfileAuth(name) {
5759
+ const profileName = this.normalizeProfileName(name);
5760
+ return this.enqueueProfileOperation(
5761
+ profileName,
5762
+ () => this.enqueueAuthSwap(async () => {
5763
+ await this.initialize();
5764
+ const record = await this.getProfileRowByName(profileName);
5765
+ if (!record) {
5766
+ throw new Error(`Profile '${profileName}' not found!`);
5767
+ }
5768
+ const activeAuth = await this.readAuthFileForImport(this.activeAuth, "profile refresh");
5769
+ return this.refreshProfileAuthWithData(profileName, record, activeAuth);
5770
+ })
5771
+ );
5772
+ }
5773
+ async refreshProfileAuthFromFile(name, authPath) {
5774
+ const profileName = this.normalizeProfileName(name);
5775
+ return this.enqueueProfileOperation(profileName, async () => {
5776
+ await this.initialize();
5777
+ const record = await this.getProfileRowByName(profileName);
5778
+ if (!record) {
5779
+ throw new Error(`Profile '${profileName}' not found!`);
5780
+ }
5781
+ const activeAuth = await this.readAuthFileForImport(authPath, "profile refresh");
5782
+ const profile = await this.refreshProfileAuthWithData(profileName, record, activeAuth);
5783
+ await this.enqueueAuthSwap(async () => {
5784
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5785
+ });
5786
+ return profile;
5787
+ });
5788
+ }
5510
5789
  /**
5511
5790
  * Execute an action with a profile's auth injected.
5512
5791
  * Creates an isolated CODEX_HOME with the profile's auth/config, invokes the action
@@ -5515,22 +5794,35 @@ var ProfileManager = class {
5515
5794
  async runWithProfileAuth(name, action) {
5516
5795
  return this.enqueueProfileOperation(
5517
5796
  name,
5518
- () => this.enqueueAuthSwap(
5519
- () => this.runWithPreparedProfileHome(name, action, {
5520
- syncFromActiveAuthBeforeAction: true
5521
- })
5522
- )
5797
+ () => this.enqueueAuthSwap(async () => {
5798
+ try {
5799
+ return await this.runWithPreparedProfileHome(name, action, {
5800
+ syncFromActiveAuthBeforeAction: true
5801
+ });
5802
+ } finally {
5803
+ await this.syncActiveAuthFromProfileIfCurrent(name);
5804
+ }
5805
+ })
5523
5806
  );
5524
5807
  }
5525
5808
  async readLiveRateLimits(name, options = {}) {
5526
- return this.enqueueProfileOperation(
5527
- name,
5528
- () => this.runWithPreparedProfileHome(
5529
- name,
5530
- (env) => fetchRateLimitsViaRpc(env, { codexPath: options.codexPath }),
5531
- { syncFromActiveAuthBeforeAction: false }
5532
- )
5533
- );
5809
+ const profileName = this.normalizeProfileName(name);
5810
+ try {
5811
+ return await this.enqueueProfileOperation(
5812
+ profileName,
5813
+ () => this.runWithPreparedProfileHome(
5814
+ profileName,
5815
+ (env) => fetchRateLimitsViaRpc(env, { codexPath: options.codexPath }),
5816
+ { syncFromActiveAuthBeforeAction: false }
5817
+ )
5818
+ );
5819
+ } finally {
5820
+ await this.enqueueAuthSwap(async () => {
5821
+ await this.syncActiveAuthFromProfileIfCurrent(profileName);
5822
+ }).catch((error) => {
5823
+ logWarn(`Failed to sync active auth after rate-limit probe for '${profileName}':`, error);
5824
+ });
5825
+ }
5534
5826
  }
5535
5827
  /**
5536
5828
  * Rename a profile
@@ -5546,10 +5838,6 @@ var ProfileManager = class {
5546
5838
  if (await this.getProfileRowByName(targetName)) {
5547
5839
  throw new Error(`Profile '${targetName}' already exists!`);
5548
5840
  }
5549
- const preferred = await this.readPreferredProfileName();
5550
- if (preferred && preferred === sourceName) {
5551
- await this.persistPreferredProfileName(targetName);
5552
- }
5553
5841
  const oldHome = this.getProfileHomePath(sourceName);
5554
5842
  const newHome = this.getProfileHomePath(targetName);
5555
5843
  try {
@@ -5618,25 +5906,51 @@ var ProfileManager = class {
5618
5906
  if (!record) {
5619
5907
  throw new Error(`Profile '${profileName}' not found!`);
5620
5908
  }
5621
- if (!record.accountId) {
5622
- const dashboardState = await this.readProfileDashboardState();
5623
- delete dashboardState.customGroupsByAccountKey[resolveFallbackAccountKey(profileName)];
5624
- await this.writeProfileDashboardState(dashboardState);
5625
- }
5626
- const preferred = await this.readPreferredProfileName();
5627
- if (preferred && preferred === profileName) {
5628
- await this.persistPreferredProfileName(null);
5629
- }
5630
- try {
5631
- await import_fs.promises.rm(this.getProfileHomePath(profileName), { recursive: true, force: true });
5632
- } catch (error) {
5633
- if (!this.isNotFoundError(error)) {
5634
- logWarn(`Failed to remove profile home for '${profileName}':`, error);
5635
- }
5636
- }
5637
- await this.deleteProfileRecord(profileName);
5909
+ await this.deleteProfileRecordAndHome(record);
5638
5910
  return true;
5639
5911
  }
5912
+ async deleteActiveProfileAndSwitch(name, replacementName) {
5913
+ const profileName = this.normalizeProfileName(name);
5914
+ const nextProfileName = this.normalizeProfileName(replacementName);
5915
+ if (profileName === nextProfileName) {
5916
+ throw new Error("Replacement profile must be different from the deleted profile.");
5917
+ }
5918
+ return this.enqueueAuthSwap(async () => {
5919
+ await this.initialize();
5920
+ const record = await this.getProfileRowByName(profileName);
5921
+ if (!record) {
5922
+ throw new Error(`Profile '${profileName}' not found!`);
5923
+ }
5924
+ const replacement = await this.getProfileRowByName(nextProfileName);
5925
+ if (!replacement) {
5926
+ throw new Error(`Profile '${nextProfileName}' not found!`);
5927
+ }
5928
+ await this.assertActiveAuthMatchesRecord(
5929
+ record,
5930
+ "Active Codex login changed before deletion. Refresh profiles and try again."
5931
+ );
5932
+ await this.writeActiveAuthForRecord(replacement);
5933
+ await this.deleteProfileRecordAndHome(record);
5934
+ return true;
5935
+ });
5936
+ }
5937
+ async deleteActiveProfileAndAuth(name) {
5938
+ const profileName = this.normalizeProfileName(name);
5939
+ return this.enqueueAuthSwap(async () => {
5940
+ await this.initialize();
5941
+ const record = await this.getProfileRowByName(profileName);
5942
+ if (!record) {
5943
+ throw new Error(`Profile '${profileName}' not found!`);
5944
+ }
5945
+ await this.assertActiveAuthMatchesRecord(
5946
+ record,
5947
+ "Active Codex login changed before deletion. Refresh profiles and try again."
5948
+ );
5949
+ await this.removeActiveAuthFiles();
5950
+ await this.deleteProfileRecordAndHome(record);
5951
+ return true;
5952
+ });
5953
+ }
5640
5954
  /**
5641
5955
  * Delete every profile that does not appear in the allow-list.
5642
5956
  * Used to enforce plan limits when local storage was tampered with.
@@ -5673,64 +5987,61 @@ var ProfileManager = class {
5673
5987
  }
5674
5988
  }
5675
5989
  }
5676
- const preferred = await this.readPreferredProfileName();
5677
- if (preferred && removed.includes(preferred)) {
5678
- await this.persistPreferredProfileName(null);
5679
- }
5680
5990
  return removed;
5681
5991
  }
5682
- /**
5683
- * Check if the current auth matches a profile (for smart detection)
5684
- */
5685
- async getCurrentProfile() {
5686
- await this.initialize();
5687
- const preferredName = await this.readPreferredProfileName();
5688
- if (preferredName) {
5689
- try {
5690
- const normalizedPreferred = this.normalizeProfileName(preferredName);
5691
- const preferredRecord = await this.getProfileRowByName(normalizedPreferred);
5692
- if (preferredRecord) {
5693
- return { name: normalizedPreferred, trusted: true };
5694
- }
5695
- } catch {
5992
+ async getActiveCodexAuthStatus(options = {}) {
5993
+ return this.enqueueAuthSwap(async () => {
5994
+ await this.initialize();
5995
+ const activeAuth = await this.readActiveAuthFile();
5996
+ if (!activeAuth) {
5997
+ return this.buildActiveCodexAuthStatus("missing");
5696
5998
  }
5697
- }
5698
- const activeAuth = await this.readActiveAuthFile();
5699
- if (activeAuth) {
5700
- const normalizedAuth = this.normalizeProfileData(activeAuth);
5701
- const authMetadata = this.extractProfileMetadata(normalizedAuth);
5702
- const workspace = this.resolveWorkspaceIdentity(normalizedAuth, authMetadata);
5703
- const activeIdentityKey = this.resolveProfileIdentityFromData(null, normalizedAuth, authMetadata);
5704
- if (activeIdentityKey) {
5705
- const scoped = await this.getProfileRowByIdentityAndWorkspace(activeIdentityKey, workspace.id ?? DEFAULT_WORKSPACE_ID, {
5706
- preferAuthMethod: "codex-cli"
5999
+ const description = this.describeActiveAuth(activeAuth);
6000
+ if (!description.hasTokenPayload || !description.identityKey) {
6001
+ return this.buildActiveCodexAuthStatus("unknown", description);
6002
+ }
6003
+ const matching = await this.findProfileForActiveAuth(
6004
+ description.identityKey,
6005
+ description.workspaceId
6006
+ );
6007
+ if (matching) {
6008
+ const normalized = this.normalizeProfileName(matching.name);
6009
+ await this.syncMatchedProfileFromActiveAuth(matching, description);
6010
+ return this.buildActiveCodexAuthStatus("matched", description, {
6011
+ profileName: normalized
5707
6012
  });
5708
- if (scoped) {
5709
- const normalized = this.normalizeProfileName(scoped.name);
5710
- await this.persistPreferredProfileName(normalized);
5711
- return { name: normalized, trusted: true };
5712
- }
5713
- const matching = await this.getProfileRowByIdentityKey(activeIdentityKey, { preferAuthMethod: "codex-cli" });
5714
- if (matching) {
5715
- const normalized = this.normalizeProfileName(matching.name);
5716
- await this.persistPreferredProfileName(normalized);
5717
- return { name: normalized, trusted: true };
5718
- }
5719
6013
  }
5720
- }
5721
- const preferred = await this.readPreferredProfileName();
5722
- if (preferred) {
5723
- try {
5724
- const normalized = this.normalizeProfileName(preferred);
5725
- const exists = await this.getProfileRowByName(normalized);
5726
- if (exists) {
5727
- return { name: normalized, trusted: true };
5728
- }
5729
- } catch {
5730
- await this.persistPreferredProfileName(null);
5731
- return { name: null, trusted: false };
6014
+ if (!options.autoImport) {
6015
+ return this.buildActiveCodexAuthStatus("unmanaged", description);
6016
+ }
6017
+ if (options.canAutoImport === false) {
6018
+ return this.buildActiveCodexAuthStatus("unmanaged", description, {
6019
+ importBlockedReason: options.importBlockedReason ?? "Profile limit reached. Upgrade to CodexUse Pro or remove a profile before importing this Codex login."
6020
+ });
5732
6021
  }
5733
- await this.persistPreferredProfileName(null);
6022
+ const records = await this.listProfileRecords();
6023
+ const profileName = this.resolveAutoImportedProfileName(description.email, records);
6024
+ const profileData = {
6025
+ ...description.normalized,
6026
+ auth_method: "codex-cli",
6027
+ created_at: description.normalized.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
6028
+ };
6029
+ await this.persistProfileRecord(profileName, profileData, description.metadata, {
6030
+ displayName: description.email ?? profileName
6031
+ });
6032
+ return this.buildActiveCodexAuthStatus("matched", description, {
6033
+ profileName,
6034
+ importedProfileName: profileName
6035
+ });
6036
+ });
6037
+ }
6038
+ /**
6039
+ * Check if the real Codex auth file matches a saved profile.
6040
+ */
6041
+ async getCurrentProfile() {
6042
+ const status = await this.getActiveCodexAuthStatus({ autoImport: false });
6043
+ if (status.state === "matched" && status.profileName) {
6044
+ return { name: status.profileName, trusted: true };
5734
6045
  }
5735
6046
  return { name: null, trusted: false };
5736
6047
  }
@@ -5831,17 +6142,6 @@ var ProfileManager = class {
5831
6142
  }
5832
6143
  }
5833
6144
  }
5834
- const preferred = await this.readPreferredProfileName();
5835
- if (preferred) {
5836
- try {
5837
- const normalizedPreferred = this.normalizeProfileName(preferred);
5838
- if (!normalized.has(normalizedPreferred)) {
5839
- await this.persistPreferredProfileName(null);
5840
- }
5841
- } catch {
5842
- await this.persistPreferredProfileName(null);
5843
- }
5844
- }
5845
6145
  return {
5846
6146
  imported: normalized.size,
5847
6147
  removed
@@ -5874,8 +6174,8 @@ Usage:
5874
6174
  codexuse profile current
5875
6175
  codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
5876
6176
  codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
5877
- codexuse profile switch <name>
5878
- codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
6177
+ codexuse profile switch <name> [--restart-codex]
6178
+ codexuse profile autoroll [--switch-left=0-50] [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds] [--restart-codex|--no-restart-codex]
5879
6179
  codexuse profile delete <name>
5880
6180
  codexuse profile rename <old> <new>
5881
6181
 
@@ -5895,22 +6195,26 @@ Flags:
5895
6195
  --compact Names only
5896
6196
  --device-auth Use device auth for Codex login
5897
6197
  --login=MODE Login mode: browser | device
5898
- --threshold=NN Auto-roll switch threshold percent (50-100)
6198
+ --switch-left=NN Auto-roll switch threshold by percent left (0-50)
6199
+ --threshold=NN Legacy auto-roll switch threshold by percent used (50-100)
5899
6200
  --watch Keep checking and auto-switch when threshold is reached
5900
6201
  --interval=SEC Watch interval in seconds (default: 30)
5901
6202
  --dry-run Print planned switch without changing active profile
6203
+ --restart-codex Restart official Codex after a profile switch
6204
+ --no-restart-codex Override saved auto-roll restart setting
5902
6205
  --profile=NAME Run Codex with one saved profile
5903
6206
  --runtime=NAME Accounts Pool runtime store: desktop | daemon
5904
6207
  --state-dir=PATH Override the runtime state dir for Accounts Pool inspection
5905
6208
  --port=NNNN Stable daemon/API port (or use with account-pool status for daemon base URL)
5906
6209
  --telegram-bot-token=TOKEN Enable Telegram remote control for daemon mode
5907
6210
  --project-path=PATH Optional path to bootstrap as the default Projects root in daemon mode
6211
+
6212
+ Note: profile autoroll requires Pro.
5908
6213
  `);
5909
6214
  }
5910
6215
 
5911
6216
  // src/platform/args.ts
5912
6217
  var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
5913
- var DEFAULT_AUTOROLL_THRESHOLD = 95;
5914
6218
  function hasFlag(args, flag) {
5915
6219
  return args.includes(flag);
5916
6220
  }
@@ -7694,6 +7998,411 @@ async function assertProfileCreationAllowed(profileManager) {
7694
7998
  }
7695
7999
  }
7696
8000
 
8001
+ // ../../packages/runtime-codex/src/codex/officialAppRestart.ts
8002
+ var import_node_child_process7 = require("child_process");
8003
+ var import_node_fs8 = require("fs");
8004
+ var import_node_os5 = require("os");
8005
+ var import_node_path11 = __toESM(require("path"), 1);
8006
+ var RESTART_COOLDOWN_MS = 6e4;
8007
+ var COMMAND_TIMEOUT_MS = 3e3;
8008
+ var EXIT_WAIT_MS = 1e4;
8009
+ var LAUNCH_WAIT_MS = 15e3;
8010
+ var POLL_INTERVAL_MS = 250;
8011
+ var APP_DISCOVERY_CACHE_TTL_MS = 6e4;
8012
+ var APP_DISCOVERY_MISS_CACHE_TTL_MS = 5e3;
8013
+ var restartPromise = null;
8014
+ var lastRestartStartedAt = 0;
8015
+ var profileActionLock = Promise.resolve();
8016
+ var appDiscoveryCache = null;
8017
+ function logOfficialCodexRestart(level, message, context) {
8018
+ const logger = level === "warn" ? console.warn : level === "error" ? console.error : console.info;
8019
+ if (context && Object.keys(context).length > 0) {
8020
+ logger(`[official-codex-restart] ${message}`, context);
8021
+ return;
8022
+ }
8023
+ logger(`[official-codex-restart] ${message}`);
8024
+ }
8025
+ function runCommand2(command, args, timeoutMs = COMMAND_TIMEOUT_MS) {
8026
+ return new Promise((resolve) => {
8027
+ const child = (0, import_node_child_process7.spawn)(command, args, {
8028
+ stdio: ["ignore", "pipe", "pipe"]
8029
+ });
8030
+ let stdout = "";
8031
+ let stderr = "";
8032
+ let timedOut = false;
8033
+ const timer = setTimeout(() => {
8034
+ timedOut = true;
8035
+ child.kill("SIGTERM");
8036
+ }, timeoutMs);
8037
+ child.stdout?.on("data", (chunk) => {
8038
+ stdout += String(chunk);
8039
+ });
8040
+ child.stderr?.on("data", (chunk) => {
8041
+ stderr += String(chunk);
8042
+ });
8043
+ child.on("error", (error) => {
8044
+ clearTimeout(timer);
8045
+ resolve({
8046
+ code: -1,
8047
+ stdout,
8048
+ stderr: error instanceof Error ? error.message : String(error),
8049
+ timedOut
8050
+ });
8051
+ });
8052
+ child.on("close", (code) => {
8053
+ clearTimeout(timer);
8054
+ resolve({ code, stdout, stderr, timedOut });
8055
+ });
8056
+ });
8057
+ }
8058
+ async function readBundleString(appPath, key) {
8059
+ const plistPath = import_node_path11.default.join(appPath, "Contents", "Info.plist");
8060
+ if (!(0, import_node_fs8.existsSync)(plistPath)) {
8061
+ return null;
8062
+ }
8063
+ const result = await runCommand2("/usr/bin/plutil", [
8064
+ "-extract",
8065
+ key,
8066
+ "raw",
8067
+ "-o",
8068
+ "-",
8069
+ plistPath
8070
+ ]);
8071
+ if (result.code !== 0) {
8072
+ return null;
8073
+ }
8074
+ const bundleId = result.stdout.trim();
8075
+ return bundleId.length > 0 ? bundleId : null;
8076
+ }
8077
+ async function discoverCodexAppCandidates() {
8078
+ const now = Date.now();
8079
+ const envPath = process.env.CODEXUSE_OFFICIAL_CODEX_APP_PATH?.trim() || null;
8080
+ if (appDiscoveryCache && appDiscoveryCache.envPath === envPath && appDiscoveryCache.expiresAt > now) {
8081
+ return appDiscoveryCache.candidates;
8082
+ }
8083
+ const rawPaths = /* @__PURE__ */ new Set();
8084
+ if (envPath) {
8085
+ rawPaths.add(envPath);
8086
+ }
8087
+ rawPaths.add("/Applications/Codex.app");
8088
+ rawPaths.add(import_node_path11.default.join((0, import_node_os5.homedir)(), "Applications", "Codex.app"));
8089
+ const mdfind = await runCommand2(
8090
+ "/usr/bin/mdfind",
8091
+ ["kMDItemFSName == 'Codex.app'"],
8092
+ 1500
8093
+ );
8094
+ if (mdfind.code === 0) {
8095
+ for (const line of mdfind.stdout.split(/\r?\n/)) {
8096
+ const trimmed = line.trim();
8097
+ if (trimmed.endsWith("/Codex.app")) {
8098
+ rawPaths.add(trimmed);
8099
+ }
8100
+ }
8101
+ }
8102
+ const candidates = [];
8103
+ for (const appPath of rawPaths) {
8104
+ if (!(0, import_node_fs8.existsSync)(appPath) || import_node_path11.default.basename(appPath) !== "Codex.app") {
8105
+ continue;
8106
+ }
8107
+ const bundleId = await readBundleString(appPath, "CFBundleIdentifier");
8108
+ if (!bundleId || bundleId.toLowerCase().includes("codexuse")) {
8109
+ continue;
8110
+ }
8111
+ candidates.push({
8112
+ appPath,
8113
+ bundleId,
8114
+ version: await readBundleString(appPath, "CFBundleShortVersionString"),
8115
+ executableName: await readBundleString(appPath, "CFBundleExecutable") ?? "Codex"
8116
+ });
8117
+ }
8118
+ const sorted = candidates.sort((a, b) => a.appPath.localeCompare(b.appPath));
8119
+ appDiscoveryCache = {
8120
+ envPath,
8121
+ expiresAt: now + (sorted.length > 0 ? APP_DISCOVERY_CACHE_TTL_MS : APP_DISCOVERY_MISS_CACHE_TTL_MS),
8122
+ candidates: sorted
8123
+ };
8124
+ return sorted;
8125
+ }
8126
+ async function getRunningCodexPids(candidate) {
8127
+ const result = await runCommand2("/bin/ps", ["-axo", "pid=,args="], 2e3);
8128
+ const pids = /* @__PURE__ */ new Set();
8129
+ const appContentsRoot = `${import_node_path11.default.join(candidate.appPath, "Contents")}${import_node_path11.default.sep}`;
8130
+ if (result.code === 0) {
8131
+ for (const line of result.stdout.split(/\r?\n/)) {
8132
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
8133
+ if (!match) {
8134
+ continue;
8135
+ }
8136
+ const pid = Number(match[1]);
8137
+ const args = match[2] ?? "";
8138
+ if (Number.isInteger(pid) && args.includes(appContentsRoot)) {
8139
+ pids.add(pid);
8140
+ }
8141
+ }
8142
+ }
8143
+ return Array.from(pids).sort((a, b) => a - b);
8144
+ }
8145
+ async function getRunningCodexMainPids(candidate) {
8146
+ const result = await runCommand2("/bin/ps", ["-axo", "pid=,args="], 2e3);
8147
+ const pids = /* @__PURE__ */ new Set();
8148
+ const executablePath = import_node_path11.default.join(
8149
+ candidate.appPath,
8150
+ "Contents",
8151
+ "MacOS",
8152
+ candidate.executableName
8153
+ );
8154
+ if (result.code === 0) {
8155
+ for (const line of result.stdout.split(/\r?\n/)) {
8156
+ const match = line.match(/^\s*(\d+)\s+(.+)$/);
8157
+ if (!match) {
8158
+ continue;
8159
+ }
8160
+ const pid = Number(match[1]);
8161
+ const args = match[2] ?? "";
8162
+ if (Number.isInteger(pid) && args.includes(executablePath)) {
8163
+ pids.add(pid);
8164
+ }
8165
+ }
8166
+ }
8167
+ return Array.from(pids).sort((a, b) => a - b);
8168
+ }
8169
+ async function resolveCodexAppTarget() {
8170
+ const candidates = await discoverCodexAppCandidates();
8171
+ const fallback = candidates[0] ?? null;
8172
+ if (!fallback) {
8173
+ return { candidate: null, runningPids: [], mainPids: [] };
8174
+ }
8175
+ for (const candidate of candidates) {
8176
+ const [runningPids2, mainPids2] = await Promise.all([
8177
+ getRunningCodexPids(candidate),
8178
+ getRunningCodexMainPids(candidate)
8179
+ ]);
8180
+ if (mainPids2.length > 0) {
8181
+ return { candidate, runningPids: runningPids2, mainPids: mainPids2 };
8182
+ }
8183
+ }
8184
+ const [runningPids, mainPids] = await Promise.all([
8185
+ getRunningCodexPids(fallback),
8186
+ getRunningCodexMainPids(fallback)
8187
+ ]);
8188
+ return { candidate: fallback, runningPids, mainPids };
8189
+ }
8190
+ async function waitForCodexExit(candidate, timeoutMs) {
8191
+ const deadline = Date.now() + timeoutMs;
8192
+ while (Date.now() < deadline) {
8193
+ const pids = await getRunningCodexPids(candidate);
8194
+ if (pids.length === 0) {
8195
+ return true;
8196
+ }
8197
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8198
+ }
8199
+ return false;
8200
+ }
8201
+ async function signalPids(pids, signal) {
8202
+ const signaled = [];
8203
+ for (const pid of pids) {
8204
+ try {
8205
+ process.kill(pid, signal);
8206
+ signaled.push(pid);
8207
+ } catch {
8208
+ }
8209
+ }
8210
+ return signaled;
8211
+ }
8212
+ async function openCodex(candidate) {
8213
+ const byBundle = await runCommand2("/usr/bin/open", ["-b", candidate.bundleId]);
8214
+ if (byBundle.code === 0) {
8215
+ return true;
8216
+ }
8217
+ const byPath = await runCommand2("/usr/bin/open", [candidate.appPath]);
8218
+ return byPath.code === 0;
8219
+ }
8220
+ async function waitForCodexLaunch(candidate) {
8221
+ const deadline = Date.now() + LAUNCH_WAIT_MS;
8222
+ while (Date.now() < deadline) {
8223
+ const pids = await getRunningCodexMainPids(candidate);
8224
+ const pid = pids[0] ?? null;
8225
+ if (pid !== null) {
8226
+ return pid;
8227
+ }
8228
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
8229
+ }
8230
+ return null;
8231
+ }
8232
+ async function rememberProfileSwitch(profileKey) {
8233
+ await patchAppState({
8234
+ officialCodex: {
8235
+ lastProfileSwitchAt: Date.now(),
8236
+ lastProfileSwitchProfileKey: profileKey
8237
+ }
8238
+ });
8239
+ }
8240
+ async function rememberRestartResult(args) {
8241
+ const now = Date.now();
8242
+ const verified = args.status === "restarted" || args.status === "started";
8243
+ await patchAppState({
8244
+ officialCodex: {
8245
+ lastVerifiedLaunchAt: verified ? now : void 0,
8246
+ lastVerifiedLaunchProfileKey: verified ? args.profileKey ?? null : void 0,
8247
+ lastObservedPid: verified ? args.pid ?? null : void 0,
8248
+ lastRestartStatus: args.status,
8249
+ lastRestartReason: args.reason ?? null
8250
+ }
8251
+ });
8252
+ }
8253
+ async function restartOfficialCodexAppOnce(options) {
8254
+ if (process.platform !== "darwin") {
8255
+ await rememberRestartResult({ status: "skipped", reason: "unsupported-platform" });
8256
+ return { status: "skipped", reason: "unsupported-platform" };
8257
+ }
8258
+ const now = Date.now();
8259
+ if (options.source !== "manual" && lastRestartStartedAt > 0 && now - lastRestartStartedAt < RESTART_COOLDOWN_MS) {
8260
+ await rememberRestartResult({
8261
+ status: "skipped",
8262
+ reason: "cooldown",
8263
+ profileKey: options.currentProfileKey
8264
+ });
8265
+ return { status: "skipped", reason: "cooldown" };
8266
+ }
8267
+ const { candidate, runningPids, mainPids } = await resolveCodexAppTarget();
8268
+ if (!candidate) {
8269
+ await rememberRestartResult({
8270
+ status: "skipped",
8271
+ reason: "official-codex-app-not-found",
8272
+ profileKey: options.currentProfileKey
8273
+ });
8274
+ return { status: "skipped", reason: "official-codex-app-not-found" };
8275
+ }
8276
+ const appIsRunning = mainPids.length > 0;
8277
+ if (!appIsRunning && !options.launchIfNotRunning) {
8278
+ await rememberRestartResult({
8279
+ status: "skipped",
8280
+ reason: "official-codex-app-not-running",
8281
+ profileKey: options.currentProfileKey
8282
+ });
8283
+ return { status: "skipped", reason: "official-codex-app-not-running" };
8284
+ }
8285
+ if (options.source !== "manual") {
8286
+ lastRestartStartedAt = Date.now();
8287
+ }
8288
+ if (appIsRunning) {
8289
+ logOfficialCodexRestart("warn", "Force killing official Codex before relaunch.", {
8290
+ appPath: candidate.appPath,
8291
+ bundleId: candidate.bundleId,
8292
+ source: options.source,
8293
+ runningPids,
8294
+ mainPids
8295
+ });
8296
+ const killedPids = await signalPids(runningPids, "SIGKILL");
8297
+ logOfficialCodexRestart("warn", "Sent SIGKILL to official Codex pids.", {
8298
+ appPath: candidate.appPath,
8299
+ bundleId: candidate.bundleId,
8300
+ source: options.source,
8301
+ killedPids
8302
+ });
8303
+ const exited = await waitForCodexExit(candidate, EXIT_WAIT_MS);
8304
+ if (!exited) {
8305
+ const remainingPids = await getRunningCodexPids(candidate);
8306
+ logOfficialCodexRestart("error", "Official Codex pids remained after force kill.", {
8307
+ appPath: candidate.appPath,
8308
+ bundleId: candidate.bundleId,
8309
+ source: options.source,
8310
+ remainingPids
8311
+ });
8312
+ const failed = {
8313
+ status: "failed",
8314
+ appPath: candidate.appPath,
8315
+ bundleId: candidate.bundleId,
8316
+ reason: "force-quit-timeout",
8317
+ phase: "quit"
8318
+ };
8319
+ await rememberRestartResult({
8320
+ status: failed.status,
8321
+ reason: failed.reason,
8322
+ profileKey: options.currentProfileKey
8323
+ });
8324
+ return failed;
8325
+ }
8326
+ logOfficialCodexRestart("info", "Official Codex exited after force kill.", {
8327
+ appPath: candidate.appPath,
8328
+ bundleId: candidate.bundleId,
8329
+ source: options.source
8330
+ });
8331
+ }
8332
+ const opened = await openCodex(candidate);
8333
+ if (!opened) {
8334
+ const failed = {
8335
+ status: "failed",
8336
+ appPath: candidate.appPath,
8337
+ bundleId: candidate.bundleId,
8338
+ reason: "open-failed",
8339
+ phase: "launch"
8340
+ };
8341
+ await rememberRestartResult({
8342
+ status: failed.status,
8343
+ reason: failed.reason,
8344
+ profileKey: options.currentProfileKey
8345
+ });
8346
+ return failed;
8347
+ }
8348
+ const launchedPid = await waitForCodexLaunch(candidate);
8349
+ if (launchedPid === null) {
8350
+ logOfficialCodexRestart("error", "Official Codex launch was not verified.", {
8351
+ appPath: candidate.appPath,
8352
+ bundleId: candidate.bundleId,
8353
+ source: options.source
8354
+ });
8355
+ const failed = {
8356
+ status: "failed",
8357
+ appPath: candidate.appPath,
8358
+ bundleId: candidate.bundleId,
8359
+ reason: "launch-timeout",
8360
+ phase: "launch"
8361
+ };
8362
+ await rememberRestartResult({
8363
+ status: failed.status,
8364
+ reason: failed.reason,
8365
+ profileKey: options.currentProfileKey
8366
+ });
8367
+ return failed;
8368
+ }
8369
+ const status = appIsRunning ? "restarted" : "started";
8370
+ logOfficialCodexRestart("info", "Official Codex launch verified.", {
8371
+ appPath: candidate.appPath,
8372
+ bundleId: candidate.bundleId,
8373
+ source: options.source,
8374
+ status,
8375
+ pid: launchedPid
8376
+ });
8377
+ await rememberRestartResult({
8378
+ status,
8379
+ profileKey: options.currentProfileKey,
8380
+ pid: launchedPid
8381
+ });
8382
+ return {
8383
+ status,
8384
+ appPath: candidate.appPath,
8385
+ bundleId: candidate.bundleId,
8386
+ pid: launchedPid
8387
+ };
8388
+ }
8389
+ function recordOfficialCodexProfileSwitch(profileKey) {
8390
+ return rememberProfileSwitch(profileKey);
8391
+ }
8392
+ function restartOfficialCodexApp(options = {}) {
8393
+ if (restartPromise) {
8394
+ return restartPromise;
8395
+ }
8396
+ restartPromise = restartOfficialCodexAppOnce({
8397
+ source: options.source ?? "manual",
8398
+ currentProfileKey: options.currentProfileKey ?? null,
8399
+ launchIfNotRunning: options.launchIfNotRunning ?? (options.source ?? "manual") === "manual"
8400
+ }).finally(() => {
8401
+ restartPromise = null;
8402
+ });
8403
+ return restartPromise;
8404
+ }
8405
+
7697
8406
  // src/commands/profile.ts
7698
8407
  function formatProfileLabel(name, displayName) {
7699
8408
  if (displayName && displayName.trim() && displayName !== name) {
@@ -7812,6 +8521,7 @@ function formatWindowUsage(window) {
7812
8521
  var EMPTY_USAGE_SUMMARY = {
7813
8522
  windowKey: "primary",
7814
8523
  usagePercent: 0,
8524
+ remainingPercent: 100,
7815
8525
  hasUsage: false,
7816
8526
  resetsInSeconds: null,
7817
8527
  windowMinutes: null
@@ -7835,6 +8545,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7835
8545
  }
7836
8546
  const usageValueRaw = toFiniteNumberOrNull(entry.window.usedPercent);
7837
8547
  const usageValue = usageValueRaw === null ? 0 : Math.max(0, Math.min(100, usageValueRaw));
8548
+ const remainingValue = 100 - usageValue;
7838
8549
  const hasUsage = usageValueRaw !== null;
7839
8550
  const resetsInSeconds = toFiniteNumberOrNull(entry.window.resetsInSeconds);
7840
8551
  const windowMinutes = toFiniteNumberOrNull(entry.window.windowMinutes);
@@ -7843,6 +8554,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7843
8554
  summary = {
7844
8555
  windowKey: entry.key,
7845
8556
  usagePercent: usageValue,
8557
+ remainingPercent: remainingValue,
7846
8558
  hasUsage,
7847
8559
  resetsInSeconds,
7848
8560
  windowMinutes
@@ -7853,6 +8565,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7853
8565
  summary = {
7854
8566
  windowKey: entry.key,
7855
8567
  usagePercent: usageValue,
8568
+ remainingPercent: remainingValue,
7856
8569
  hasUsage,
7857
8570
  resetsInSeconds,
7858
8571
  windowMinutes
@@ -7864,6 +8577,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7864
8577
  summary = {
7865
8578
  windowKey: entry.key,
7866
8579
  usagePercent: usageValue,
8580
+ remainingPercent: remainingValue,
7867
8581
  hasUsage,
7868
8582
  resetsInSeconds,
7869
8583
  windowMinutes
@@ -7875,6 +8589,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7875
8589
  summary = {
7876
8590
  windowKey: entry.key,
7877
8591
  usagePercent: usageValue,
8592
+ remainingPercent: remainingValue,
7878
8593
  hasUsage,
7879
8594
  resetsInSeconds,
7880
8595
  windowMinutes
@@ -7885,6 +8600,7 @@ function summarizeRateLimitSnapshot(snapshot) {
7885
8600
  summary = {
7886
8601
  windowKey: entry.key,
7887
8602
  usagePercent: usageValue,
8603
+ remainingPercent: remainingValue,
7888
8604
  hasUsage,
7889
8605
  resetsInSeconds,
7890
8606
  windowMinutes
@@ -7929,9 +8645,16 @@ async function resolveProfileUsage(manager, profile, codexPath) {
7929
8645
  return fallbackUsage(profile);
7930
8646
  }
7931
8647
  }
7932
- function compareAutoRollCandidates(a, b) {
7933
- if (a.usageSummary.usagePercent !== b.usageSummary.usagePercent) {
7934
- return a.usageSummary.usagePercent - b.usageSummary.usagePercent;
8648
+ function compareAutoRollCandidates(a, b, priorityRanks) {
8649
+ const aRank = priorityRanks.get(resolveProfileIdentityKeyFromProfile(a.profile));
8650
+ const bRank = priorityRanks.get(resolveProfileIdentityKeyFromProfile(b.profile));
8651
+ if (aRank !== void 0 || bRank !== void 0) {
8652
+ if (aRank === void 0) return 1;
8653
+ if (bRank === void 0) return -1;
8654
+ if (aRank !== bRank) return aRank - bRank;
8655
+ }
8656
+ if (a.usageSummary.remainingPercent !== b.usageSummary.remainingPercent) {
8657
+ return b.usageSummary.remainingPercent - a.usageSummary.remainingPercent;
7935
8658
  }
7936
8659
  const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7937
8660
  const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
@@ -7940,37 +8663,38 @@ function compareAutoRollCandidates(a, b) {
7940
8663
  }
7941
8664
  return a.profile.name.localeCompare(b.profile.name);
7942
8665
  }
7943
- function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchThreshold) {
7944
- const candidates = profiles.filter((profile) => profile.name !== currentProfileName && profile.isValid && !profile.tokenStatus?.requiresUserAction).map((profile) => ({
8666
+ function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchRemainingThreshold, priorityOrder = [], blockedProfileNames = /* @__PURE__ */ new Set()) {
8667
+ const priorityRanks = /* @__PURE__ */ new Map();
8668
+ for (const [index, key] of priorityOrder.entries()) {
8669
+ const normalized = key.trim();
8670
+ if (normalized && !priorityRanks.has(normalized)) {
8671
+ priorityRanks.set(normalized, index);
8672
+ }
8673
+ }
8674
+ const candidates = profiles.filter(
8675
+ (profile) => profile.name !== currentProfileName && !blockedProfileNames.has(profile.name) && profile.isValid && !profile.tokenStatus?.requiresUserAction
8676
+ ).map((profile) => ({
7945
8677
  profile,
7946
8678
  usageSummary: usageMap.get(profile.name)?.usageSummary ?? summarizeRateLimitSnapshot(profile.rateLimit ?? null)
7947
8679
  }));
7948
8680
  if (candidates.length === 0) {
7949
8681
  return null;
7950
8682
  }
7951
- const belowThreshold = candidates.filter((candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.usagePercent < switchThreshold).sort(compareAutoRollCandidates);
8683
+ const belowThreshold = candidates.filter(
8684
+ (candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.remainingPercent > switchRemainingThreshold
8685
+ ).sort((a, b) => compareAutoRollCandidates(a, b, priorityRanks));
7952
8686
  let selected = belowThreshold[0] ?? null;
7953
8687
  if (!selected) {
7954
- selected = candidates.filter((candidate) => candidate.usageSummary.hasUsage).sort(compareAutoRollCandidates)[0] ?? null;
7955
- }
7956
- if (!selected) {
7957
- selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => {
7958
- const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7959
- const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7960
- if (aReset !== bReset) {
7961
- return aReset - bReset;
7962
- }
7963
- return a.profile.name.localeCompare(b.profile.name);
7964
- })[0] ?? null;
8688
+ selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => compareAutoRollCandidates(a, b, priorityRanks))[0] ?? null;
7965
8689
  }
7966
8690
  if (!selected) {
7967
8691
  return null;
7968
8692
  }
7969
8693
  if (selected.usageSummary.hasUsage && currentSummary.hasUsage) {
7970
- if (selected.usageSummary.usagePercent > currentSummary.usagePercent) {
8694
+ if (selected.usageSummary.remainingPercent < currentSummary.remainingPercent) {
7971
8695
  return null;
7972
8696
  }
7973
- if (selected.usageSummary.usagePercent === currentSummary.usagePercent) {
8697
+ if (selected.usageSummary.remainingPercent === currentSummary.remainingPercent) {
7974
8698
  const selectedReset = selected.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7975
8699
  const currentReset = currentSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
7976
8700
  if (selectedReset >= currentReset) {
@@ -8054,18 +8778,31 @@ async function mapWithConcurrency(items, limit, fn) {
8054
8778
  await Promise.all(workers);
8055
8779
  return results;
8056
8780
  }
8057
- function parseAutoRollThreshold(flags, fallbackThreshold) {
8058
- const explicitThreshold = parseNumericFlag(flags, "--threshold");
8059
- if (explicitThreshold === null) {
8060
- return fallbackThreshold;
8781
+ function parseAutoRollSwitchRemainingThreshold(flags, fallbackThreshold) {
8782
+ const explicitLeft = parseNumericFlag(flags, "--switch-left");
8783
+ const explicitLegacyUsed = parseNumericFlag(flags, "--threshold");
8784
+ if (explicitLeft !== null && explicitLegacyUsed !== null) {
8785
+ throw new Error("Use either --switch-left or legacy --threshold, not both.");
8061
8786
  }
8062
- if (Number.isNaN(explicitThreshold)) {
8063
- throw new Error("Invalid --threshold value. Use --threshold=50-100.");
8787
+ if (explicitLeft !== null) {
8788
+ if (Number.isNaN(explicitLeft)) {
8789
+ throw new Error("Invalid --switch-left value. Use --switch-left=0-50.");
8790
+ }
8791
+ if (explicitLeft < AUTO_ROLL_SWITCH_REMAINING_MIN || explicitLeft > AUTO_ROLL_SWITCH_REMAINING_MAX) {
8792
+ throw new Error("Invalid --switch-left value. Use a number between 0 and 50.");
8793
+ }
8794
+ return sanitizeAutoRollSwitchRemainingThreshold(explicitLeft);
8064
8795
  }
8065
- if (explicitThreshold < 50 || explicitThreshold > 100) {
8066
- throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
8796
+ if (explicitLegacyUsed !== null) {
8797
+ if (Number.isNaN(explicitLegacyUsed)) {
8798
+ throw new Error("Invalid --threshold value. Use --threshold=50-100.");
8799
+ }
8800
+ if (explicitLegacyUsed < 50 || explicitLegacyUsed > 100) {
8801
+ throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
8802
+ }
8803
+ return sanitizeAutoRollSwitchRemainingThreshold(100 - explicitLegacyUsed);
8067
8804
  }
8068
- return Math.round(explicitThreshold);
8805
+ return sanitizeAutoRollSwitchRemainingThreshold(fallbackThreshold);
8069
8806
  }
8070
8807
  function parseAutoRollIntervalSeconds(flags) {
8071
8808
  const explicitInterval = parseIntegerFlag(flags, "--interval");
@@ -8077,6 +8814,74 @@ function parseAutoRollIntervalSeconds(flags) {
8077
8814
  }
8078
8815
  return explicitInterval;
8079
8816
  }
8817
+ function resolveRestartCodexFlag(flags, fallback) {
8818
+ const restart = hasFlag(flags, "--restart-codex");
8819
+ const noRestart = hasFlag(flags, "--no-restart-codex");
8820
+ if (restart && noRestart) {
8821
+ throw new Error("Use either --restart-codex or --no-restart-codex, not both.");
8822
+ }
8823
+ if (restart) {
8824
+ return true;
8825
+ }
8826
+ if (noRestart) {
8827
+ return false;
8828
+ }
8829
+ return fallback;
8830
+ }
8831
+ function findProfileKey(profiles, name) {
8832
+ const profile = profiles.find((entry) => entry.name === name);
8833
+ return profile ? profileRateLimitKey(profile) : null;
8834
+ }
8835
+ function rearmBlockedProfiles(blockedProfiles, usageMap, rearmRemainingThreshold) {
8836
+ if (!blockedProfiles) {
8837
+ return;
8838
+ }
8839
+ for (const profileName of Array.from(blockedProfiles)) {
8840
+ const summary = usageMap.get(profileName)?.usageSummary;
8841
+ if (!summary || !summary.hasUsage || summary.remainingPercent > rearmRemainingThreshold) {
8842
+ blockedProfiles.delete(profileName);
8843
+ }
8844
+ }
8845
+ }
8846
+ async function rememberOfficialCodexSwitch(profileKey) {
8847
+ try {
8848
+ await recordOfficialCodexProfileSwitch(profileKey);
8849
+ } catch (error) {
8850
+ console.warn(
8851
+ `Could not record official Codex sync state: ${error instanceof Error ? error.message : String(error)}`
8852
+ );
8853
+ }
8854
+ }
8855
+ function formatOfficialCodexRestart(result) {
8856
+ if (result.status === "restarted") {
8857
+ return "Official Codex restarted.";
8858
+ }
8859
+ if (result.status === "started") {
8860
+ return "Official Codex started.";
8861
+ }
8862
+ if (result.status === "failed") {
8863
+ return `Official Codex restart failed: ${result.reason}.`;
8864
+ }
8865
+ if (result.status === "skipped") {
8866
+ return `Official Codex restart skipped: ${result.reason}.`;
8867
+ }
8868
+ return "Official Codex restart status unknown.";
8869
+ }
8870
+ async function maybeRestartOfficialCodex(options) {
8871
+ if (!options.enabled) {
8872
+ return null;
8873
+ }
8874
+ try {
8875
+ const result = await restartOfficialCodexApp({
8876
+ source: options.source,
8877
+ currentProfileKey: options.profileKey,
8878
+ launchIfNotRunning: options.launchIfNotRunning
8879
+ });
8880
+ return formatOfficialCodexRestart(result);
8881
+ } catch (error) {
8882
+ return `Official Codex restart failed: ${error instanceof Error ? error.message : String(error)}.`;
8883
+ }
8884
+ }
8080
8885
  async function collectUsageMap(manager, profiles, codexPath) {
8081
8886
  const usageMap = /* @__PURE__ */ new Map();
8082
8887
  const uniqueProfilesByAccount = /* @__PURE__ */ new Map();
@@ -8126,6 +8931,11 @@ async function runAutoRollPass(manager, options) {
8126
8931
  throw new Error("Codex CLI binary not found on PATH. Auto-roll requires codex for live rate limits.");
8127
8932
  }
8128
8933
  const usageMap = await collectUsageMap(manager, profiles, codexPath);
8934
+ rearmBlockedProfiles(
8935
+ options.blockedProfiles,
8936
+ usageMap,
8937
+ options.rearmRemainingThreshold
8938
+ );
8129
8939
  const currentProfile = profiles.find((profile) => profile.name === current.name);
8130
8940
  if (!currentProfile) {
8131
8941
  return {
@@ -8144,10 +8954,31 @@ async function runAutoRollPass(manager, options) {
8144
8954
  target: null
8145
8955
  };
8146
8956
  }
8147
- if (currentUsage.usagePercent < options.threshold) {
8957
+ if (options.blockedProfiles?.has(current.name)) {
8958
+ if (currentUsage.remainingPercent > options.rearmRemainingThreshold) {
8959
+ options.blockedProfiles.delete(current.name);
8960
+ } else {
8961
+ return {
8962
+ switched: false,
8963
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left; waiting for re-arm above ${options.rearmRemainingThreshold}% left.`,
8964
+ source: current.name,
8965
+ target: null
8966
+ };
8967
+ }
8968
+ }
8969
+ if (options.blockCurrentUntilRearm && currentUsage.remainingPercent <= options.switchRemainingThreshold) {
8970
+ options.blockedProfiles?.add(current.name);
8971
+ return {
8972
+ switched: false,
8973
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left; waiting for re-arm above ${options.rearmRemainingThreshold}% left.`,
8974
+ source: current.name,
8975
+ target: null
8976
+ };
8977
+ }
8978
+ if (currentUsage.remainingPercent > options.switchRemainingThreshold) {
8148
8979
  return {
8149
8980
  switched: false,
8150
- message: `'${current.name}' at ${Math.round(currentUsage.usagePercent)}%, below threshold ${options.threshold}%.`,
8981
+ message: `'${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left, above switch threshold ${options.switchRemainingThreshold}% left.`,
8151
8982
  source: current.name,
8152
8983
  target: null
8153
8984
  };
@@ -8157,29 +8988,41 @@ async function runAutoRollPass(manager, options) {
8157
8988
  usageMap,
8158
8989
  current.name,
8159
8990
  currentUsage,
8160
- options.threshold
8991
+ options.switchRemainingThreshold,
8992
+ options.priorityOrder ?? [],
8993
+ options.blockedProfiles
8161
8994
  );
8162
8995
  if (!candidate) {
8163
8996
  return {
8164
8997
  switched: false,
8165
- message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.usagePercent)}%.`,
8998
+ message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.remainingPercent)}% left.`,
8166
8999
  source: current.name,
8167
9000
  target: null
8168
9001
  };
8169
9002
  }
8170
- const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.usagePercent)}%` : "n/a";
9003
+ const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.remainingPercent)}% left` : "n/a";
8171
9004
  if (options.dryRun) {
9005
+ const restartSuffix = options.restartOfficialCodex ? " Official Codex would restart after the switch if running." : "";
8172
9006
  return {
8173
9007
  switched: false,
8174
- message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
9008
+ message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.remainingPercent)}% left) -> '${candidate.profile.name}' (${candidateUsage}).${restartSuffix}`,
8175
9009
  source: current.name,
8176
9010
  target: candidate.profile.name
8177
9011
  };
8178
9012
  }
8179
9013
  await manager.switchToProfile(candidate.profile.name);
9014
+ const targetProfileKey = profileRateLimitKey(candidate.profile);
9015
+ await rememberOfficialCodexSwitch(targetProfileKey);
9016
+ const restartMessage = await maybeRestartOfficialCodex({
9017
+ enabled: options.restartOfficialCodex,
9018
+ source: "auto-roll",
9019
+ profileKey: targetProfileKey,
9020
+ launchIfNotRunning: options.launchOfficialCodexWhenClosed === true
9021
+ });
9022
+ options.blockedProfiles?.add(current.name);
8180
9023
  return {
8181
9024
  switched: true,
8182
- message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
9025
+ message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.remainingPercent)}% left) -> '${candidate.profile.name}' (${candidateUsage}).${restartMessage ? ` ${restartMessage}` : ""}`,
8183
9026
  source: current.name,
8184
9027
  target: candidate.profile.name
8185
9028
  };
@@ -8279,20 +9122,51 @@ async function handleProfileCommand(args, version) {
8279
9122
  if (!name) {
8280
9123
  throw new Error("Profile name is required.");
8281
9124
  }
9125
+ const restartOfficialCodex = resolveRestartCodexFlag(flags, false);
9126
+ const profiles = await manager.listProfiles();
9127
+ const targetProfileKey = findProfileKey(profiles, name);
8282
9128
  await manager.switchToProfile(name);
8283
9129
  console.log(`Switched to profile: ${name}`);
9130
+ await rememberOfficialCodexSwitch(targetProfileKey);
9131
+ const restartMessage = await maybeRestartOfficialCodex({
9132
+ enabled: restartOfficialCodex,
9133
+ source: "manual",
9134
+ profileKey: targetProfileKey
9135
+ });
9136
+ if (restartMessage) {
9137
+ console.log(restartMessage);
9138
+ }
8284
9139
  return;
8285
9140
  }
8286
9141
  case "autoroll":
8287
9142
  case "auto-roll": {
9143
+ const license = await licenseService.getStatus();
9144
+ if (!license.isPro) {
9145
+ throw new Error("Auto-roll requires a Pro license.");
9146
+ }
8288
9147
  const watch = hasFlag(flags, "--watch");
8289
9148
  const dryRun = hasFlag(flags, "--dry-run");
8290
9149
  const settings = await getStoredAutoRollSettings().catch(() => null);
8291
- const fallbackThreshold = typeof settings?.switchThreshold === "number" && Number.isFinite(settings.switchThreshold) ? Math.round(settings.switchThreshold) : DEFAULT_AUTOROLL_THRESHOLD;
8292
- const threshold = parseAutoRollThreshold(flags, fallbackThreshold);
9150
+ const restartOfficialCodex = resolveRestartCodexFlag(
9151
+ flags,
9152
+ settings?.restartOfficialCodexOnAutoRoll === true
9153
+ );
9154
+ const fallbackSwitchRemainingThreshold = typeof settings?.switchRemainingThreshold === "number" && Number.isFinite(settings.switchRemainingThreshold) ? settings.switchRemainingThreshold : DEFAULT_AUTO_ROLL_SWITCH_REMAINING_THRESHOLD;
9155
+ const switchRemainingThreshold = parseAutoRollSwitchRemainingThreshold(
9156
+ flags,
9157
+ fallbackSwitchRemainingThreshold
9158
+ );
9159
+ const rearmRemainingThreshold = sanitizeAutoRollRearmRemainingThreshold(
9160
+ settings?.rearmRemainingThreshold ?? DEFAULT_AUTO_ROLL_REARM_REMAINING_THRESHOLD,
9161
+ switchRemainingThreshold
9162
+ );
8293
9163
  const intervalSeconds = parseAutoRollIntervalSeconds(flags);
8294
9164
  if (watch) {
8295
- console.log(`Auto-roll watch: threshold ${threshold}% | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}`);
9165
+ console.log(
9166
+ `Auto-roll watch: switch at/below ${switchRemainingThreshold}% left | re-arm above ${rearmRemainingThreshold}% left | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}${restartOfficialCodex ? " | restart-codex" : ""}`
9167
+ );
9168
+ const blockedProfiles = /* @__PURE__ */ new Set();
9169
+ let lastObservedProfile = null;
8296
9170
  let stop = false;
8297
9171
  process.on("SIGINT", () => {
8298
9172
  stop = true;
@@ -8301,8 +9175,23 @@ async function handleProfileCommand(args, version) {
8301
9175
  stop = true;
8302
9176
  });
8303
9177
  while (!stop) {
8304
- const result2 = await runAutoRollPass(manager, { threshold, dryRun });
9178
+ const current = await manager.getCurrentProfile();
9179
+ const currentName = current.name ?? null;
9180
+ const blockCurrentUntilRearm = Boolean(
9181
+ currentName && lastObservedProfile && currentName !== lastObservedProfile
9182
+ );
9183
+ const result2 = await runAutoRollPass(manager, {
9184
+ switchRemainingThreshold,
9185
+ rearmRemainingThreshold,
9186
+ dryRun,
9187
+ priorityOrder: settings?.priorityOrder ?? [],
9188
+ restartOfficialCodex,
9189
+ launchOfficialCodexWhenClosed: settings?.launchOfficialCodexWhenClosedOnAutoRoll === true,
9190
+ blockedProfiles,
9191
+ blockCurrentUntilRearm
9192
+ });
8305
9193
  console.log(result2.message);
9194
+ lastObservedProfile = result2.switched ? result2.target ?? currentName : currentName ?? result2.source;
8306
9195
  if (stop) {
8307
9196
  break;
8308
9197
  }
@@ -8311,7 +9200,14 @@ async function handleProfileCommand(args, version) {
8311
9200
  console.log("Auto-roll watch stopped.");
8312
9201
  return;
8313
9202
  }
8314
- const result = await runAutoRollPass(manager, { threshold, dryRun });
9203
+ const result = await runAutoRollPass(manager, {
9204
+ switchRemainingThreshold,
9205
+ rearmRemainingThreshold,
9206
+ dryRun,
9207
+ priorityOrder: settings?.priorityOrder ?? [],
9208
+ restartOfficialCodex,
9209
+ launchOfficialCodexWhenClosed: settings?.launchOfficialCodexWhenClosedOnAutoRoll === true
9210
+ });
8315
9211
  console.log(result.message);
8316
9212
  return;
8317
9213
  }
@@ -8341,8 +9237,8 @@ async function handleProfileCommand(args, version) {
8341
9237
  }
8342
9238
 
8343
9239
  // ../../packages/runtime-profiles/src/cloud-sync/service.ts
8344
- var import_node_fs8 = require("fs");
8345
- var import_node_path14 = __toESM(require("path"), 1);
9240
+ var import_node_fs9 = require("fs");
9241
+ var import_node_path15 = __toESM(require("path"), 1);
8346
9242
 
8347
9243
  // ../../packages/contracts/src/cloud-sync/types.ts
8348
9244
  var CLOUD_SYNC_SCHEMA_VERSION = 1;
@@ -8502,9 +9398,9 @@ async function pushRemoteSnapshot(licenseKey, snapshot, options) {
8502
9398
  var import_toml2 = __toESM(require_toml(), 1);
8503
9399
 
8504
9400
  // ../../packages/runtime-codex/src/codex/config-metadata.ts
8505
- var import_node_child_process7 = require("child_process");
9401
+ var import_node_child_process8 = require("child_process");
8506
9402
  var import_promises = require("fs/promises");
8507
- var import_node_path11 = __toESM(require("path"), 1);
9403
+ var import_node_path12 = __toESM(require("path"), 1);
8508
9404
  var CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/config.schema.json";
8509
9405
  var METADATA_CACHE_TTL_MS = 6e4;
8510
9406
  var FALLBACK_SCHEMA_TOP_LEVEL_KEYS = [
@@ -8612,9 +9508,9 @@ function isRecord5(value) {
8612
9508
  function uniqueSorted(values) {
8613
9509
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
8614
9510
  }
8615
- async function runCommand2(command, args, timeoutMs = 3e3) {
9511
+ async function runCommand3(command, args, timeoutMs = 3e3) {
8616
9512
  return new Promise((resolve, reject) => {
8617
- const child = (0, import_node_child_process7.spawn)(command, args, {
9513
+ const child = (0, import_node_child_process8.spawn)(command, args, {
8618
9514
  stdio: ["ignore", "pipe", "pipe"],
8619
9515
  env: process.env
8620
9516
  });
@@ -8649,7 +9545,7 @@ async function runCodexCommand(args) {
8649
9545
  const isJsLauncher = codexPath.endsWith(".js");
8650
9546
  const command = isJsLauncher ? process.execPath : codexPath;
8651
9547
  const commandArgs = isJsLauncher ? [codexPath, ...args] : args;
8652
- return runCommand2(command, commandArgs);
9548
+ return runCommand3(command, commandArgs);
8653
9549
  }
8654
9550
  function parseFeatureCatalog(output) {
8655
9551
  const entries = [];
@@ -8690,8 +9586,8 @@ function parseSchemaKeys(schemaRaw) {
8690
9586
  async function readSchemaFromLocal() {
8691
9587
  const candidates = [
8692
9588
  process.env.CODEX_CONFIG_SCHEMA_PATH,
8693
- import_node_path11.default.join(process.cwd(), "codex-rs", "core", "config.schema.json"),
8694
- import_node_path11.default.join(process.cwd(), "node_modules", "@openai", "codex", "codex-rs", "core", "config.schema.json")
9589
+ import_node_path12.default.join(process.cwd(), "codex-rs", "core", "config.schema.json"),
9590
+ import_node_path12.default.join(process.cwd(), "node_modules", "@openai", "codex", "codex-rs", "core", "config.schema.json")
8695
9591
  ].filter((candidate) => Boolean(candidate));
8696
9592
  for (const candidate of candidates) {
8697
9593
  try {
@@ -8796,20 +9692,20 @@ async function getCodexConfigMetadata(forceRefresh = false) {
8796
9692
 
8797
9693
  // ../../packages/runtime-codex/src/codex/config-io.ts
8798
9694
  var import_promises2 = require("fs/promises");
8799
- var import_node_path13 = __toESM(require("path"), 1);
9695
+ var import_node_path14 = __toESM(require("path"), 1);
8800
9696
 
8801
9697
  // ../../packages/runtime-codex/src/codex/home.ts
8802
- var import_node_os5 = __toESM(require("os"), 1);
8803
- var import_node_path12 = __toESM(require("path"), 1);
9698
+ var import_node_os6 = __toESM(require("os"), 1);
9699
+ var import_node_path13 = __toESM(require("path"), 1);
8804
9700
  function resolveHomeDir() {
8805
- return process.env.HOME || process.env.USERPROFILE || import_node_os5.default.homedir();
9701
+ return process.env.HOME || process.env.USERPROFILE || import_node_os6.default.homedir();
8806
9702
  }
8807
9703
  function expandHomePrefix(input, homeDir) {
8808
9704
  if (input === "~") {
8809
9705
  return homeDir;
8810
9706
  }
8811
9707
  if (input.startsWith("~/") || input.startsWith("~\\")) {
8812
- return import_node_path12.default.join(homeDir, input.slice(2));
9708
+ return import_node_path13.default.join(homeDir, input.slice(2));
8813
9709
  }
8814
9710
  return input;
8815
9711
  }
@@ -8825,13 +9721,10 @@ function normalizeCodexHomePath(input) {
8825
9721
  if (!configured) {
8826
9722
  return null;
8827
9723
  }
8828
- return import_node_path12.default.resolve(expandHomePrefix(configured, resolveHomeDir()));
9724
+ return import_node_path13.default.resolve(expandHomePrefix(configured, resolveHomeDir()));
8829
9725
  }
8830
9726
  function resolveDefaultCodexHomeDir() {
8831
- return import_node_path12.default.join(resolveHomeDir(), ".codex");
8832
- }
8833
- function resolveProfileCodexHomeDir(profileName) {
8834
- return import_node_path12.default.join(getUserDataDir(), "profile-homes", profileName);
9727
+ return import_node_path13.default.join(resolveHomeDir(), ".codex");
8835
9728
  }
8836
9729
  function resolveCodexHomeDir(options) {
8837
9730
  return normalizeCodexHomePath(options?.codexHomePath) ?? resolveDefaultCodexHomeDir();
@@ -8846,14 +9739,6 @@ async function resolveCodexRuntimeContext(options) {
8846
9739
  };
8847
9740
  }
8848
9741
  const state = await getAppState();
8849
- const profileName = normalizePathCandidate(state.app.lastProfileName);
8850
- if (profileName && state.profilesByName[profileName]) {
8851
- return {
8852
- codexHomePath: resolveProfileCodexHomeDir(profileName),
8853
- profileName,
8854
- source: "profile"
8855
- };
8856
- }
8857
9742
  const configured = normalizeCodexHomePath(
8858
9743
  state.runtimeSettings?.codexHome
8859
9744
  );
@@ -8912,10 +9797,10 @@ var YOLO_PRESET = {
8912
9797
 
8913
9798
  // ../../packages/runtime-codex/src/codex/config-io.ts
8914
9799
  function getConfigPath(options) {
8915
- return import_node_path13.default.join(resolveCodexHomeDir(options), "config.toml");
9800
+ return import_node_path14.default.join(resolveCodexHomeDir(options), "config.toml");
8916
9801
  }
8917
9802
  async function ensureConfigDirExists(filePath) {
8918
- const dir = import_node_path13.default.dirname(filePath);
9803
+ const dir = import_node_path14.default.dirname(filePath);
8919
9804
  await (0, import_promises2.mkdir)(dir, { recursive: true });
8920
9805
  }
8921
9806
  function normalizeLineEndings2(content) {
@@ -9569,13 +10454,13 @@ function formatMegabytes(bytes) {
9569
10454
  return (bytes / MB_DIVISOR).toFixed(2);
9570
10455
  }
9571
10456
  function resolveSyncBackupsDir() {
9572
- return import_node_path14.default.join(getUserDataDir(), SYNC_BACKUP_DIR);
10457
+ return import_node_path15.default.join(getUserDataDir(), SYNC_BACKUP_DIR);
9573
10458
  }
9574
10459
  function toSafeIsoForFileName(iso) {
9575
10460
  return iso.replace(/:/g, "-");
9576
10461
  }
9577
10462
  async function pruneOldPrePullBackups(backupsDir) {
9578
- const entries = await import_node_fs8.promises.readdir(backupsDir, { withFileTypes: true });
10463
+ const entries = await import_node_fs9.promises.readdir(backupsDir, { withFileTypes: true });
9579
10464
  const files = entries.filter(
9580
10465
  (entry) => entry.isFile() && entry.name.startsWith(PRE_PULL_BACKUP_PREFIX) && entry.name.endsWith(PRE_PULL_BACKUP_SUFFIX)
9581
10466
  ).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
@@ -9583,7 +10468,7 @@ async function pruneOldPrePullBackups(backupsDir) {
9583
10468
  return;
9584
10469
  }
9585
10470
  for (const stale of files.slice(PRE_PULL_BACKUP_KEEP_COUNT)) {
9586
- await import_node_fs8.promises.rm(import_node_path14.default.join(backupsDir, stale), { force: true });
10471
+ await import_node_fs9.promises.rm(import_node_path15.default.join(backupsDir, stale), { force: true });
9587
10472
  }
9588
10473
  }
9589
10474
  async function createPrePullBackup(profileManager) {
@@ -9591,13 +10476,13 @@ async function createPrePullBackup(profileManager) {
9591
10476
  enforcePushGuards: false
9592
10477
  });
9593
10478
  const backupsDir = resolveSyncBackupsDir();
9594
- await import_node_fs8.promises.mkdir(backupsDir, { recursive: true });
10479
+ await import_node_fs9.promises.mkdir(backupsDir, { recursive: true });
9595
10480
  const timestamp = toSafeIsoForFileName((/* @__PURE__ */ new Date()).toISOString());
9596
- const backupPath = import_node_path14.default.join(
10481
+ const backupPath = import_node_path15.default.join(
9597
10482
  backupsDir,
9598
10483
  `${PRE_PULL_BACKUP_PREFIX}${timestamp}${PRE_PULL_BACKUP_SUFFIX}`
9599
10484
  );
9600
- await import_node_fs8.promises.writeFile(backupPath, `${JSON.stringify(snapshot, null, 2)}
10485
+ await import_node_fs9.promises.writeFile(backupPath, `${JSON.stringify(snapshot, null, 2)}
9601
10486
  `, "utf8");
9602
10487
  await pruneOldPrePullBackups(backupsDir);
9603
10488
  logInfo("[cloud-sync] created pre-pull backup", { backupPath });
@@ -9866,9 +10751,9 @@ async function handleSync(args, version) {
9866
10751
  }
9867
10752
 
9868
10753
  // ../../packages/runtime-app-state/src/storage/migrations/v1.ts
9869
- var import_node_fs9 = require("fs");
9870
- var import_node_path15 = __toESM(require("path"), 1);
9871
- var import_node_os6 = __toESM(require("os"), 1);
10754
+ var import_node_fs10 = require("fs");
10755
+ var import_node_path16 = __toESM(require("path"), 1);
10756
+ var import_node_os7 = __toESM(require("os"), 1);
9872
10757
 
9873
10758
  // ../../packages/contracts/src/settings/legacy-localstorage-keys.ts
9874
10759
  var LEGACY_LOCALSTORAGE_KEYS = [
@@ -9907,20 +10792,20 @@ function isMissingPathError(error) {
9907
10792
  return error.code === "ENOENT";
9908
10793
  }
9909
10794
  function resolveHomeDir2() {
9910
- const home = process.env.HOME || process.env.USERPROFILE || import_node_os6.default.homedir();
10795
+ const home = process.env.HOME || process.env.USERPROFILE || import_node_os7.default.homedir();
9911
10796
  if (!home) {
9912
10797
  throw new Error("HOME is not set.");
9913
10798
  }
9914
10799
  return home;
9915
10800
  }
9916
10801
  function resolveCodexDir() {
9917
- return import_node_path15.default.join(resolveHomeDir2(), ".codex");
10802
+ return import_node_path16.default.join(resolveHomeDir2(), ".codex");
9918
10803
  }
9919
10804
  function resolveLegacyPath(...segments) {
9920
- return import_node_path15.default.join(resolveCodexDir(), ...segments);
10805
+ return import_node_path16.default.join(resolveCodexDir(), ...segments);
9921
10806
  }
9922
10807
  async function readJsonFile(filePath) {
9923
- const raw = await import_node_fs9.promises.readFile(filePath, "utf8");
10808
+ const raw = await import_node_fs10.promises.readFile(filePath, "utf8");
9924
10809
  return JSON.parse(raw);
9925
10810
  }
9926
10811
  async function readJsonFileIfExists(filePath) {
@@ -9935,14 +10820,14 @@ async function readJsonFileIfExists(filePath) {
9935
10820
  }
9936
10821
  }
9937
10822
  async function copyDirectoryManual(source, destination) {
9938
- await import_node_fs9.promises.mkdir(destination, { recursive: true });
9939
- const entries = await import_node_fs9.promises.readdir(source, { withFileTypes: true });
10823
+ await import_node_fs10.promises.mkdir(destination, { recursive: true });
10824
+ const entries = await import_node_fs10.promises.readdir(source, { withFileTypes: true });
9940
10825
  for (const entry of entries) {
9941
- const srcPath = import_node_path15.default.join(source, entry.name);
9942
- const destPath = import_node_path15.default.join(destination, entry.name);
10826
+ const srcPath = import_node_path16.default.join(source, entry.name);
10827
+ const destPath = import_node_path16.default.join(destination, entry.name);
9943
10828
  if (entry.isSymbolicLink()) {
9944
- const linkTarget = await import_node_fs9.promises.readlink(srcPath);
9945
- await import_node_fs9.promises.symlink(linkTarget, destPath).catch((error) => {
10829
+ const linkTarget = await import_node_fs10.promises.readlink(srcPath);
10830
+ await import_node_fs10.promises.symlink(linkTarget, destPath).catch((error) => {
9946
10831
  if (error.code !== "EEXIST") {
9947
10832
  throw error;
9948
10833
  }
@@ -9954,14 +10839,14 @@ async function copyDirectoryManual(source, destination) {
9954
10839
  continue;
9955
10840
  }
9956
10841
  if (entry.isFile()) {
9957
- await import_node_fs9.promises.copyFile(srcPath, destPath);
10842
+ await import_node_fs10.promises.copyFile(srcPath, destPath);
9958
10843
  }
9959
10844
  }
9960
10845
  }
9961
10846
  async function copyDirIfExists(source, destination) {
9962
10847
  let stats = null;
9963
10848
  try {
9964
- stats = await import_node_fs9.promises.stat(source);
10849
+ stats = await import_node_fs10.promises.stat(source);
9965
10850
  } catch (error) {
9966
10851
  if (error.code === "ENOENT") {
9967
10852
  return;
@@ -9971,10 +10856,10 @@ async function copyDirIfExists(source, destination) {
9971
10856
  if (!stats.isDirectory()) {
9972
10857
  return;
9973
10858
  }
9974
- await import_node_fs9.promises.rm(destination, { recursive: true, force: true });
9975
- await import_node_fs9.promises.mkdir(import_node_path15.default.dirname(destination), { recursive: true });
10859
+ await import_node_fs10.promises.rm(destination, { recursive: true, force: true });
10860
+ await import_node_fs10.promises.mkdir(import_node_path16.default.dirname(destination), { recursive: true });
9976
10861
  try {
9977
- await import_node_fs9.promises.cp(source, destination, {
10862
+ await import_node_fs10.promises.cp(source, destination, {
9978
10863
  recursive: true,
9979
10864
  errorOnExist: false,
9980
10865
  force: true,
@@ -9991,7 +10876,7 @@ async function copyDirIfExists(source, destination) {
9991
10876
  }
9992
10877
  async function copyFileIfExists(source, destination, options) {
9993
10878
  try {
9994
- const sourceStat = await import_node_fs9.promises.stat(source);
10879
+ const sourceStat = await import_node_fs10.promises.stat(source);
9995
10880
  if (!sourceStat.isFile()) {
9996
10881
  return;
9997
10882
  }
@@ -10002,7 +10887,7 @@ async function copyFileIfExists(source, destination, options) {
10002
10887
  throw error;
10003
10888
  }
10004
10889
  try {
10005
- const destinationStat = await import_node_fs9.promises.stat(destination);
10890
+ const destinationStat = await import_node_fs10.promises.stat(destination);
10006
10891
  if (destinationStat.isFile()) {
10007
10892
  return;
10008
10893
  }
@@ -10011,17 +10896,17 @@ async function copyFileIfExists(source, destination, options) {
10011
10896
  throw error;
10012
10897
  }
10013
10898
  }
10014
- await import_node_fs9.promises.mkdir(import_node_path15.default.dirname(destination), { recursive: true });
10015
- await import_node_fs9.promises.copyFile(source, destination);
10899
+ await import_node_fs10.promises.mkdir(import_node_path16.default.dirname(destination), { recursive: true });
10900
+ await import_node_fs10.promises.copyFile(source, destination);
10016
10901
  if (typeof options?.mode === "number") {
10017
- await import_node_fs9.promises.chmod(destination, options.mode).catch(() => void 0);
10902
+ await import_node_fs10.promises.chmod(destination, options.mode).catch(() => void 0);
10018
10903
  }
10019
10904
  }
10020
10905
  async function cleanupCopiedSkillsMetadata(skillsDir) {
10021
- await removeIfExists(import_node_path15.default.join(skillsDir, LEGACY_SKILLS_REPOS_FILE));
10906
+ await removeIfExists(import_node_path16.default.join(skillsDir, LEGACY_SKILLS_REPOS_FILE));
10022
10907
  let entries = [];
10023
10908
  try {
10024
- entries = await import_node_fs9.promises.readdir(skillsDir, { withFileTypes: true });
10909
+ entries = await import_node_fs10.promises.readdir(skillsDir, { withFileTypes: true });
10025
10910
  } catch {
10026
10911
  return;
10027
10912
  }
@@ -10029,30 +10914,30 @@ async function cleanupCopiedSkillsMetadata(skillsDir) {
10029
10914
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10030
10915
  continue;
10031
10916
  }
10032
- await removeIfExists(import_node_path15.default.join(skillsDir, entry.name, LEGACY_SKILL_MANIFEST));
10917
+ await removeIfExists(import_node_path16.default.join(skillsDir, entry.name, LEGACY_SKILL_MANIFEST));
10033
10918
  }
10034
10919
  }
10035
10920
  async function migrateLegacyDirectoriesToUserData() {
10036
10921
  const userDataDir = getUserDataDir();
10037
10922
  const legacyCodexDir = resolveCodexDir();
10038
10923
  await copyDirIfExists(
10039
- import_node_path15.default.join(legacyCodexDir, LEGACY_PROFILE_HOMES_DIR),
10040
- import_node_path15.default.join(userDataDir, LEGACY_PROFILE_HOMES_DIR)
10924
+ import_node_path16.default.join(legacyCodexDir, LEGACY_PROFILE_HOMES_DIR),
10925
+ import_node_path16.default.join(userDataDir, LEGACY_PROFILE_HOMES_DIR)
10041
10926
  );
10042
10927
  await copyDirIfExists(
10043
- import_node_path15.default.join(legacyCodexDir, LEGACY_SKILLS_DIR),
10044
- import_node_path15.default.join(userDataDir, LEGACY_SKILLS_DIR)
10928
+ import_node_path16.default.join(legacyCodexDir, LEGACY_SKILLS_DIR),
10929
+ import_node_path16.default.join(userDataDir, LEGACY_SKILLS_DIR)
10045
10930
  );
10046
10931
  await copyDirIfExists(
10047
- import_node_path15.default.join(legacyCodexDir, LEGACY_SKILL_CACHE_DIR),
10048
- import_node_path15.default.join(userDataDir, LEGACY_SKILL_CACHE_DIR)
10932
+ import_node_path16.default.join(legacyCodexDir, LEGACY_SKILL_CACHE_DIR),
10933
+ import_node_path16.default.join(userDataDir, LEGACY_SKILL_CACHE_DIR)
10049
10934
  );
10050
10935
  await copyFileIfExists(
10051
- import_node_path15.default.join(legacyCodexDir, LEGACY_LICENSE_SECRET_FILE2),
10052
- import_node_path15.default.join(userDataDir, LEGACY_LICENSE_SECRET_FILE2),
10936
+ import_node_path16.default.join(legacyCodexDir, LEGACY_LICENSE_SECRET_FILE2),
10937
+ import_node_path16.default.join(userDataDir, LEGACY_LICENSE_SECRET_FILE2),
10053
10938
  { mode: 384 }
10054
10939
  );
10055
- await cleanupCopiedSkillsMetadata(import_node_path15.default.join(userDataDir, LEGACY_SKILLS_DIR));
10940
+ await cleanupCopiedSkillsMetadata(import_node_path16.default.join(userDataDir, LEGACY_SKILLS_DIR));
10056
10941
  }
10057
10942
  function pickAutoRoll(raw) {
10058
10943
  if (!raw) {
@@ -10137,9 +11022,13 @@ async function loadLegacySettingsPatch() {
10137
11022
  license,
10138
11023
  autoRoll: autoRoll ? {
10139
11024
  enabled: autoRoll.enabled,
10140
- warningThreshold: autoRoll.warningThreshold,
10141
- switchThreshold: autoRoll.switchThreshold,
10142
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
11025
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
11026
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
11027
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
11028
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
11029
+ priorityOrder: autoRoll.priorityOrder,
11030
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
11031
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
10143
11032
  } : void 0
10144
11033
  };
10145
11034
  }
@@ -10159,7 +11048,7 @@ async function loadLegacySyncPatch() {
10159
11048
  };
10160
11049
  }
10161
11050
  async function loadLegacyAppSettingsParityPatch() {
10162
- const filePath = import_node_path15.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE);
11051
+ const filePath = import_node_path16.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE);
10163
11052
  const raw = await readJsonFileIfExists(filePath);
10164
11053
  if (!isRecord6(raw)) {
10165
11054
  return {};
@@ -10172,7 +11061,7 @@ async function loadLegacyProfilesPatch() {
10172
11061
  const root = resolveLegacyPath(LEGACY_PROFILE_HOMES_DIR);
10173
11062
  let entries = [];
10174
11063
  try {
10175
- entries = await import_node_fs9.promises.readdir(root, { withFileTypes: true });
11064
+ entries = await import_node_fs10.promises.readdir(root, { withFileTypes: true });
10176
11065
  } catch (error) {
10177
11066
  if (error.code === "ENOENT") {
10178
11067
  return {};
@@ -10184,7 +11073,7 @@ async function loadLegacyProfilesPatch() {
10184
11073
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10185
11074
  continue;
10186
11075
  }
10187
- const profileFile = import_node_path15.default.join(root, entry.name, "profile.json");
11076
+ const profileFile = import_node_path16.default.join(root, entry.name, "profile.json");
10188
11077
  const raw = await readJsonFileIfExists(profileFile);
10189
11078
  if (!raw) {
10190
11079
  continue;
@@ -10220,12 +11109,12 @@ function parseSkillInstallMetadata(raw) {
10220
11109
  }
10221
11110
  async function loadLegacySkillsPatch() {
10222
11111
  const skillsRoot = resolveLegacyPath(LEGACY_SKILLS_DIR);
10223
- const reposPath = import_node_path15.default.join(skillsRoot, LEGACY_SKILLS_REPOS_FILE);
11112
+ const reposPath = import_node_path16.default.join(skillsRoot, LEGACY_SKILLS_REPOS_FILE);
10224
11113
  const reposRaw = await readJsonFileIfExists(reposPath);
10225
11114
  const installsBySlug = {};
10226
11115
  let dirEntries = [];
10227
11116
  try {
10228
- dirEntries = await import_node_fs9.promises.readdir(skillsRoot, { withFileTypes: true });
11117
+ dirEntries = await import_node_fs10.promises.readdir(skillsRoot, { withFileTypes: true });
10229
11118
  } catch (error) {
10230
11119
  if (error.code !== "ENOENT") {
10231
11120
  throw error;
@@ -10238,7 +11127,7 @@ async function loadLegacySkillsPatch() {
10238
11127
  if (entry.name.startsWith(".")) {
10239
11128
  continue;
10240
11129
  }
10241
- const manifestPath = import_node_path15.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST);
11130
+ const manifestPath = import_node_path16.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST);
10242
11131
  const manifestRaw = await readJsonFileIfExists(manifestPath);
10243
11132
  const parsed = parseSkillInstallMetadata(manifestRaw);
10244
11133
  if (!parsed) {
@@ -10334,9 +11223,13 @@ function mergeLegacyLocalStoragePatch(payload) {
10334
11223
  if (autoRoll) {
10335
11224
  patch.autoRoll = {
10336
11225
  enabled: autoRoll.enabled,
10337
- warningThreshold: autoRoll.warningThreshold,
10338
- switchThreshold: autoRoll.switchThreshold,
10339
- restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll
11226
+ rearmRemainingThreshold: autoRoll.rearmRemainingThreshold,
11227
+ switchRemainingThreshold: autoRoll.switchRemainingThreshold,
11228
+ restartOfficialCodexOnAutoRoll: autoRoll.restartOfficialCodexOnAutoRoll,
11229
+ launchOfficialCodexWhenClosedOnAutoRoll: autoRoll.launchOfficialCodexWhenClosedOnAutoRoll,
11230
+ priorityOrder: autoRoll.priorityOrder,
11231
+ lowRemainingNotificationEnabled: autoRoll.lowRemainingNotificationEnabled,
11232
+ lowRemainingNotificationThreshold: autoRoll.lowRemainingNotificationThreshold
10340
11233
  };
10341
11234
  markSkippedIfPresent("codex:auto-roll-settings", true);
10342
11235
  } else {
@@ -10349,13 +11242,13 @@ function mergeLegacyLocalStoragePatch(payload) {
10349
11242
  };
10350
11243
  }
10351
11244
  async function removeIfExists(target) {
10352
- await import_node_fs9.promises.rm(target, { recursive: true, force: true });
11245
+ await import_node_fs10.promises.rm(target, { recursive: true, force: true });
10353
11246
  }
10354
11247
  async function cleanupLegacyCanonicalSources() {
10355
11248
  const homeDir = resolveHomeDir2();
10356
11249
  await removeIfExists(resolveLegacyAppStatePath());
10357
- await removeIfExists(import_node_path15.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE));
10358
- await removeIfExists(import_node_path15.default.join(homeDir, SQLITE_STORAGE_DIR));
11250
+ await removeIfExists(import_node_path16.default.join(getUserDataDir(), LEGACY_APP_SETTINGS_PARITY_FILE));
11251
+ await removeIfExists(import_node_path16.default.join(homeDir, SQLITE_STORAGE_DIR));
10359
11252
  await removeIfExists(resolveLegacyPath(LEGACY_SETTINGS_FILE));
10360
11253
  await removeIfExists(resolveLegacyPath(LEGACY_SETTINGS_BACKUP_FILE));
10361
11254
  await removeIfExists(resolveLegacyPath(LEGACY_SYNC_STATE_FILE));
@@ -10363,15 +11256,15 @@ async function cleanupLegacyCanonicalSources() {
10363
11256
  await removeIfExists(resolveLegacyPath(LEGACY_SKILLS_DIR, LEGACY_SKILLS_REPOS_FILE));
10364
11257
  const profileHomesRoot = resolveLegacyPath(LEGACY_PROFILE_HOMES_DIR);
10365
11258
  try {
10366
- const profileHomes = await import_node_fs9.promises.readdir(profileHomesRoot, { withFileTypes: true });
11259
+ const profileHomes = await import_node_fs10.promises.readdir(profileHomesRoot, { withFileTypes: true });
10367
11260
  for (const profileHome of profileHomes) {
10368
11261
  if (!profileHome.isDirectory()) {
10369
11262
  continue;
10370
11263
  }
10371
- const profileDir = import_node_path15.default.join(profileHomesRoot, profileHome.name);
11264
+ const profileDir = import_node_path16.default.join(profileHomesRoot, profileHome.name);
10372
11265
  let files = [];
10373
11266
  try {
10374
- files = await import_node_fs9.promises.readdir(profileDir);
11267
+ files = await import_node_fs10.promises.readdir(profileDir);
10375
11268
  } catch {
10376
11269
  continue;
10377
11270
  }
@@ -10379,7 +11272,7 @@ async function cleanupLegacyCanonicalSources() {
10379
11272
  if (!file.startsWith("profile.json")) {
10380
11273
  continue;
10381
11274
  }
10382
- await removeIfExists(import_node_path15.default.join(profileDir, file));
11275
+ await removeIfExists(import_node_path16.default.join(profileDir, file));
10383
11276
  }
10384
11277
  }
10385
11278
  } catch (error) {
@@ -10392,12 +11285,12 @@ async function cleanupLegacyCanonicalSources() {
10392
11285
  }
10393
11286
  const skillsRoot = resolveLegacyPath(LEGACY_SKILLS_DIR);
10394
11287
  try {
10395
- const skillDirs = await import_node_fs9.promises.readdir(skillsRoot, { withFileTypes: true });
11288
+ const skillDirs = await import_node_fs10.promises.readdir(skillsRoot, { withFileTypes: true });
10396
11289
  for (const entry of skillDirs) {
10397
11290
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
10398
11291
  continue;
10399
11292
  }
10400
- await removeIfExists(import_node_path15.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST));
11293
+ await removeIfExists(import_node_path16.default.join(skillsRoot, entry.name, LEGACY_SKILL_MANIFEST));
10401
11294
  }
10402
11295
  } catch (error) {
10403
11296
  if (!isMissingPathError(error)) {
@@ -10610,7 +11503,7 @@ async function ensureCliStorageReady() {
10610
11503
  }
10611
11504
 
10612
11505
  // src/app/main.ts
10613
- var VERSION = true ? "3.9.2" : "0.0.0";
11506
+ var VERSION = true ? "3.9.7" : "0.0.0";
10614
11507
  async function runCli() {
10615
11508
  const args = process.argv.slice(2);
10616
11509
  if (args.length === 0) {