claude-multi 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +104 -6
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.3] - 2026-01-26
9
+
10
+ ### Added
11
+ - **Broken Symlink Detection**: Automatically detect and fix broken symlinks
12
+ - New `isBrokenSymlink()` and `detectBrokenSymlinks()` functions
13
+ - Auto-fix broken symlinks when creating or updating instances
14
+ - Handles edge cases: broken symlinks, existing directories, incorrect symlink targets
15
+ - **`fix-symlinks` Command**: Fix broken symlinks for existing instances
16
+ - Interactive instance selection with multi-select support
17
+ - `--all` flag to fix all instances at once
18
+ - Auto-fixes instances with auto-sync enabled, warns for manual mode
19
+ - Clear status indicators for broken symlinks vs. valid symlinks
20
+ - **Interactive Re-sync**: Added "Re-sync symlinks" option to interactive mode menu
21
+ - Quick access to symlink diagnosis and repair from interactive UI
22
+ - **Test Suite**: Comprehensive tests for symlink detection and fixing
23
+ - Tests for broken symlink detection
24
+ - Tests for valid symlink handling
25
+ - Tests for mixed scenarios (broken + valid symlinks)
26
+ - Error handling tests for non-existent paths
27
+
28
+ ### Fixed
29
+ - Symlink creation now uses `lstatSync` for proper broken symlink detection
30
+ - Previously, `existsSync()` returned `false` for broken symlinks, preventing detection
31
+ - Now correctly identifies and repairs broken symlinks in all scenarios
32
+
33
+ [0.4.3]: https://github.com/hmziqrs/claude-multi/compare/v0.4.2...v0.4.3
34
+
8
35
  ## [0.4.2] - 2026-01-26
9
36
 
10
37
  ### Fixed
package/dist/cli.js CHANGED
@@ -7286,7 +7286,7 @@ import { homedir as homedir2 } from "os";
7286
7286
  import { join as join3 } from "path";
7287
7287
 
7288
7288
  // src/config.ts
7289
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, readlinkSync, rmSync } from "fs";
7289
+ import { existsSync, mkdirSync, readdirSync, statSync, readlinkSync, rmSync, lstatSync } from "fs";
7290
7290
  import { readFile, writeFile, copyFile, mkdir, symlink } from "fs/promises";
7291
7291
  import { homedir } from "os";
7292
7292
  import { join } from "path";
@@ -7420,6 +7420,41 @@ function getDefaultClaudeDir() {
7420
7420
  }
7421
7421
  return join(homedir(), ".claude");
7422
7422
  }
7423
+ function isBrokenSymlink(path) {
7424
+ try {
7425
+ const stats = lstatSync(path);
7426
+ if (!stats.isSymbolicLink())
7427
+ return false;
7428
+ const target = readlinkSync(path);
7429
+ if (!target)
7430
+ return true;
7431
+ const { resolve, dirname, isAbsolute } = __require("path");
7432
+ let targetPath;
7433
+ if (isAbsolute(target)) {
7434
+ targetPath = target;
7435
+ } else {
7436
+ targetPath = resolve(dirname(path), target);
7437
+ }
7438
+ return !existsSync(targetPath);
7439
+ } catch {
7440
+ return false;
7441
+ }
7442
+ }
7443
+ function detectBrokenSymlinks(configDir) {
7444
+ const symlinkDirs = ["plugins", "skills"];
7445
+ const result = { broken: [], all: [] };
7446
+ for (const dir of symlinkDirs) {
7447
+ const targetPath = join(configDir, dir);
7448
+ try {
7449
+ const stats = lstatSync(targetPath);
7450
+ result.all.push(dir);
7451
+ if (stats.isSymbolicLink() && isBrokenSymlink(targetPath)) {
7452
+ result.broken.push(dir);
7453
+ }
7454
+ } catch {}
7455
+ }
7456
+ return result;
7457
+ }
7423
7458
  function hasDefaultClaudeConfig() {
7424
7459
  const defaultDir = getDefaultClaudeDir();
7425
7460
  const settingsFile = join(defaultDir, "settings.json");
@@ -7485,12 +7520,19 @@ async function copyAllFromDefault(targetConfigDir, autoSync = true) {
7485
7520
  if (stat.isDirectory()) {
7486
7521
  if (autoSync && symlinkDirs.includes(entry)) {
7487
7522
  if (existsSync(targetPath)) {
7488
- try {
7489
- readlinkSync(targetPath);
7490
- unlinkSync(targetPath);
7491
- } catch {
7492
- rmSync(targetPath, { force: true, recursive: true });
7523
+ const absoluteSource2 = join(homedir(), ".claude", entry);
7524
+ const stat2 = lstatSync(targetPath);
7525
+ if (stat2.isSymbolicLink()) {
7526
+ const currentTarget = readlinkSync(targetPath);
7527
+ if (currentTarget === absoluteSource2) {
7528
+ console.log(` \u2713 Already symlinked: ${entry}`);
7529
+ continue;
7530
+ }
7531
+ console.log(source_default.yellow(` \u26A0 Replacing incorrect symlink: ${entry}`));
7532
+ } else {
7533
+ console.log(source_default.yellow(` \u26A0 Removing existing directory: ${entry}`));
7493
7534
  }
7535
+ rmSync(targetPath, { force: true, recursive: true });
7494
7536
  }
7495
7537
  const absoluteSource = join(homedir(), ".claude", entry);
7496
7538
  await symlink(absoluteSource, targetPath, "dir");
@@ -8160,6 +8202,9 @@ program2.command("auto-sync <name> <status>").description("Toggle auto-sync for
8160
8202
  process.exit(1);
8161
8203
  }
8162
8204
  });
8205
+ program2.command("fix-symlinks [name...]").description("Fix broken symlinks for instances (auto-detects and repairs)").option("-a, --all", "Fix all instances").action(async (names, options) => {
8206
+ await handleFixSymlinks(names, options.all);
8207
+ });
8163
8208
  program2.command("plugins").description("Manage enabled plugins for instances").argument("[action]", "Action to perform (list, enable, disable, copy)", "list").argument("[instance]", "Instance name (for list/enable/disable)", "").argument("[plugins...]", "Plugin IDs to enable/disable", []).action(async (action = "list", instanceName = "", plugins = []) => {
8164
8209
  try {
8165
8210
  switch (action) {
@@ -8317,6 +8362,55 @@ async function handlePluginsCopy(instanceName) {
8317
8362
  const enabledCount = Object.values(defaultPlugins).filter((v) => v === true).length;
8318
8363
  console.log(source_default.green(`\u2713 Copied ${enabledCount} enabled plugins to '${instanceName}'`));
8319
8364
  }
8365
+ async function handleFixSymlinks(names, fixAll) {
8366
+ const instances = await listInstances();
8367
+ if (instances.length === 0) {
8368
+ console.log(source_default.yellow("No instances found."));
8369
+ return;
8370
+ }
8371
+ let instancesToFix;
8372
+ if (fixAll) {
8373
+ instancesToFix = instances;
8374
+ } else if (names.length > 0) {
8375
+ instancesToFix = names.map((name) => instances.find((i) => i.name === name)).filter(Boolean);
8376
+ } else {
8377
+ const { selected } = await import_prompts.default({
8378
+ type: "multiselect",
8379
+ name: "selected",
8380
+ message: "Select instances to fix:",
8381
+ choices: instances.map((i) => ({
8382
+ title: `${i.name} ${i.autoSync ? "(auto-sync)" : "(manual)"}`,
8383
+ value: i.name
8384
+ }))
8385
+ });
8386
+ instancesToFix = selected.map((name) => instances.find((i) => i.name === name)).filter(Boolean);
8387
+ }
8388
+ for (const instance of instancesToFix) {
8389
+ const diagnosis = detectBrokenSymlinks(instance.configDir);
8390
+ const needsFix = diagnosis.broken.length > 0;
8391
+ console.log(source_default.bold(`
8392
+ \uD83D\uDD0D ${instance.name}`));
8393
+ console.log(` Config: ${instance.configDir}`);
8394
+ console.log(` Auto-sync: ${instance.autoSync ? "enabled" : "disabled"}`);
8395
+ if (diagnosis.all.length === 0) {
8396
+ console.log(source_default.gray(" No symlinks found (manual mode)"));
8397
+ continue;
8398
+ }
8399
+ if (needsFix) {
8400
+ console.log(source_default.red(` \u274C Broken: ${diagnosis.broken.join(", ")}`));
8401
+ if (instance.autoSync) {
8402
+ await syncPluginsAndSkills(instance.configDir);
8403
+ console.log(source_default.green(` \u2705 Fixed: ${diagnosis.broken.join(", ")}`));
8404
+ } else {
8405
+ console.log(source_default.yellow(" \u26A0 Auto-sync is disabled. Enable it first."));
8406
+ }
8407
+ } else {
8408
+ console.log(source_default.green(` \u2705 All symlinks OK: ${diagnosis.all.join(", ")}`));
8409
+ }
8410
+ }
8411
+ console.log(source_default.bold(`
8412
+ \u2728 Done!`));
8413
+ }
8320
8414
  program2.command("interactive").alias("i").description("Launch interactive mode for managing Claude Code instances").action(async () => {
8321
8415
  try {
8322
8416
  await runInteractiveMode();
@@ -8348,6 +8442,7 @@ async function runInteractiveMode() {
8348
8442
  { title: "\u2139\uFE0F Show instance details", value: "info" },
8349
8443
  { title: "\uD83D\uDD0C Manage plugins", value: "plugins" },
8350
8444
  { title: "\uD83D\uDD04 Toggle auto-sync", value: "autosync" },
8445
+ { title: "\uD83D\uDD04 Re-sync symlinks", value: "resync" },
8351
8446
  { title: "\uD83D\uDDD1\uFE0F Remove instance", value: "remove" }
8352
8447
  ] : [],
8353
8448
  { title: "\uD83D\uDEAA Exit", value: "exit" }
@@ -8375,6 +8470,9 @@ async function runInteractiveMode() {
8375
8470
  case "autosync":
8376
8471
  await handleToggleAutoSync(instances);
8377
8472
  break;
8473
+ case "resync":
8474
+ await handleFixSymlinks([], false);
8475
+ break;
8378
8476
  case "remove":
8379
8477
  await handleRemoveInstance(instances);
8380
8478
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multi",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Manage multiple Claude Code instances with different aliases and configurations",
5
5
  "type": "module",
6
6
  "bin": {