claude-multi 0.4.1 → 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.
- package/CHANGELOG.md +42 -2
- package/dist/cli.js +106 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,47 @@ 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.
|
|
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
|
+
|
|
35
|
+
## [0.4.2] - 2026-01-26
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Auto-sync symlinks now use absolute paths instead of relative paths
|
|
39
|
+
- Previously used hardcoded `../../.claude/` which assumed instances were in `~/.claude-instances/`
|
|
40
|
+
- Now uses `homedir()` to resolve absolute paths, working for any instance location
|
|
41
|
+
- Fixes broken symlinks when instances are created directly in home directory (e.g., `~/.claude-tester/`)
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Symlink test suite to verify both relative and absolute symlink behaviors
|
|
45
|
+
|
|
46
|
+
[0.4.2]: https://github.com/hmziqrs/claude-multi/compare/v0.4.1...v0.4.2
|
|
47
|
+
|
|
48
|
+
## [0.4.1] - 2026-01-26
|
|
9
49
|
|
|
10
50
|
### Added
|
|
11
51
|
- **Test Suite**: Comprehensive sandboxing tests for auto-sync functionality
|
|
@@ -21,7 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
61
|
|
|
22
62
|
[0.4.1]: https://github.com/hmziqrs/claude-multi/compare/v0.4.0...v0.4.1
|
|
23
63
|
|
|
24
|
-
## [0.4.0] -
|
|
64
|
+
## [0.4.0] - 2026-01-26
|
|
25
65
|
|
|
26
66
|
### Added
|
|
27
67
|
- **Auto-Sync for Plugins and Skills**: Share plugins/skills across instances via symlinks
|
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,
|
|
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,15 +7520,22 @@ async function copyAllFromDefault(targetConfigDir, autoSync = true) {
|
|
|
7485
7520
|
if (stat.isDirectory()) {
|
|
7486
7521
|
if (autoSync && symlinkDirs.includes(entry)) {
|
|
7487
7522
|
if (existsSync(targetPath)) {
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
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
|
-
const
|
|
7496
|
-
await symlink(
|
|
7537
|
+
const absoluteSource = join(homedir(), ".claude", entry);
|
|
7538
|
+
await symlink(absoluteSource, targetPath, "dir");
|
|
7497
7539
|
console.log(` Symlinked: ${entry} -> ~/.claude/${entry}`);
|
|
7498
7540
|
} else {
|
|
7499
7541
|
if (!existsSync(targetPath)) {
|
|
@@ -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;
|