zcf 3.4.1 → 3.4.2

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.
@@ -4,7 +4,7 @@ import process from 'node:process';
4
4
  import ansis from 'ansis';
5
5
  import inquirer from 'inquirer';
6
6
  import { exec as exec$1 } from 'node:child_process';
7
- import { homedir, platform } from 'node:os';
7
+ import { homedir, platform as platform$1 } from 'node:os';
8
8
  import { promisify } from 'node:util';
9
9
  import dayjs from 'dayjs';
10
10
  import { dirname, join } from 'pathe';
@@ -18,7 +18,7 @@ import { rm, mkdir, copyFile as copyFile$1 } from 'node:fs/promises';
18
18
  import i18next from 'i18next';
19
19
  import Backend from 'i18next-fs-backend';
20
20
 
21
- const version = "3.4.1";
21
+ const version = "3.4.2";
22
22
  const homepage = "https://github.com/UfoMiao/zcf";
23
23
 
24
24
  const i18n = i18next.createInstance();
@@ -517,7 +517,7 @@ function displayBannerWithInfo(subtitle) {
517
517
 
518
518
  const WINDOWS_WRAPPED_COMMANDS = ["npx", "uvx", "uv"];
519
519
  function getPlatform() {
520
- const p = platform();
520
+ const p = platform$1();
521
521
  if (p === "win32")
522
522
  return "windows";
523
523
  if (p === "darwin")
@@ -703,9 +703,105 @@ async function commandExists(command) {
703
703
  return true;
704
704
  }
705
705
  }
706
+ if (getPlatform() === "macos") {
707
+ const homebrewPaths = await getHomebrewCommandPaths(command);
708
+ for (const path of homebrewPaths) {
709
+ if (nodeFs.existsSync(path)) {
710
+ return true;
711
+ }
712
+ }
713
+ }
706
714
  }
707
715
  return false;
708
716
  }
717
+ async function getHomebrewCommandPaths(command) {
718
+ const paths = [];
719
+ const homebrewPrefixes = [
720
+ "/opt/homebrew",
721
+ // Apple Silicon (M1/M2)
722
+ "/usr/local"
723
+ // Intel Mac
724
+ ];
725
+ for (const prefix of homebrewPrefixes) {
726
+ paths.push(`${prefix}/bin/${command}`);
727
+ }
728
+ for (const prefix of homebrewPrefixes) {
729
+ const cellarNodePath = `${prefix}/Cellar/node`;
730
+ if (nodeFs.existsSync(cellarNodePath)) {
731
+ try {
732
+ const versions = nodeFs.readdirSync(cellarNodePath);
733
+ for (const version of versions) {
734
+ const binPath = `${cellarNodePath}/${version}/bin/${command}`;
735
+ paths.push(binPath);
736
+ }
737
+ } catch {
738
+ }
739
+ }
740
+ }
741
+ const caskNameMap = {
742
+ claude: "claude-code",
743
+ codex: "codex"
744
+ };
745
+ const caskName = caskNameMap[command];
746
+ if (caskName) {
747
+ for (const prefix of homebrewPrefixes) {
748
+ const caskroomPath = `${prefix}/Caskroom/${caskName}`;
749
+ if (nodeFs.existsSync(caskroomPath)) {
750
+ try {
751
+ const versions = nodeFs.readdirSync(caskroomPath).filter((v) => !v.startsWith("."));
752
+ for (const version of versions) {
753
+ const binPath = `${caskroomPath}/${version}/${command}`;
754
+ paths.push(binPath);
755
+ }
756
+ } catch {
757
+ }
758
+ }
759
+ }
760
+ }
761
+ return paths;
762
+ }
763
+ async function findCommandPath(command) {
764
+ try {
765
+ const cmd = getPlatform() === "windows" ? "where" : "which";
766
+ const res = await exec(cmd, [command]);
767
+ if (res.exitCode === 0 && res.stdout) {
768
+ return res.stdout.trim().split("\n")[0];
769
+ }
770
+ } catch {
771
+ }
772
+ const commonPaths = [
773
+ `/usr/local/bin/${command}`,
774
+ `/usr/bin/${command}`,
775
+ `/bin/${command}`,
776
+ `${process.env.HOME}/.local/bin/${command}`
777
+ ];
778
+ for (const path of commonPaths) {
779
+ if (nodeFs.existsSync(path)) {
780
+ return path;
781
+ }
782
+ }
783
+ if (getPlatform() === "macos") {
784
+ const homebrewPaths = await getHomebrewCommandPaths(command);
785
+ for (const path of homebrewPaths) {
786
+ if (nodeFs.existsSync(path)) {
787
+ return path;
788
+ }
789
+ }
790
+ }
791
+ if (isTermux()) {
792
+ const termuxPrefix = getTermuxPrefix();
793
+ const termuxPaths = [
794
+ `${termuxPrefix}/bin/${command}`,
795
+ `${termuxPrefix}/usr/bin/${command}`
796
+ ];
797
+ for (const path of termuxPaths) {
798
+ if (nodeFs.existsSync(path)) {
799
+ return path;
800
+ }
801
+ }
802
+ }
803
+ return null;
804
+ }
709
805
  function getRecommendedInstallMethods(codeType) {
710
806
  const platform2 = getPlatform();
711
807
  const wsl = isWSL();
@@ -731,6 +827,26 @@ function getRecommendedInstallMethods(codeType) {
731
827
  return ["npm"];
732
828
  }
733
829
 
830
+ const platform = {
831
+ __proto__: null,
832
+ commandExists: commandExists,
833
+ findCommandPath: findCommandPath,
834
+ getHomebrewCommandPaths: getHomebrewCommandPaths,
835
+ getMcpCommand: getMcpCommand,
836
+ getPlatform: getPlatform,
837
+ getRecommendedInstallMethods: getRecommendedInstallMethods,
838
+ getSystemRoot: getSystemRoot,
839
+ getTermuxPrefix: getTermuxPrefix,
840
+ getWSLDistro: getWSLDistro,
841
+ getWSLInfo: getWSLInfo,
842
+ isTermux: isTermux,
843
+ isWSL: isWSL,
844
+ isWindows: isWindows,
845
+ normalizeTomlPath: normalizeTomlPath,
846
+ shouldUseSudoForGlobalInstall: shouldUseSudoForGlobalInstall,
847
+ wrapCommandWithSudo: wrapCommandWithSudo
848
+ };
849
+
734
850
  class FileSystemError extends Error {
735
851
  constructor(message, path, cause) {
736
852
  super(message);
@@ -2014,6 +2130,327 @@ async function getLatestVersion(packageName, maxRetries = 3) {
2014
2130
  }
2015
2131
  return null;
2016
2132
  }
2133
+ async function getClaudeCodeInstallationSource() {
2134
+ if (getPlatform() !== "macos") {
2135
+ const commandPath2 = await findCommandPath("claude");
2136
+ return {
2137
+ isHomebrew: false,
2138
+ commandPath: commandPath2,
2139
+ source: commandPath2 ? "other" : "not-found"
2140
+ };
2141
+ }
2142
+ const commandPath = await findCommandPath("claude");
2143
+ if (!commandPath) {
2144
+ return { isHomebrew: false, commandPath: null, source: "not-found" };
2145
+ }
2146
+ const isFromCaskroom = commandPath.includes("/Caskroom/claude-code/");
2147
+ if (isFromCaskroom) {
2148
+ return { isHomebrew: true, commandPath, source: "homebrew-cask" };
2149
+ }
2150
+ try {
2151
+ const { stdout: realPath } = await execAsync$3(`readlink -f "${commandPath}" 2>/dev/null || realpath "${commandPath}" 2>/dev/null || echo "${commandPath}"`);
2152
+ const resolvedPath = realPath.trim();
2153
+ if (resolvedPath.includes("/Caskroom/claude-code/")) {
2154
+ return { isHomebrew: true, commandPath, source: "homebrew-cask" };
2155
+ }
2156
+ } catch {
2157
+ }
2158
+ if (commandPath.includes("/node_modules/") || commandPath.includes("/npm/") || commandPath.includes("/Cellar/node/")) {
2159
+ return { isHomebrew: false, commandPath, source: "npm" };
2160
+ }
2161
+ return { isHomebrew: false, commandPath, source: "other" };
2162
+ }
2163
+ async function detectAllClaudeCodeInstallations() {
2164
+ const installations = [];
2165
+ const checkedPaths = /* @__PURE__ */ new Set();
2166
+ const activeCommandPath = await findCommandPath("claude");
2167
+ let activeResolvedPath = null;
2168
+ if (activeCommandPath) {
2169
+ try {
2170
+ const { stdout } = await execAsync$3(`readlink -f "${activeCommandPath}" 2>/dev/null || realpath "${activeCommandPath}" 2>/dev/null || echo "${activeCommandPath}"`);
2171
+ activeResolvedPath = stdout.trim();
2172
+ } catch {
2173
+ activeResolvedPath = activeCommandPath;
2174
+ }
2175
+ }
2176
+ async function getVersionFromPath(path) {
2177
+ try {
2178
+ const { stdout } = await execAsync$3(`"${path}" -v 2>/dev/null || "${path}" --version 2>/dev/null`);
2179
+ const versionMatch = stdout.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
2180
+ return versionMatch ? versionMatch[1] : null;
2181
+ } catch {
2182
+ return null;
2183
+ }
2184
+ }
2185
+ function isActivePath(path) {
2186
+ if (!activeResolvedPath)
2187
+ return false;
2188
+ return path === activeResolvedPath || path === activeCommandPath;
2189
+ }
2190
+ async function addInstallation(path, source) {
2191
+ let resolvedPath = path;
2192
+ try {
2193
+ const { stdout } = await execAsync$3(`readlink -f "${path}" 2>/dev/null || realpath "${path}" 2>/dev/null || echo "${path}"`);
2194
+ resolvedPath = stdout.trim();
2195
+ } catch {
2196
+ }
2197
+ if (checkedPaths.has(resolvedPath))
2198
+ return;
2199
+ checkedPaths.add(resolvedPath);
2200
+ if (!nodeFs.existsSync(path))
2201
+ return;
2202
+ const version = await getVersionFromPath(path);
2203
+ installations.push({
2204
+ source,
2205
+ path,
2206
+ version,
2207
+ isActive: isActivePath(path) || isActivePath(resolvedPath)
2208
+ });
2209
+ }
2210
+ if (activeCommandPath && nodeFs.existsSync(activeCommandPath)) {
2211
+ let activeSource = "other";
2212
+ if (activeResolvedPath?.includes("/Caskroom/claude-code/")) {
2213
+ activeSource = "homebrew-cask";
2214
+ } else if (activeResolvedPath?.includes("/node_modules/") || activeResolvedPath?.includes("/npm/") || activeResolvedPath?.includes("/fnm_multishells/") || activeResolvedPath?.includes("/.nvm/") || activeResolvedPath?.includes("/Cellar/node/") || activeCommandPath.includes("/fnm_multishells/") || activeCommandPath.includes("/.nvm/")) {
2215
+ activeSource = "npm";
2216
+ }
2217
+ await addInstallation(activeCommandPath, activeSource);
2218
+ }
2219
+ if (getPlatform() === "macos") {
2220
+ const homebrewPaths = await getHomebrewCommandPaths("claude");
2221
+ for (const path of homebrewPaths) {
2222
+ if (path.includes("/Caskroom/claude-code/")) {
2223
+ await addInstallation(path, "homebrew-cask");
2224
+ } else if (path.includes("/Cellar/node/")) {
2225
+ await addInstallation(path, "npm-homebrew-node");
2226
+ }
2227
+ }
2228
+ try {
2229
+ await execAsync$3("brew list --cask claude-code");
2230
+ const homebrewPrefixes = ["/opt/homebrew", "/usr/local"];
2231
+ for (const prefix of homebrewPrefixes) {
2232
+ const caskroomPath = `${prefix}/Caskroom/claude-code`;
2233
+ if (nodeFs.existsSync(caskroomPath)) {
2234
+ const versions = nodeFs.readdirSync(caskroomPath).filter((v) => !v.startsWith("."));
2235
+ for (const version of versions) {
2236
+ const claudePath = `${caskroomPath}/${version}/claude`;
2237
+ await addInstallation(claudePath, "homebrew-cask");
2238
+ }
2239
+ }
2240
+ }
2241
+ } catch {
2242
+ }
2243
+ }
2244
+ const npmGlobalPaths = [
2245
+ "/usr/local/bin/claude",
2246
+ "/usr/bin/claude",
2247
+ `${process.env.HOME}/.npm-global/bin/claude`,
2248
+ `${process.env.HOME}/.local/bin/claude`
2249
+ ];
2250
+ for (const path of npmGlobalPaths) {
2251
+ if (nodeFs.existsSync(path)) {
2252
+ let resolvedPath = path;
2253
+ try {
2254
+ const { stdout } = await execAsync$3(`readlink -f "${path}" 2>/dev/null || realpath "${path}" 2>/dev/null || echo "${path}"`);
2255
+ resolvedPath = stdout.trim();
2256
+ } catch {
2257
+ }
2258
+ if (resolvedPath.includes("/node_modules/") || resolvedPath.includes("/npm/")) {
2259
+ await addInstallation(path, "npm");
2260
+ } else if (resolvedPath.includes("/Caskroom/")) ; else {
2261
+ await addInstallation(path, "other");
2262
+ }
2263
+ }
2264
+ }
2265
+ if (getPlatform() === "macos") {
2266
+ const homebrewPrefixes = ["/opt/homebrew", "/usr/local"];
2267
+ for (const prefix of homebrewPrefixes) {
2268
+ const cellarNodePath = `${prefix}/Cellar/node`;
2269
+ if (nodeFs.existsSync(cellarNodePath)) {
2270
+ try {
2271
+ const versions = nodeFs.readdirSync(cellarNodePath);
2272
+ for (const version of versions) {
2273
+ const claudePath = `${cellarNodePath}/${version}/bin/claude`;
2274
+ await addInstallation(claudePath, "npm-homebrew-node");
2275
+ }
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ }
2280
+ }
2281
+ return installations;
2282
+ }
2283
+ async function checkDuplicateInstallations() {
2284
+ const installations = await detectAllClaudeCodeInstallations();
2285
+ const activeInstallation = installations.find((i) => i.isActive) || null;
2286
+ const inactiveInstallations = installations.filter((i) => !i.isActive);
2287
+ const homebrewInstallation = installations.find((i) => i.source === "homebrew-cask") || null;
2288
+ const npmInstallation = installations.find((i) => i.source === "npm" || i.source === "npm-homebrew-node") || null;
2289
+ const hasDuplicates = homebrewInstallation !== null && npmInstallation !== null;
2290
+ const recommendation = hasDuplicates ? "remove-npm" : "none";
2291
+ return {
2292
+ hasDuplicates,
2293
+ installations,
2294
+ activeInstallation,
2295
+ inactiveInstallations,
2296
+ homebrewInstallation,
2297
+ npmInstallation,
2298
+ recommendation
2299
+ };
2300
+ }
2301
+ function getSourceDisplayName(source, i18n) {
2302
+ const sourceMap = {
2303
+ "homebrew-cask": i18n.t("installation:sourceHomebrewCask"),
2304
+ "npm": i18n.t("installation:sourceNpm"),
2305
+ "npm-homebrew-node": i18n.t("installation:sourceNpmHomebrewNode"),
2306
+ "curl": i18n.t("installation:sourceCurl"),
2307
+ "other": i18n.t("installation:sourceOther")
2308
+ };
2309
+ return sourceMap[source] || source;
2310
+ }
2311
+ async function performNpmRemovalAndActivateHomebrew(_npmInstallation, homebrewInstallation, tinyExec, i18n, ansis) {
2312
+ const ora = (await import('ora')).default;
2313
+ const spinner = ora(i18n.t("installation:removingDuplicateInstallation")).start();
2314
+ try {
2315
+ const { wrapCommandWithSudo } = await Promise.resolve().then(function () { return platform; });
2316
+ const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["uninstall", "-g", "@anthropic-ai/claude-code"]);
2317
+ if (usedSudo) {
2318
+ spinner.info(i18n.t("installation:usingSudo"));
2319
+ spinner.start();
2320
+ }
2321
+ await tinyExec(command, args);
2322
+ spinner.succeed(i18n.t("installation:duplicateRemoved"));
2323
+ if (homebrewInstallation && !homebrewInstallation.isActive) {
2324
+ console.log("");
2325
+ console.log(ansis.cyan(`\u{1F517} ${i18n.t("installation:activatingHomebrew")}`));
2326
+ const { createHomebrewSymlink } = await Promise.resolve().then(function () { return installer; });
2327
+ const symlinkResult = await createHomebrewSymlink("claude", homebrewInstallation.path);
2328
+ if (symlinkResult.success) {
2329
+ console.log(ansis.green(`\u2714 ${i18n.t("installation:symlinkCreated", { path: symlinkResult.symlinkPath || "/usr/local/bin/claude" })}`));
2330
+ } else {
2331
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("installation:manualSymlinkHint")}`));
2332
+ if (symlinkResult.error) {
2333
+ console.log(ansis.gray(` ${symlinkResult.error}`));
2334
+ } else {
2335
+ const homebrewBin = nodeFs.existsSync("/opt/homebrew/bin") ? "/opt/homebrew/bin" : "/usr/local/bin";
2336
+ console.log(ansis.gray(` sudo ln -sf "${homebrewInstallation.path}" ${homebrewBin}/claude`));
2337
+ }
2338
+ }
2339
+ }
2340
+ return { hadDuplicates: true, resolved: true, action: "removed-npm" };
2341
+ } catch (error) {
2342
+ spinner.fail(i18n.t("installation:duplicateRemovalFailed"));
2343
+ if (error instanceof Error) {
2344
+ console.error(ansis.gray(error.message));
2345
+ }
2346
+ return { hadDuplicates: true, resolved: false, action: "kept-both" };
2347
+ }
2348
+ }
2349
+ async function handleDuplicateInstallations(skipPrompt = false) {
2350
+ const { ensureI18nInitialized, format, i18n } = await Promise.resolve().then(function () { return index; });
2351
+ const ansis = (await import('ansis')).default;
2352
+ ensureI18nInitialized();
2353
+ const duplicateInfo = await checkDuplicateInstallations();
2354
+ if (!duplicateInfo.hasDuplicates) {
2355
+ return { hadDuplicates: false, resolved: true, action: "no-duplicates" };
2356
+ }
2357
+ const { npmInstallation, homebrewInstallation } = duplicateInfo;
2358
+ console.log("");
2359
+ console.log(ansis.yellow.bold(i18n.t("installation:duplicateInstallationsDetected")));
2360
+ console.log(ansis.gray(i18n.t("installation:duplicateInstallationsWarning")));
2361
+ console.log("");
2362
+ if (homebrewInstallation) {
2363
+ const isActive = homebrewInstallation.isActive;
2364
+ const statusIcon = isActive ? "\u2705" : "\u26A0\uFE0F";
2365
+ const statusColor = isActive ? ansis.green : ansis.yellow;
2366
+ console.log(ansis.cyan.bold(`\u{1F37A} Homebrew Cask ${i18n.t("installation:recommendedMethod")}:`));
2367
+ console.log(ansis.white(` ${i18n.t("installation:installationSource")}: ${statusColor(getSourceDisplayName(homebrewInstallation.source, i18n))}`));
2368
+ console.log(ansis.white(` ${i18n.t("installation:installationPath")}: ${ansis.gray(homebrewInstallation.path)}`));
2369
+ if (homebrewInstallation.version) {
2370
+ console.log(ansis.white(` ${i18n.t("installation:installationVersion")}: ${ansis.cyan(homebrewInstallation.version)}`));
2371
+ }
2372
+ console.log(ansis.white(` ${statusIcon} ${isActive ? i18n.t("installation:currentActiveInstallation") : i18n.t("installation:inactiveInstallations")}`));
2373
+ console.log("");
2374
+ }
2375
+ if (npmInstallation) {
2376
+ const isActive = npmInstallation.isActive;
2377
+ console.log(ansis.yellow.bold(`\u{1F4E6} npm ${i18n.t("installation:notRecommended")}:`));
2378
+ console.log(ansis.white(` ${i18n.t("installation:installationSource")}: ${ansis.yellow(getSourceDisplayName(npmInstallation.source, i18n))}`));
2379
+ console.log(ansis.white(` ${i18n.t("installation:installationPath")}: ${ansis.gray(npmInstallation.path)}`));
2380
+ if (npmInstallation.version) {
2381
+ console.log(ansis.white(` ${i18n.t("installation:installationVersion")}: ${ansis.cyan(npmInstallation.version)}`));
2382
+ if (homebrewInstallation?.version && npmInstallation.version !== homebrewInstallation.version) {
2383
+ console.log(ansis.red(` ${format(i18n.t("installation:versionMismatchWarning"), {
2384
+ npmVersion: npmInstallation.version,
2385
+ homebrewVersion: homebrewInstallation.version
2386
+ })}`));
2387
+ }
2388
+ }
2389
+ if (isActive) {
2390
+ console.log(ansis.white(` \u26A0\uFE0F ${i18n.t("installation:currentActiveInstallation")}`));
2391
+ }
2392
+ console.log("");
2393
+ }
2394
+ console.log(ansis.cyan(`\u{1F4A1} ${i18n.t("installation:recommendRemoveNpm")}`));
2395
+ console.log("");
2396
+ if (!npmInstallation) {
2397
+ return { hadDuplicates: true, resolved: false, action: "kept-both" };
2398
+ }
2399
+ const { exec: tinyExec } = await import('tinyexec');
2400
+ if (skipPrompt) {
2401
+ console.log(ansis.cyan(`\u{1F504} ${i18n.t("installation:autoRemovingNpm")}`));
2402
+ return await performNpmRemovalAndActivateHomebrew(
2403
+ npmInstallation,
2404
+ homebrewInstallation,
2405
+ tinyExec,
2406
+ i18n,
2407
+ ansis
2408
+ );
2409
+ }
2410
+ const inquirer = (await import('inquirer')).default;
2411
+ const sourceDisplayName = getSourceDisplayName(npmInstallation.source, i18n);
2412
+ const confirmMessage = format(i18n.t("installation:confirmRemoveDuplicate"), { source: sourceDisplayName });
2413
+ const { action } = await inquirer.prompt([
2414
+ {
2415
+ type: "list",
2416
+ name: "action",
2417
+ message: confirmMessage,
2418
+ choices: [
2419
+ {
2420
+ name: `\u2705 ${i18n.t("common:yes")} - ${i18n.t("installation:removingDuplicateInstallation")}`,
2421
+ value: "remove"
2422
+ },
2423
+ {
2424
+ name: `\u274C ${i18n.t("installation:keepBothInstallations")}`,
2425
+ value: "keep"
2426
+ }
2427
+ ]
2428
+ }
2429
+ ]);
2430
+ if (action === "keep") {
2431
+ console.log(ansis.gray(i18n.t("installation:duplicateWarningContinue")));
2432
+ return { hadDuplicates: true, resolved: false, action: "kept-both" };
2433
+ }
2434
+ return await performNpmRemovalAndActivateHomebrew(
2435
+ npmInstallation,
2436
+ homebrewInstallation,
2437
+ tinyExec,
2438
+ i18n,
2439
+ ansis
2440
+ );
2441
+ }
2442
+ async function getHomebrewClaudeCodeVersion() {
2443
+ try {
2444
+ const { stdout } = await execAsync$3("brew info --cask claude-code --json=v2");
2445
+ const info = JSON.parse(stdout);
2446
+ if (info.casks && info.casks.length > 0) {
2447
+ return info.casks[0].version;
2448
+ }
2449
+ return null;
2450
+ } catch {
2451
+ return null;
2452
+ }
2453
+ }
2017
2454
  function compareVersions(current, latest) {
2018
2455
  if (!semver.valid(current) || !semver.valid(latest)) {
2019
2456
  return -1;
@@ -2035,12 +2472,22 @@ async function checkCcrVersion() {
2035
2472
  }
2036
2473
  async function checkClaudeCodeVersion() {
2037
2474
  const currentVersion = await getInstalledVersion("claude");
2038
- const latestVersion = await getLatestVersion("@anthropic-ai/claude-code");
2475
+ const installationInfo = await getClaudeCodeInstallationSource();
2476
+ const { isHomebrew, commandPath, source: installationSource } = installationInfo;
2477
+ let latestVersion;
2478
+ if (isHomebrew) {
2479
+ latestVersion = await getHomebrewClaudeCodeVersion();
2480
+ } else {
2481
+ latestVersion = await getLatestVersion("@anthropic-ai/claude-code");
2482
+ }
2039
2483
  return {
2040
2484
  installed: currentVersion !== null,
2041
2485
  currentVersion,
2042
2486
  latestVersion,
2043
- needsUpdate: currentVersion && latestVersion ? shouldUpdate(currentVersion, latestVersion) : false
2487
+ needsUpdate: currentVersion && latestVersion ? shouldUpdate(currentVersion, latestVersion) : false,
2488
+ isHomebrew,
2489
+ commandPath,
2490
+ installationSource
2044
2491
  };
2045
2492
  }
2046
2493
  async function checkCometixLineVersion() {
@@ -2120,7 +2567,7 @@ async function updateClaudeCode(force = false, skipPrompt = false) {
2120
2567
  ensureI18nInitialized();
2121
2568
  const spinner = ora(i18n.t("updater:checkingVersion")).start();
2122
2569
  try {
2123
- const { installed, currentVersion, latestVersion, needsUpdate } = await checkClaudeCodeVersion();
2570
+ const { installed, currentVersion, latestVersion, needsUpdate, isHomebrew } = await checkClaudeCodeVersion();
2124
2571
  spinner.stop();
2125
2572
  if (!installed) {
2126
2573
  console.log(ansis.yellow(i18n.t("updater:claudeCodeNotInstalled")));
@@ -2148,9 +2595,14 @@ async function updateClaudeCode(force = false, skipPrompt = false) {
2148
2595
  } else {
2149
2596
  console.log(ansis.cyan(format(i18n.t("updater:autoUpdating"), { tool: "Claude Code" })));
2150
2597
  }
2151
- const updateSpinner = ora(format(i18n.t("updater:updating"), { tool: "Claude Code" })).start();
2598
+ const toolName = isHomebrew ? "Claude Code (Homebrew)" : "Claude Code";
2599
+ const updateSpinner = ora(format(i18n.t("updater:updating"), { tool: toolName })).start();
2152
2600
  try {
2153
- await execAsync$2("claude update");
2601
+ if (isHomebrew) {
2602
+ await execAsync$2("brew upgrade --cask claude-code");
2603
+ } else {
2604
+ await execAsync$2("claude update");
2605
+ }
2154
2606
  updateSpinner.succeed(format(i18n.t("updater:updateSuccess"), { tool: "Claude Code" }));
2155
2607
  return true;
2156
2608
  } catch (error) {
@@ -2217,6 +2669,15 @@ async function checkAndUpdateTools(skipPrompt = false) {
2217
2669
  console.log(ansis.bold.cyan(`
2218
2670
  \u{1F50D} ${i18n.t("updater:checkingTools")}
2219
2671
  `));
2672
+ try {
2673
+ const duplicateResult = await handleDuplicateInstallations(skipPrompt);
2674
+ if (duplicateResult.hadDuplicates) {
2675
+ console.log();
2676
+ }
2677
+ } catch (error) {
2678
+ const errorMessage = error instanceof Error ? error.message : String(error);
2679
+ console.warn(ansis.yellow(`\u26A0 Duplicate installation check failed: ${errorMessage}`));
2680
+ }
2220
2681
  const results = [];
2221
2682
  try {
2222
2683
  const success = await updateCcr(false, skipPrompt);
@@ -2962,7 +3423,7 @@ async function configureCodexMcp(options) {
2962
3423
  command: serviceConfig.command,
2963
3424
  args: serviceConfig.args,
2964
3425
  env: Object.keys(env).length > 0 ? env : void 0,
2965
- startup_timeout_ms: configInfo.config.startup_timeout_ms
3426
+ startup_timeout_sec: 30
2966
3427
  });
2967
3428
  }
2968
3429
  const mergedMap = /* @__PURE__ */ new Map();
@@ -3137,9 +3598,91 @@ function backupCodexPrompts() {
3137
3598
  function getBackupMessage(path) {
3138
3599
  if (!path)
3139
3600
  return "";
3140
- ensureI18nInitialized();
3601
+ if (!i18n.isInitialized) {
3602
+ return `Backup created: ${path}`;
3603
+ }
3141
3604
  return i18n.t("codex:backupSuccess", { path });
3142
3605
  }
3606
+ function needsEnvKeyMigration() {
3607
+ if (!exists(CODEX_CONFIG_FILE))
3608
+ return false;
3609
+ try {
3610
+ const content = readFile(CODEX_CONFIG_FILE);
3611
+ const hasOldEnvKey = /^\s*env_key\s*=/m.test(content);
3612
+ return hasOldEnvKey;
3613
+ } catch {
3614
+ return false;
3615
+ }
3616
+ }
3617
+ function migrateEnvKeyToTempEnvKey() {
3618
+ if (!exists(CODEX_CONFIG_FILE))
3619
+ return false;
3620
+ try {
3621
+ const content = readFile(CODEX_CONFIG_FILE);
3622
+ if (!needsEnvKeyMigration())
3623
+ return false;
3624
+ const backupPath = backupCodexConfig();
3625
+ if (backupPath) {
3626
+ console.log(ansis.gray(getBackupMessage(backupPath)));
3627
+ }
3628
+ const migratedContent = migrateEnvKeyInContent(content);
3629
+ writeFile(CODEX_CONFIG_FILE, migratedContent);
3630
+ updateTomlConfig(ZCF_CONFIG_FILE, {
3631
+ codex: {
3632
+ envKeyMigrated: true
3633
+ }
3634
+ });
3635
+ const message = i18n.isInitialized ? i18n.t("codex:envKeyMigrationComplete") : "\u2714 env_key to temp_env_key migration completed";
3636
+ console.log(ansis.green(message));
3637
+ return true;
3638
+ } catch (error) {
3639
+ console.error(ansis.yellow(`env_key migration warning: ${error.message}`));
3640
+ return false;
3641
+ }
3642
+ }
3643
+ function migrateEnvKeyInContent(content) {
3644
+ const lines = content.split("\n");
3645
+ const result = [];
3646
+ let currentSectionHasTempEnvKey = false;
3647
+ let currentSection = "";
3648
+ const sectionHasTempEnvKey = /* @__PURE__ */ new Map();
3649
+ let tempSection = "";
3650
+ for (const line of lines) {
3651
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]/);
3652
+ if (sectionMatch) {
3653
+ tempSection = sectionMatch[1];
3654
+ }
3655
+ if (tempSection && /^\s*temp_env_key\s*=/.test(line)) {
3656
+ sectionHasTempEnvKey.set(tempSection, true);
3657
+ }
3658
+ }
3659
+ for (const line of lines) {
3660
+ const sectionMatch = line.match(/^\s*\[([^\]]+)\]/);
3661
+ if (sectionMatch) {
3662
+ currentSection = sectionMatch[1];
3663
+ currentSectionHasTempEnvKey = sectionHasTempEnvKey.get(currentSection) || false;
3664
+ }
3665
+ const envKeyMatch = line.match(/^(\s*)env_key(\s*=.*)$/);
3666
+ if (envKeyMatch) {
3667
+ if (currentSectionHasTempEnvKey) {
3668
+ continue;
3669
+ } else {
3670
+ result.push(`${envKeyMatch[1]}temp_env_key${envKeyMatch[2]}`);
3671
+ continue;
3672
+ }
3673
+ }
3674
+ result.push(line);
3675
+ }
3676
+ return result.join("\n");
3677
+ }
3678
+ function ensureEnvKeyMigration() {
3679
+ const tomlConfig = readDefaultTomlConfig();
3680
+ if (tomlConfig?.codex?.envKeyMigrated)
3681
+ return;
3682
+ if (needsEnvKeyMigration()) {
3683
+ migrateEnvKeyToTempEnvKey();
3684
+ }
3685
+ }
3143
3686
  function sanitizeProviderName(input) {
3144
3687
  const cleaned = input.trim();
3145
3688
  if (!cleaned)
@@ -3172,7 +3715,7 @@ function parseCodexConfig(content) {
3172
3715
  name: provider.name || id,
3173
3716
  baseUrl: provider.base_url || "",
3174
3717
  wireApi: provider.wire_api || "responses",
3175
- envKey: provider.env_key || "OPENAI_API_KEY",
3718
+ tempEnvKey: provider.temp_env_key || "OPENAI_API_KEY",
3176
3719
  requiresOpenaiAuth: provider.requires_openai_auth !== false,
3177
3720
  model: provider.model || void 0
3178
3721
  // Parse model field from provider
@@ -3181,7 +3724,7 @@ function parseCodexConfig(content) {
3181
3724
  }
3182
3725
  const mcpServices = [];
3183
3726
  if (tomlData.mcp_servers) {
3184
- const KNOWN_MCP_FIELDS = /* @__PURE__ */ new Set(["command", "args", "env", "startup_timeout_ms"]);
3727
+ const KNOWN_MCP_FIELDS = /* @__PURE__ */ new Set(["command", "args", "env", "startup_timeout_sec"]);
3185
3728
  for (const [id, mcpData] of Object.entries(tomlData.mcp_servers)) {
3186
3729
  const mcp = mcpData;
3187
3730
  const extraFields = {};
@@ -3195,7 +3738,7 @@ function parseCodexConfig(content) {
3195
3738
  command: mcp.command || id,
3196
3739
  args: mcp.args || [],
3197
3740
  env: Object.keys(mcp.env || {}).length > 0 ? mcp.env : void 0,
3198
- startup_timeout_ms: mcp.startup_timeout_ms,
3741
+ startup_timeout_sec: mcp.startup_timeout_sec,
3199
3742
  // Only add extraFields if there are any extra fields
3200
3743
  extraFields: Object.keys(extraFields).length > 0 ? extraFields : void 0
3201
3744
  });
@@ -3363,6 +3906,7 @@ function formatTomlField(key, value) {
3363
3906
  function readCodexConfig() {
3364
3907
  if (!exists(CODEX_CONFIG_FILE))
3365
3908
  return null;
3909
+ ensureEnvKeyMigration();
3366
3910
  try {
3367
3911
  const content = readFile(CODEX_CONFIG_FILE);
3368
3912
  return parseCodexConfig(content);
@@ -3416,7 +3960,7 @@ function renderCodexConfig(data) {
3416
3960
  lines.push(`name = "${provider.name}"`);
3417
3961
  lines.push(`base_url = "${provider.baseUrl}"`);
3418
3962
  lines.push(`wire_api = "${provider.wireApi}"`);
3419
- lines.push(`env_key = "${provider.envKey}"`);
3963
+ lines.push(`temp_env_key = "${provider.tempEnvKey}"`);
3420
3964
  lines.push(`requires_openai_auth = ${provider.requiresOpenaiAuth}`);
3421
3965
  if (provider.model) {
3422
3966
  lines.push(`model = "${provider.model}"`);
@@ -3436,8 +3980,8 @@ function renderCodexConfig(data) {
3436
3980
  const envEntries = Object.entries(service.env).map(([key, value]) => `${key} = '${value}'`).join(", ");
3437
3981
  lines.push(`env = {${envEntries}}`);
3438
3982
  }
3439
- if (service.startup_timeout_ms) {
3440
- lines.push(`startup_timeout_ms = ${service.startup_timeout_ms}`);
3983
+ if (service.startup_timeout_sec) {
3984
+ lines.push(`startup_timeout_sec = ${service.startup_timeout_sec}`);
3441
3985
  }
3442
3986
  if (service.extraFields) {
3443
3987
  for (const [key, value] of Object.entries(service.extraFields)) {
@@ -3460,6 +4004,7 @@ function renderCodexConfig(data) {
3460
4004
  return result;
3461
4005
  }
3462
4006
  function writeCodexConfig(data) {
4007
+ ensureEnvKeyMigration();
3463
4008
  ensureDir(CODEX_DIR);
3464
4009
  writeFile(CODEX_CONFIG_FILE, renderCodexConfig(data));
3465
4010
  }
@@ -3595,8 +4140,8 @@ async function runCodexSystemPromptSelection(skipPrompt = false) {
3595
4140
  const rootDir = getRootDir$1();
3596
4141
  const templateRoot = join(rootDir, "templates", "codex");
3597
4142
  const zcfConfig$1 = readZcfConfig();
3598
- const { readDefaultTomlConfig } = await Promise.resolve().then(function () { return zcfConfig; });
3599
- const tomlConfig = readDefaultTomlConfig();
4143
+ const { readDefaultTomlConfig: readDefaultTomlConfig2 } = await Promise.resolve().then(function () { return zcfConfig; });
4144
+ const tomlConfig = readDefaultTomlConfig2();
3600
4145
  const { resolveTemplateLanguage } = await Promise.resolve().then(function () { return prompts; });
3601
4146
  const preferredLang = await resolveTemplateLanguage(
3602
4147
  void 0,
@@ -3656,9 +4201,9 @@ async function runCodexSystemPromptSelection(skipPrompt = false) {
3656
4201
  }
3657
4202
  writeFile(CODEX_AGENTS_FILE, content);
3658
4203
  try {
3659
- const { updateTomlConfig } = await Promise.resolve().then(function () { return zcfConfig; });
3660
- const { ZCF_CONFIG_FILE } = await Promise.resolve().then(function () { return constants; });
3661
- updateTomlConfig(ZCF_CONFIG_FILE, {
4204
+ const { updateTomlConfig: updateTomlConfig2 } = await Promise.resolve().then(function () { return zcfConfig; });
4205
+ const { ZCF_CONFIG_FILE: ZCF_CONFIG_FILE2 } = await Promise.resolve().then(function () { return constants; });
4206
+ updateTomlConfig2(ZCF_CONFIG_FILE2, {
3662
4207
  codex: {
3663
4208
  systemPromptStyle: systemPrompt
3664
4209
  }
@@ -3817,7 +4362,7 @@ async function applyCustomApiConfig(customApiConfig) {
3817
4362
  name: providerName,
3818
4363
  baseUrl: baseUrl || "https://api.anthropic.com",
3819
4364
  wireApi: "claude",
3820
- envKey: `${providerId.toUpperCase()}_API_KEY`,
4365
+ tempEnvKey: `${providerId.toUpperCase()}_API_KEY`,
3821
4366
  requiresOpenaiAuth: false
3822
4367
  });
3823
4368
  if (token) {
@@ -4008,7 +4553,7 @@ async function configureCodexApi(options) {
4008
4553
  }
4009
4554
  }
4010
4555
  const providerId = sanitizeProviderName(answers.providerName);
4011
- const envKey = `${providerId.toUpperCase().replace(/-/g, "_")}_API_KEY`;
4556
+ const tempEnvKey = `${providerId.toUpperCase().replace(/-/g, "_")}_API_KEY`;
4012
4557
  const existingProvider = existingMap.get(providerId);
4013
4558
  const sessionProvider = currentSessionProviders.get(providerId);
4014
4559
  if (existingProvider || sessionProvider) {
@@ -4038,14 +4583,14 @@ async function configureCodexApi(options) {
4038
4583
  name: answers.providerName,
4039
4584
  baseUrl: selectedProvider2 === "custom" ? answers.baseUrl : prefilledBaseUrl,
4040
4585
  wireApi: selectedProvider2 === "custom" ? answers.wireApi || "responses" : prefilledWireApi,
4041
- envKey,
4586
+ tempEnvKey,
4042
4587
  requiresOpenaiAuth: true,
4043
4588
  model: customModel || prefilledModel || "gpt-5-codex"
4044
4589
  // Use custom model, provider's default model, or fallback
4045
4590
  };
4046
4591
  providers.push(newProvider);
4047
4592
  currentSessionProviders.set(providerId, newProvider);
4048
- authEntries[envKey] = answers.apiKey;
4593
+ authEntries[tempEnvKey] = answers.apiKey;
4049
4594
  const addAnother = await promptBoolean({
4050
4595
  message: i18n.t("codex:addProviderPrompt"),
4051
4596
  defaultValue: false
@@ -4065,8 +4610,8 @@ async function configureCodexApi(options) {
4065
4610
  }]);
4066
4611
  const selectedProvider = providers.find((provider) => provider.id === defaultProvider);
4067
4612
  if (selectedProvider) {
4068
- const envKey = selectedProvider.envKey;
4069
- const defaultApiKey = authEntries[envKey] ?? existingAuth[envKey] ?? null;
4613
+ const tempEnvKey = selectedProvider.tempEnvKey;
4614
+ const defaultApiKey = authEntries[tempEnvKey] ?? existingAuth[tempEnvKey] ?? null;
4070
4615
  if (defaultApiKey)
4071
4616
  authEntries.OPENAI_API_KEY = defaultApiKey;
4072
4617
  }
@@ -4351,7 +4896,7 @@ async function switchToProvider(providerId) {
4351
4896
  };
4352
4897
  writeCodexConfig(updatedConfig);
4353
4898
  const auth = readJsonConfig(CODEX_AUTH_FILE, { defaultValue: {} }) || {};
4354
- const envValue = auth[provider.envKey] || null;
4899
+ const envValue = auth[provider.tempEnvKey] || null;
4355
4900
  auth.OPENAI_API_KEY = envValue;
4356
4901
  writeJsonConfig(CODEX_AUTH_FILE, auth, { pretty: true });
4357
4902
  console.log(ansis.green(i18n.t("codex:providerSwitchSuccess", { provider: providerId })));
@@ -4375,11 +4920,15 @@ const codex = {
4375
4920
  configureCodexApi: configureCodexApi,
4376
4921
  configureCodexMcp: configureCodexMcp,
4377
4922
  createBackupDirectory: createBackupDirectory,
4923
+ ensureEnvKeyMigration: ensureEnvKeyMigration,
4378
4924
  getBackupMessage: getBackupMessage,
4379
4925
  getCodexVersion: getCodexVersion,
4380
4926
  installCodexCli: installCodexCli,
4381
4927
  isCodexInstalled: isCodexInstalled$1,
4382
4928
  listCodexProviders: listCodexProviders,
4929
+ migrateEnvKeyInContent: migrateEnvKeyInContent,
4930
+ migrateEnvKeyToTempEnvKey: migrateEnvKeyToTempEnvKey,
4931
+ needsEnvKeyMigration: needsEnvKeyMigration,
4383
4932
  parseCodexConfig: parseCodexConfig,
4384
4933
  readCodexConfig: readCodexConfig,
4385
4934
  renderCodexConfig: renderCodexConfig,
@@ -4989,6 +5538,10 @@ async function installClaudeCode(skipMethodSelection = false) {
4989
5538
  if (version) {
4990
5539
  console.log(ansis.gray(` ${i18n.t("installation:detectedVersion", { version })}`));
4991
5540
  }
5541
+ const verification = await verifyInstallation(codeType);
5542
+ if (verification.symlinkCreated) {
5543
+ displayVerificationResult(verification, codeType);
5544
+ }
4992
5545
  await updateClaudeCode();
4993
5546
  return;
4994
5547
  }
@@ -5011,13 +5564,15 @@ async function installClaudeCode(skipMethodSelection = false) {
5011
5564
  if (skipMethodSelection) {
5012
5565
  console.log(i18n.t("installation:installing"));
5013
5566
  try {
5014
- const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", "@anthropic-ai/claude-code"]);
5567
+ const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", "@anthropic-ai/claude-code", "--force"]);
5015
5568
  if (usedSudo) {
5016
5569
  console.log(ansis.yellow(`\u2139 ${i18n.t("installation:usingSudo")}`));
5017
5570
  }
5018
5571
  await exec(command, args);
5019
5572
  console.log(`\u2714 ${i18n.t("installation:installSuccess")}`);
5020
5573
  await setInstallMethod("npm");
5574
+ const verification = await verifyInstallation(codeType);
5575
+ displayVerificationResult(verification, codeType);
5021
5576
  if (isTermux()) {
5022
5577
  console.log(ansis.gray(`
5023
5578
  Claude Code installed to: ${getTermuxPrefix()}/bin/claude`));
@@ -5078,12 +5633,14 @@ async function installCodex(skipMethodSelection = false) {
5078
5633
  if (skipMethodSelection) {
5079
5634
  console.log(i18n.t("installation:installingWith", { method: "npm", codeType: codeTypeName }));
5080
5635
  try {
5081
- const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", "@openai/codex"]);
5636
+ const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", "@openai/codex", "--force"]);
5082
5637
  if (usedSudo) {
5083
5638
  console.log(ansis.yellow(`\u2139 ${i18n.t("installation:usingSudo")}`));
5084
5639
  }
5085
5640
  await exec(command, args);
5086
5641
  console.log(ansis.green(`\u2714 ${codeTypeName} ${i18n.t("installation:installSuccess")}`));
5642
+ const verification = await verifyInstallation(codeType);
5643
+ displayVerificationResult(verification, codeType);
5087
5644
  } catch (error) {
5088
5645
  console.error(ansis.red(`\u2716 ${codeTypeName} ${i18n.t("installation:installFailed")}`));
5089
5646
  throw error;
@@ -5161,7 +5718,7 @@ async function uninstallCodeTool(codeType) {
5161
5718
  }
5162
5719
  } else if (codeType === "codex") {
5163
5720
  try {
5164
- const result = await exec("brew", ["list", "codex"]);
5721
+ const result = await exec("brew", ["list", "--cask", "codex"]);
5165
5722
  if (result.exitCode === 0) {
5166
5723
  method = "homebrew";
5167
5724
  }
@@ -5176,7 +5733,7 @@ async function uninstallCodeTool(codeType) {
5176
5733
  const platform = getPlatform();
5177
5734
  if (platform === "macos" || platform === "linux") {
5178
5735
  try {
5179
- const testResult = codeType === "claude-code" ? await exec("brew", ["list", "--cask", "claude-code"]) : await exec("brew", ["list", "codex"]);
5736
+ const testResult = codeType === "claude-code" ? await exec("brew", ["list", "--cask", "claude-code"]) : await exec("brew", ["list", "--cask", "codex"]);
5180
5737
  if (testResult.exitCode === 0) {
5181
5738
  method = "homebrew";
5182
5739
  }
@@ -5205,7 +5762,7 @@ async function uninstallCodeTool(codeType) {
5205
5762
  if (codeType === "claude-code") {
5206
5763
  await exec("brew", ["uninstall", "--cask", "claude-code"]);
5207
5764
  } else {
5208
- await exec("brew", ["uninstall", "codex"]);
5765
+ await exec("brew", ["uninstall", "--cask", "codex"]);
5209
5766
  }
5210
5767
  break;
5211
5768
  }
@@ -5276,6 +5833,22 @@ async function detectInstalledVersion(codeType) {
5276
5833
  }
5277
5834
  return null;
5278
5835
  }
5836
+ function getInstallMethodLabel(method) {
5837
+ switch (method) {
5838
+ case "npm":
5839
+ return i18n.t("installation:installMethodNpm");
5840
+ case "homebrew":
5841
+ return i18n.t("installation:installMethodHomebrew");
5842
+ case "curl":
5843
+ return i18n.t("installation:installMethodCurl");
5844
+ case "powershell":
5845
+ return i18n.t("installation:installMethodPowershell");
5846
+ case "cmd":
5847
+ return i18n.t("installation:installMethodCmd");
5848
+ default:
5849
+ return method;
5850
+ }
5851
+ }
5279
5852
  function getInstallMethodOptions(codeType, recommendedMethods) {
5280
5853
  const allMethods = ["npm", "homebrew", "curl", "powershell", "cmd"];
5281
5854
  const platform = getPlatform();
@@ -5294,7 +5867,8 @@ function getInstallMethodOptions(codeType, recommendedMethods) {
5294
5867
  const topRecommended = recommendedMethods.length > 0 ? recommendedMethods[0] : null;
5295
5868
  return availableMethods.map((method) => {
5296
5869
  const isTopRecommended = method === topRecommended;
5297
- const title = isTopRecommended ? `${i18n.t(`installation:installMethod${method.charAt(0).toUpperCase() + method.slice(1)}`)} ${ansis.green(`[${i18n.t("installation:recommendedMethod")}]`)}` : i18n.t(`installation:installMethod${method.charAt(0).toUpperCase() + method.slice(1)}`);
5870
+ const methodLabel = getInstallMethodLabel(method);
5871
+ const title = isTopRecommended ? `${methodLabel} ${ansis.green(`[${i18n.t("installation:recommendedMethod")}]`)}` : methodLabel;
5298
5872
  return {
5299
5873
  title,
5300
5874
  value: method
@@ -5329,7 +5903,7 @@ async function executeInstallMethod(method, codeType) {
5329
5903
  switch (method) {
5330
5904
  case "npm": {
5331
5905
  const packageName = codeType === "claude-code" ? "@anthropic-ai/claude-code" : "@openai/codex";
5332
- const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", packageName]);
5906
+ const { command, args, usedSudo } = wrapCommandWithSudo("npm", ["install", "-g", packageName, "--force"]);
5333
5907
  if (usedSudo) {
5334
5908
  spinner.info(i18n.t("installation:usingSudo"));
5335
5909
  spinner.start();
@@ -5342,7 +5916,7 @@ async function executeInstallMethod(method, codeType) {
5342
5916
  if (codeType === "claude-code") {
5343
5917
  await exec("brew", ["install", "--cask", "claude-code"]);
5344
5918
  } else {
5345
- await exec("brew", ["install", "codex"]);
5919
+ await exec("brew", ["install", "--cask", "codex"]);
5346
5920
  }
5347
5921
  await setInstallMethod("homebrew", codeType);
5348
5922
  break;
@@ -5381,6 +5955,8 @@ async function executeInstallMethod(method, codeType) {
5381
5955
  throw new Error(`Unsupported install method: ${method}`);
5382
5956
  }
5383
5957
  spinner.succeed(i18n.t("installation:installMethodSuccess", { method }));
5958
+ const verification = await verifyInstallation(codeType);
5959
+ displayVerificationResult(verification, codeType);
5384
5960
  return true;
5385
5961
  } catch (error) {
5386
5962
  spinner.fail(i18n.t("installation:installMethodFailed", { method }));
@@ -5411,10 +5987,187 @@ async function handleInstallFailure(codeType, failedMethods) {
5411
5987
  }
5412
5988
  return await handleInstallFailure(codeType, [...failedMethods, newMethod]);
5413
5989
  }
5990
+ async function isCommandInPath(command) {
5991
+ try {
5992
+ const cmd = getPlatform() === "windows" ? "where" : "which";
5993
+ const res = await exec(cmd, [command]);
5994
+ return res.exitCode === 0;
5995
+ } catch {
5996
+ return false;
5997
+ }
5998
+ }
5999
+ async function verifyInstallation(codeType) {
6000
+ const command = codeType === "claude-code" ? "claude" : "codex";
6001
+ const commandInPath = await isCommandInPath(command);
6002
+ if (commandInPath) {
6003
+ const version = await detectInstalledVersion(codeType);
6004
+ return {
6005
+ success: true,
6006
+ commandPath: await findCommandPath(command),
6007
+ version,
6008
+ needsSymlink: false,
6009
+ symlinkCreated: false
6010
+ };
6011
+ }
6012
+ if (getPlatform() === "macos") {
6013
+ const homebrewPaths = await getHomebrewCommandPaths(command);
6014
+ let foundPath = null;
6015
+ for (const path of homebrewPaths) {
6016
+ if (exists(path)) {
6017
+ foundPath = path;
6018
+ break;
6019
+ }
6020
+ }
6021
+ if (foundPath) {
6022
+ const symlinkResult = await createHomebrewSymlink(command, foundPath);
6023
+ if (symlinkResult.success) {
6024
+ const version = await detectInstalledVersion(codeType);
6025
+ return {
6026
+ success: true,
6027
+ commandPath: symlinkResult.symlinkPath,
6028
+ version,
6029
+ needsSymlink: true,
6030
+ symlinkCreated: true
6031
+ };
6032
+ }
6033
+ return {
6034
+ success: false,
6035
+ commandPath: foundPath,
6036
+ version: null,
6037
+ needsSymlink: true,
6038
+ symlinkCreated: false,
6039
+ error: symlinkResult.error
6040
+ };
6041
+ }
6042
+ }
6043
+ if (isTermux()) {
6044
+ const termuxPrefix = getTermuxPrefix();
6045
+ const termuxPaths = [
6046
+ `${termuxPrefix}/bin/${command}`,
6047
+ `${termuxPrefix}/usr/bin/${command}`
6048
+ ];
6049
+ for (const path of termuxPaths) {
6050
+ if (exists(path)) {
6051
+ const version = await detectInstalledVersion(codeType);
6052
+ return {
6053
+ success: true,
6054
+ commandPath: path,
6055
+ version,
6056
+ needsSymlink: false,
6057
+ symlinkCreated: false
6058
+ };
6059
+ }
6060
+ }
6061
+ }
6062
+ return {
6063
+ success: false,
6064
+ commandPath: null,
6065
+ version: null,
6066
+ needsSymlink: false,
6067
+ symlinkCreated: false,
6068
+ error: "Command not found in any known location"
6069
+ };
6070
+ }
6071
+ async function createHomebrewSymlink(command, sourcePath) {
6072
+ const homebrewBinPaths = [
6073
+ "/opt/homebrew/bin",
6074
+ // Apple Silicon (M1/M2)
6075
+ "/usr/local/bin"
6076
+ // Intel Mac
6077
+ ];
6078
+ let targetDir = null;
6079
+ for (const binPath of homebrewBinPaths) {
6080
+ if (nodeFs.existsSync(binPath)) {
6081
+ targetDir = binPath;
6082
+ break;
6083
+ }
6084
+ }
6085
+ if (!targetDir) {
6086
+ return {
6087
+ success: false,
6088
+ symlinkPath: null,
6089
+ error: "No suitable Homebrew bin directory found"
6090
+ };
6091
+ }
6092
+ const symlinkPath = join(targetDir, command);
6093
+ try {
6094
+ const stats = nodeFs.lstatSync(symlinkPath);
6095
+ if (stats.isSymbolicLink()) {
6096
+ const existingTarget = nodeFs.readlinkSync(symlinkPath);
6097
+ if (existingTarget === sourcePath) {
6098
+ return {
6099
+ success: true,
6100
+ symlinkPath
6101
+ };
6102
+ }
6103
+ nodeFs.unlinkSync(symlinkPath);
6104
+ } else {
6105
+ return {
6106
+ success: false,
6107
+ symlinkPath: null,
6108
+ error: `File already exists at ${symlinkPath} and is not a symlink`
6109
+ };
6110
+ }
6111
+ } catch (error) {
6112
+ if (error.code !== "ENOENT") {
6113
+ return {
6114
+ success: false,
6115
+ symlinkPath: null,
6116
+ error: `Failed to check existing file: ${error}`
6117
+ };
6118
+ }
6119
+ }
6120
+ try {
6121
+ nodeFs.symlinkSync(sourcePath, symlinkPath);
6122
+ return {
6123
+ success: true,
6124
+ symlinkPath
6125
+ };
6126
+ } catch (error) {
6127
+ if (error.code === "EACCES") {
6128
+ return {
6129
+ success: false,
6130
+ symlinkPath: null,
6131
+ error: `Permission denied. Try running: sudo ln -sf ${sourcePath} ${symlinkPath}`
6132
+ };
6133
+ }
6134
+ return {
6135
+ success: false,
6136
+ symlinkPath: null,
6137
+ error: `Failed to create symlink: ${error.message}`
6138
+ };
6139
+ }
6140
+ }
6141
+ function displayVerificationResult(result, codeType) {
6142
+ ensureI18nInitialized();
6143
+ const codeTypeName = codeType === "claude-code" ? i18n.t("common:claudeCode") : i18n.t("common:codex");
6144
+ if (result.success) {
6145
+ if (result.symlinkCreated) {
6146
+ console.log(ansis.green(`\u2714 ${codeTypeName} ${i18n.t("installation:verificationSuccess")}`));
6147
+ console.log(ansis.gray(` ${i18n.t("installation:symlinkCreated", { path: result.commandPath })}`));
6148
+ }
6149
+ if (result.version) {
6150
+ console.log(ansis.gray(` ${i18n.t("installation:detectedVersion", { version: result.version })}`));
6151
+ }
6152
+ } else {
6153
+ console.log(ansis.yellow(`\u26A0 ${codeTypeName} ${i18n.t("installation:verificationFailed")}`));
6154
+ if (result.commandPath) {
6155
+ console.log(ansis.gray(` ${i18n.t("installation:foundAtPath", { path: result.commandPath })}`));
6156
+ }
6157
+ if (result.error) {
6158
+ console.log(ansis.gray(` ${result.error}`));
6159
+ }
6160
+ if (result.needsSymlink && !result.symlinkCreated) {
6161
+ console.log(ansis.yellow(` ${i18n.t("installation:manualSymlinkHint")}`));
6162
+ }
6163
+ }
6164
+ }
5414
6165
 
5415
6166
  const installer = {
5416
6167
  __proto__: null,
6168
+ createHomebrewSymlink: createHomebrewSymlink,
5417
6169
  detectInstalledVersion: detectInstalledVersion,
6170
+ displayVerificationResult: displayVerificationResult,
5418
6171
  executeInstallMethod: executeInstallMethod,
5419
6172
  getInstallationStatus: getInstallationStatus,
5420
6173
  handleInstallFailure: handleInstallFailure,
@@ -5426,7 +6179,8 @@ const installer = {
5426
6179
  removeLocalClaudeCode: removeLocalClaudeCode,
5427
6180
  selectInstallMethod: selectInstallMethod,
5428
6181
  setInstallMethod: setInstallMethod,
5429
- uninstallCodeTool: uninstallCodeTool
6182
+ uninstallCodeTool: uninstallCodeTool,
6183
+ verifyInstallation: verifyInstallation
5430
6184
  };
5431
6185
 
5432
6186
  async function chooseInstallationMethod() {
@@ -6008,6 +6762,17 @@ async function init(options = {}) {
6008
6762
  console.log(ansis.green(`\u2714 ${i18n.t("installation:localInstallationRemoved")}`));
6009
6763
  }
6010
6764
  }
6765
+ const { verifyInstallation, displayVerificationResult } = await Promise.resolve().then(function () { return installer; });
6766
+ const verification = await verifyInstallation("claude-code");
6767
+ if (verification.symlinkCreated) {
6768
+ console.log(ansis.green(`\u2714 ${i18n.t("installation:alreadyInstalled")}`));
6769
+ displayVerificationResult(verification, "claude-code");
6770
+ } else if (!verification.success) {
6771
+ console.log(ansis.yellow(`\u26A0 ${i18n.t("installation:verificationFailed")}`));
6772
+ if (verification.error) {
6773
+ console.log(ansis.gray(` ${verification.error}`));
6774
+ }
6775
+ }
6011
6776
  } else {
6012
6777
  if (options.skipPrompt) {
6013
6778
  await installClaudeCode(true);
@@ -6559,7 +7324,7 @@ async function convertToCodexProvider(config) {
6559
7324
  name: config.name,
6560
7325
  baseUrl,
6561
7326
  wireApi,
6562
- envKey: API_ENV_KEY,
7327
+ tempEnvKey: API_ENV_KEY,
6563
7328
  requiresOpenaiAuth: false,
6564
7329
  model
6565
7330
  };
@@ -6643,4 +7408,4 @@ async function openSettingsJson() {
6643
7408
  }
6644
7409
  }
6645
7410
 
6646
- export { getExistingModelConfig as $, API_DEFAULT_URL as A, getAiOutputLanguageLabel as B, CLAUDE_DIR as C, DEFAULT_CODE_TOOL_TYPE as D, getMcpConfigPath as E, readMcpConfig as F, writeMcpConfig as G, backupMcpConfig as H, mergeMcpServers as I, buildMcpServerConfig as J, fixWindowsMcpConfig as K, LEGACY_ZCF_CONFIG_FILES as L, addCompletedOnboarding as M, ensureApiKeyApproved as N, removeApiKeyFromRejected as O, manageApiKeyApproval as P, setPrimaryApiKey as Q, ensureClaudeDir as R, SETTINGS_FILE as S, backupExistingConfig as T, copyConfigFiles as U, configureApi as V, mergeConfigs as W, updateCustomModel as X, updateDefaultModel as Y, ZCF_CONFIG_DIR as Z, mergeSettingsFile as _, commandExists as a, displayBannerWithInfo as a$, getExistingApiConfig as a0, applyAiLanguageDirective as a1, switchToOfficialLogin$1 as a2, promptApiConfigurationAction as a3, isClaudeCodeInstalled as a4, installClaudeCode as a5, isCodexInstalled as a6, installCodex as a7, isLocalClaudeCodeInstalled as a8, getInstallationStatus as a9, readZcfConfig as aA, configureOutputStyle as aB, isWindows as aC, selectMcpServices as aD, getMcpServices as aE, isCcrInstalled as aF, installCcr as aG, setupCcrConfiguration as aH, modifyApiConfigPartially as aI, formatApiKeyDisplay as aJ, readCcrConfig as aK, configureCcrFeature as aL, handleExitPromptError as aM, handleGeneralError as aN, COMETIX_COMMAND_NAME as aO, COMETIX_COMMANDS as aP, installCometixLine as aQ, checkAndUpdateTools as aR, runCodexUpdate as aS, resolveCodeType as aT, writeJsonConfig as aU, displayBanner as aV, version as aW, resolveAiOutputLanguage as aX, updatePromptOnly as aY, selectAndInstallWorkflows as aZ, checkClaudeCodeVersionAndPrompt as a_, removeLocalClaudeCode as aa, uninstallCodeTool as ab, setInstallMethod as ac, detectInstalledVersion as ad, selectInstallMethod as ae, executeInstallMethod as af, handleInstallFailure as ag, ensureI18nInitialized as ah, i18n as ai, addNumbersToChoices as aj, validateApiKey as ak, promptBoolean as al, ensureDir as am, readDefaultTomlConfig as an, createDefaultTomlConfig as ao, exists as ap, readJsonConfig as aq, writeTomlConfig as ar, copyFile as as, detectConfigManagementMode as at, readCodexConfig as au, backupCodexComplete as av, writeCodexConfig as aw, writeAuthFile as ax, updateZcfConfig as ay, changeLanguage as az, importRecommendedEnv as b, runCodexUninstall as b0, configureCodexMcp as b1, configureCodexApi as b2, runCodexWorkflowImportWithLanguageSelection as b3, runCodexFullInit as b4, switchCodexProvider as b5, listCodexProviders as b6, switchToOfficialLogin as b7, switchToProvider as b8, readZcfConfigAsync as b9, initI18n as ba, selectScriptLanguage as bb, index as bc, fsOperations as bd, jsonConfig as be, claudeConfig as bf, config$1 as bg, config as bh, prompts as bi, codex as bj, installer as bk, cleanupPermissions as c, importRecommendedPermissions as d, CLAUDE_MD_FILE as e, ClAUDE_CONFIG_FILE as f, getPlatform as g, CLAUDE_VSC_CONFIG_FILE as h, init as i, CODEX_DIR as j, CODEX_CONFIG_FILE as k, CODEX_AUTH_FILE as l, mergeAndCleanPermissions as m, CODEX_AGENTS_FILE as n, openSettingsJson as o, CODEX_PROMPTS_DIR as p, ZCF_CONFIG_FILE as q, CODE_TOOL_TYPES as r, CODE_TOOL_BANNERS as s, CODE_TOOL_ALIASES as t, isCodeToolType as u, API_ENV_KEY as v, resolveCodeToolType as w, SUPPORTED_LANGS as x, LANG_LABELS as y, AI_OUTPUT_LANGUAGES as z };
7411
+ export { getExistingModelConfig as $, API_DEFAULT_URL as A, getAiOutputLanguageLabel as B, CLAUDE_DIR as C, DEFAULT_CODE_TOOL_TYPE as D, getMcpConfigPath as E, readMcpConfig as F, writeMcpConfig as G, backupMcpConfig as H, mergeMcpServers as I, buildMcpServerConfig as J, fixWindowsMcpConfig as K, LEGACY_ZCF_CONFIG_FILES as L, addCompletedOnboarding as M, ensureApiKeyApproved as N, removeApiKeyFromRejected as O, manageApiKeyApproval as P, setPrimaryApiKey as Q, ensureClaudeDir as R, SETTINGS_FILE as S, backupExistingConfig as T, copyConfigFiles as U, configureApi as V, mergeConfigs as W, updateCustomModel as X, updateDefaultModel as Y, ZCF_CONFIG_DIR as Z, mergeSettingsFile as _, commandExists as a, updatePromptOnly as a$, getExistingApiConfig as a0, applyAiLanguageDirective as a1, switchToOfficialLogin$1 as a2, promptApiConfigurationAction as a3, isClaudeCodeInstalled as a4, installClaudeCode as a5, isCodexInstalled as a6, installCodex as a7, isLocalClaudeCodeInstalled as a8, getInstallationStatus as a9, writeAuthFile as aA, updateZcfConfig as aB, changeLanguage as aC, readZcfConfig as aD, configureOutputStyle as aE, isWindows as aF, selectMcpServices as aG, getMcpServices as aH, isCcrInstalled as aI, installCcr as aJ, setupCcrConfiguration as aK, modifyApiConfigPartially as aL, formatApiKeyDisplay as aM, readCcrConfig as aN, configureCcrFeature as aO, handleExitPromptError as aP, handleGeneralError as aQ, COMETIX_COMMAND_NAME as aR, COMETIX_COMMANDS as aS, installCometixLine as aT, checkAndUpdateTools as aU, runCodexUpdate as aV, resolveCodeType as aW, writeJsonConfig as aX, displayBanner as aY, version as aZ, resolveAiOutputLanguage as a_, removeLocalClaudeCode as aa, uninstallCodeTool as ab, setInstallMethod as ac, detectInstalledVersion as ad, selectInstallMethod as ae, executeInstallMethod as af, handleInstallFailure as ag, verifyInstallation as ah, createHomebrewSymlink as ai, displayVerificationResult as aj, ensureI18nInitialized as ak, i18n as al, addNumbersToChoices as am, validateApiKey as an, promptBoolean as ao, ensureDir as ap, readDefaultTomlConfig as aq, createDefaultTomlConfig as ar, exists as as, readJsonConfig as at, writeTomlConfig as au, copyFile as av, detectConfigManagementMode as aw, readCodexConfig as ax, backupCodexComplete as ay, writeCodexConfig as az, importRecommendedEnv as b, selectAndInstallWorkflows as b0, checkClaudeCodeVersionAndPrompt as b1, displayBannerWithInfo as b2, runCodexUninstall as b3, configureCodexMcp as b4, configureCodexApi as b5, runCodexWorkflowImportWithLanguageSelection as b6, runCodexFullInit as b7, switchCodexProvider as b8, listCodexProviders as b9, switchToOfficialLogin as ba, switchToProvider as bb, readZcfConfigAsync as bc, initI18n as bd, selectScriptLanguage as be, index as bf, fsOperations as bg, jsonConfig as bh, claudeConfig as bi, config$1 as bj, config as bk, prompts as bl, codex as bm, installer as bn, cleanupPermissions as c, importRecommendedPermissions as d, CLAUDE_MD_FILE as e, ClAUDE_CONFIG_FILE as f, getPlatform as g, CLAUDE_VSC_CONFIG_FILE as h, init as i, CODEX_DIR as j, CODEX_CONFIG_FILE as k, CODEX_AUTH_FILE as l, mergeAndCleanPermissions as m, CODEX_AGENTS_FILE as n, openSettingsJson as o, CODEX_PROMPTS_DIR as p, ZCF_CONFIG_FILE as q, CODE_TOOL_TYPES as r, CODE_TOOL_BANNERS as s, CODE_TOOL_ALIASES as t, isCodeToolType as u, API_ENV_KEY as v, resolveCodeToolType as w, SUPPORTED_LANGS as x, LANG_LABELS as y, AI_OUTPUT_LANGUAGES as z };