claude-multi 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +84 -58
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7289,7 +7289,7 @@ import { join as join3 } from "path";
7289
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
- import { join } from "path";
7292
+ import { dirname, isAbsolute, join, relative, resolve } from "path";
7293
7293
 
7294
7294
  // src/templates.ts
7295
7295
  var PROVIDER_TEMPLATES = {
@@ -7353,6 +7353,56 @@ function applyProviderTemplate(template, apiKey) {
7353
7353
  // src/config.ts
7354
7354
  var CONFIG_DIR = join(homedir(), ".claude-multi");
7355
7355
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
7356
+ var SYNC_DIRS = ["plugins", "skills"];
7357
+ function lstatSafe(path) {
7358
+ try {
7359
+ return lstatSync(path);
7360
+ } catch {
7361
+ return null;
7362
+ }
7363
+ }
7364
+ function resolveSymlinkTarget(symlinkPath) {
7365
+ try {
7366
+ const link = readlinkSync(symlinkPath);
7367
+ if (isAbsolute(link))
7368
+ return link;
7369
+ return resolve(dirname(symlinkPath), link);
7370
+ } catch {
7371
+ return null;
7372
+ }
7373
+ }
7374
+ async function ensureDirSymlink(params) {
7375
+ const { configDir, sourceBaseDir, dir, logLabel } = params;
7376
+ const targetPath = join(configDir, dir);
7377
+ const sourcePath = join(sourceBaseDir, dir);
7378
+ if (!existsSync(sourcePath)) {
7379
+ if (logLabel) {
7380
+ console.log(source_default.yellow(` \u26A0 Source ${dir} not found in ${logLabel}, skipping`));
7381
+ }
7382
+ return "skipped";
7383
+ }
7384
+ const existing = lstatSafe(targetPath);
7385
+ if (existing) {
7386
+ if (existing.isSymbolicLink()) {
7387
+ const resolvedExisting = resolveSymlinkTarget(targetPath);
7388
+ if (resolvedExisting === sourcePath) {
7389
+ if (logLabel)
7390
+ console.log(source_default.gray(` \u2713 ${dir} already synced`));
7391
+ return "already";
7392
+ }
7393
+ rmSync(targetPath, { force: true });
7394
+ } else {
7395
+ rmSync(targetPath, { force: true, recursive: true });
7396
+ }
7397
+ }
7398
+ const linkTarget = relative(configDir, sourcePath);
7399
+ const type = process.platform === "win32" ? "junction" : "dir";
7400
+ await symlink(linkTarget, targetPath, type);
7401
+ if (logLabel) {
7402
+ console.log(source_default.green(` \u2713 Symlinked: ${dir} -> ${logLabel}/${dir}`));
7403
+ }
7404
+ return "linked";
7405
+ }
7356
7406
  function ensureConfigDir() {
7357
7407
  if (!existsSync(CONFIG_DIR)) {
7358
7408
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -7428,7 +7478,6 @@ function isBrokenSymlink(path) {
7428
7478
  const target = readlinkSync(path);
7429
7479
  if (!target)
7430
7480
  return true;
7431
- const { resolve, dirname, isAbsolute } = __require("path");
7432
7481
  let targetPath;
7433
7482
  if (isAbsolute(target)) {
7434
7483
  targetPath = target;
@@ -7441,9 +7490,8 @@ function isBrokenSymlink(path) {
7441
7490
  }
7442
7491
  }
7443
7492
  function detectBrokenSymlinks(configDir) {
7444
- const symlinkDirs = ["plugins", "skills"];
7445
7493
  const result = { broken: [], all: [] };
7446
- for (const dir of symlinkDirs) {
7494
+ for (const dir of SYNC_DIRS) {
7447
7495
  const targetPath = join(configDir, dir);
7448
7496
  try {
7449
7497
  const stats = lstatSync(targetPath);
@@ -7493,7 +7541,6 @@ async function copyAllFromDefault(targetConfigDir, autoSync = true) {
7493
7541
  if (!existsSync(targetConfigDir)) {
7494
7542
  await mkdir(targetConfigDir, { recursive: true });
7495
7543
  }
7496
- const symlinkDirs = ["plugins", "skills"];
7497
7544
  const excludeFiles = [
7498
7545
  "config.json",
7499
7546
  ".config.json",
@@ -7518,25 +7565,12 @@ async function copyAllFromDefault(targetConfigDir, autoSync = true) {
7518
7565
  continue;
7519
7566
  }
7520
7567
  if (stat.isDirectory()) {
7521
- if (autoSync && symlinkDirs.includes(entry)) {
7522
- if (existsSync(targetPath)) {
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}`));
7534
- }
7535
- rmSync(targetPath, { force: true, recursive: true });
7536
- }
7537
- const absoluteSource = join(homedir(), ".claude", entry);
7538
- await symlink(absoluteSource, targetPath, "dir");
7539
- console.log(` Symlinked: ${entry} -> ~/.claude/${entry}`);
7568
+ if (autoSync && SYNC_DIRS.includes(entry)) {
7569
+ await ensureDirSymlink({
7570
+ configDir: target,
7571
+ sourceBaseDir: defaultDir,
7572
+ dir: entry
7573
+ });
7540
7574
  } else {
7541
7575
  if (!existsSync(targetPath)) {
7542
7576
  await mkdir(targetPath, { recursive: true });
@@ -7653,46 +7687,28 @@ async function createSettingsFromTemplate(targetConfigDir, template, apiKey) {
7653
7687
  }
7654
7688
  async function syncPluginsAndSkills(configDir) {
7655
7689
  const defaultDir = getDefaultClaudeDir();
7656
- const symlinkDirs = ["plugins", "skills"];
7657
7690
  if (!existsSync(configDir)) {
7658
7691
  throw new Error("Instance config directory does not exist");
7659
7692
  }
7660
- for (const dir of symlinkDirs) {
7661
- const targetPath = join(configDir, dir);
7662
- const sourcePath = join(defaultDir, dir);
7663
- if (!existsSync(sourcePath)) {
7664
- console.log(source_default.yellow(` \u26A0 Source ${dir} not found in ~/.claude, skipping`));
7665
- continue;
7666
- }
7667
- if (existsSync(targetPath)) {
7668
- try {
7669
- const linkTarget = readlinkSync(targetPath);
7670
- const expectedTarget = join("..", "..", ".claude", dir);
7671
- if (linkTarget === expectedTarget || linkTarget === sourcePath) {
7672
- console.log(source_default.gray(` \u2713 ${dir} already synced`));
7673
- continue;
7674
- }
7675
- rmSync(targetPath, { force: true });
7676
- } catch {
7677
- rmSync(targetPath, { force: true, recursive: true });
7678
- }
7679
- }
7680
- const relativePath = join("..", "..", ".claude", dir);
7681
- await symlink(relativePath, targetPath, "dir");
7682
- console.log(source_default.green(` \u2713 Symlinked: ${dir} -> ~/.claude/${dir}`));
7693
+ for (const dir of SYNC_DIRS) {
7694
+ await ensureDirSymlink({
7695
+ configDir,
7696
+ sourceBaseDir: defaultDir,
7697
+ dir,
7698
+ logLabel: defaultDir
7699
+ });
7683
7700
  }
7684
7701
  }
7685
7702
  async function unsyncPluginsAndSkills(configDir) {
7686
7703
  const defaultDir = getDefaultClaudeDir();
7687
- const symlinkDirs = ["plugins", "skills"];
7688
7704
  if (!existsSync(configDir)) {
7689
7705
  throw new Error("Instance config directory does not exist");
7690
7706
  }
7691
- for (const dir of symlinkDirs) {
7707
+ for (const dir of SYNC_DIRS) {
7692
7708
  const targetPath = join(configDir, dir);
7693
7709
  const sourcePath = join(defaultDir, dir);
7694
7710
  if (!existsSync(sourcePath)) {
7695
- console.log(source_default.yellow(` \u26A0 Source ${dir} not found in ~/.claude, skipping`));
7711
+ console.log(source_default.yellow(` \u26A0 Source ${dir} not found in ${defaultDir}, skipping`));
7696
7712
  continue;
7697
7713
  }
7698
7714
  let isSymlink = false;
@@ -7779,7 +7795,7 @@ async function listAvailablePlugins() {
7779
7795
  // src/wrapper.ts
7780
7796
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync, unlinkSync as unlinkSync2 } from "fs";
7781
7797
  import { writeFile as writeFile2 } from "fs/promises";
7782
- import { resolve, dirname } from "path";
7798
+ import { resolve as resolve2, dirname as dirname2 } from "path";
7783
7799
  import { execSync } from "child_process";
7784
7800
  function getClaudePath() {
7785
7801
  try {
@@ -7829,7 +7845,7 @@ set "CLAUDE_CONFIG_DIR=${options.configDir}"
7829
7845
  }
7830
7846
  async function createWrapper(options) {
7831
7847
  const { binaryPath, configDir } = options;
7832
- const binDir = dirname(binaryPath);
7848
+ const binDir = dirname2(binaryPath);
7833
7849
  if (!existsSync2(binDir)) {
7834
7850
  mkdirSync2(binDir, { recursive: true });
7835
7851
  }
@@ -7851,9 +7867,9 @@ function removeWrapper(binaryPath) {
7851
7867
  }
7852
7868
  }
7853
7869
  function getDefaultBinaryPath(name) {
7854
- const binDir = process.platform === "win32" ? resolve(process.env.LOCALAPPDATA || "", "bun", "bin") : resolve(process.env.HOME || "", ".local", "bin");
7870
+ const binDir = process.platform === "win32" ? resolve2(process.env.LOCALAPPDATA || "", "bun", "bin") : resolve2(process.env.HOME || "", ".local", "bin");
7855
7871
  const binaryName = process.platform === "win32" ? `claude-${name}.cmd` : `claude-${name}`;
7856
- return resolve(binDir, binaryName);
7872
+ return resolve2(binDir, binaryName);
7857
7873
  }
7858
7874
 
7859
7875
  // src/version.ts
@@ -8817,6 +8833,7 @@ async function handleToggleAutoSync(instances) {
8817
8833
  message: `Auto-sync is currently ${source_default.cyan(currentStatus ? "enabled" : "disabled")}. What would you like to do?`,
8818
8834
  choices: [
8819
8835
  { title: currentStatus ? "Turn off (copy files locally)" : "Turn on (use symlinks)", value: "toggle" },
8836
+ ...currentStatus ? [{ title: "Force re-sync now (rebuild symlinks)", value: "force-sync" }] : [],
8820
8837
  { title: "Cancel", value: "cancel" }
8821
8838
  ],
8822
8839
  initial: 0
@@ -8825,11 +8842,20 @@ async function handleToggleAutoSync(instances) {
8825
8842
  console.log(source_default.yellow("\u2717 Cancelled"));
8826
8843
  return;
8827
8844
  }
8828
- const newStatus = !currentStatus;
8829
- console.log(source_default.bold(`
8845
+ try {
8846
+ if (action === "force-sync") {
8847
+ console.log(source_default.bold(`
8848
+ \uD83D\uDD04 Re-syncing symlinks for '${instanceName}'...
8849
+ `));
8850
+ await syncPluginsAndSkills(instance.configDir);
8851
+ console.log(source_default.green(`
8852
+ \u2713 Symlinks rebuilt for '${instanceName}'`));
8853
+ return;
8854
+ }
8855
+ const newStatus = !currentStatus;
8856
+ console.log(source_default.bold(`
8830
8857
  \uD83D\uDD04 ${newStatus ? "Enabling" : "Disabling"} auto-sync for '${instanceName}'...
8831
8858
  `));
8832
- try {
8833
8859
  await updateInstanceAutoSync(instanceName, newStatus);
8834
8860
  if (newStatus) {
8835
8861
  await syncPluginsAndSkills(instance.configDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multi",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Manage multiple Claude Code instances with different aliases and configurations",
5
5
  "type": "module",
6
6
  "bin": {