claude-code-swarm 0.3.10 → 0.3.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/mcp-launcher.mjs +176 -0
- package/.claude-plugin/plugin.json +16 -7
- package/CLAUDE.md +10 -0
- package/package.json +1 -4
- package/scripts/map-sidecar.mjs +4 -2
- package/src/__tests__/bootstrap.test.mjs +46 -0
- package/src/__tests__/e2e-main-agent-registration.test.mjs +229 -0
- package/src/__tests__/e2e-reconnection.test.mjs +5 -5
- package/src/__tests__/sidecar-server.test.mjs +4 -4
- package/src/__tests__/swarmkit-resolver.test.mjs +168 -0
- package/src/bootstrap.mjs +14 -0
- package/src/map-connection.mjs +10 -3
- package/src/mesh-connection.mjs +10 -3
- package/src/sessionlog.mjs +4 -1
- package/src/sidecar-server.mjs +63 -25
- package/src/skilltree-client.mjs +7 -31
- package/src/swarmkit-resolver.mjs +48 -0
- package/.claude-plugin/run-agent-inbox-mcp.sh +0 -95
- package/.claude-plugin/run-minimem-mcp.sh +0 -98
- package/.claude-plugin/run-opentasks-mcp.sh +0 -65
- package/scripts/dev-link.mjs +0 -179
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Wrapper script to run agent-inbox MCP server
|
|
3
|
-
# When the sidecar's inbox socket exists, runs in proxy mode (IPC client).
|
|
4
|
-
# Otherwise falls back to standalone mode with its own storage.
|
|
5
|
-
# Exits silently if inbox is not enabled or not installed.
|
|
6
|
-
|
|
7
|
-
# Check if inbox is enabled in config
|
|
8
|
-
ENABLED=false
|
|
9
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
10
|
-
ENABLED=$(node -e "
|
|
11
|
-
try {
|
|
12
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
13
|
-
const envEnabled = (process.env.SWARM_INBOX_ENABLED || '').toLowerCase();
|
|
14
|
-
const isEnabled = ['true', '1', 'yes'].includes(envEnabled) || c.inbox?.enabled === true;
|
|
15
|
-
process.stdout.write(isEnabled ? 'true' : 'false');
|
|
16
|
-
} catch { process.stdout.write('false'); }
|
|
17
|
-
" 2>/dev/null || echo "false")
|
|
18
|
-
elif [ -n "$SWARM_INBOX_ENABLED" ]; then
|
|
19
|
-
case "$(echo "$SWARM_INBOX_ENABLED" | tr '[:upper:]' '[:lower:]')" in
|
|
20
|
-
true|1|yes) ENABLED=true ;;
|
|
21
|
-
esac
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
if [ "$ENABLED" != "true" ]; then
|
|
25
|
-
# Not enabled — exit silently so Claude Code doesn't show an error
|
|
26
|
-
sleep 0.1
|
|
27
|
-
exit 0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
# Read scope from config (defaults to MAP scope or "default")
|
|
31
|
-
SCOPE="default"
|
|
32
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
33
|
-
CONFIGURED_SCOPE=$(node -e "
|
|
34
|
-
try {
|
|
35
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
36
|
-
const s = c.inbox?.scope || c.map?.scope || process.env.SWARM_INBOX_SCOPE || '';
|
|
37
|
-
if (s) process.stdout.write(s);
|
|
38
|
-
} catch {}
|
|
39
|
-
" 2>/dev/null)
|
|
40
|
-
if [ -n "$CONFIGURED_SCOPE" ]; then
|
|
41
|
-
SCOPE="$CONFIGURED_SCOPE"
|
|
42
|
-
fi
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
if [ -n "$SWARM_INBOX_SCOPE" ]; then
|
|
46
|
-
SCOPE="$SWARM_INBOX_SCOPE"
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
export INBOX_SCOPE="$SCOPE"
|
|
50
|
-
|
|
51
|
-
# Discover sidecar inbox socket for proxy mode
|
|
52
|
-
# Check well-known paths: .swarm/claude-swarm/tmp/map/inbox.sock
|
|
53
|
-
INBOX_SOCK=""
|
|
54
|
-
if [ -S .swarm/claude-swarm/tmp/map/inbox.sock ]; then
|
|
55
|
-
INBOX_SOCK=".swarm/claude-swarm/tmp/map/inbox.sock"
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
# Also check per-session paths
|
|
59
|
-
if [ -z "$INBOX_SOCK" ] && [ -d .swarm/claude-swarm/tmp/map/sessions ]; then
|
|
60
|
-
# Find the most recently modified inbox.sock in session dirs
|
|
61
|
-
INBOX_SOCK=$(find .swarm/claude-swarm/tmp/map/sessions -name inbox.sock -type s 2>/dev/null | head -1)
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
# If inbox socket found, enable proxy mode
|
|
65
|
-
if [ -n "$INBOX_SOCK" ]; then
|
|
66
|
-
export INBOX_SOCKET_PATH="$INBOX_SOCK"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
# Try to find the agent-inbox module entry point
|
|
70
|
-
INBOX_MAIN=""
|
|
71
|
-
|
|
72
|
-
# 1. Check global npm root (swarmkit installs here)
|
|
73
|
-
GLOBAL_ROOT=$(npm root -g 2>/dev/null)
|
|
74
|
-
if [ -n "$GLOBAL_ROOT" ] && [ -f "$GLOBAL_ROOT/agent-inbox/dist/index.js" ]; then
|
|
75
|
-
INBOX_MAIN="$GLOBAL_ROOT/agent-inbox/dist/index.js"
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
# 2. Check plugin directory's node_modules (dev installs)
|
|
79
|
-
if [ -z "$INBOX_MAIN" ] && [ -n "$CLAUDE_PLUGIN_ROOT" ] && [ -f "$CLAUDE_PLUGIN_ROOT/node_modules/agent-inbox/dist/index.js" ]; then
|
|
80
|
-
INBOX_MAIN="$CLAUDE_PLUGIN_ROOT/node_modules/agent-inbox/dist/index.js"
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
# 3. Fallback: try require.resolve from CWD
|
|
84
|
-
if [ -z "$INBOX_MAIN" ]; then
|
|
85
|
-
INBOX_MAIN=$(node -e "try { console.log(require.resolve('agent-inbox/dist/index.js')); } catch {}" 2>/dev/null)
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
if [ -n "$INBOX_MAIN" ]; then
|
|
89
|
-
# Uses proxy mode when INBOX_SOCKET_PATH is set, standalone otherwise
|
|
90
|
-
exec node "$INBOX_MAIN" mcp
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
# agent-inbox not installed — log to stderr and exit cleanly
|
|
94
|
-
echo "[agent-inbox-mcp] agent-inbox not found. Install with: npm install -g agent-inbox or install via swarmkit" >&2
|
|
95
|
-
exit 0
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Wrapper script to run minimem MCP server
|
|
3
|
-
# Reads provider and directory from swarm config
|
|
4
|
-
# Exits silently if minimem is not enabled or not installed
|
|
5
|
-
|
|
6
|
-
# Check if minimem is enabled in config
|
|
7
|
-
ENABLED=false
|
|
8
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
9
|
-
ENABLED=$(node -e "
|
|
10
|
-
try {
|
|
11
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
12
|
-
const envEnabled = (process.env.SWARM_MINIMEM_ENABLED || '').toLowerCase();
|
|
13
|
-
const isEnabled = ['true', '1', 'yes'].includes(envEnabled) || c.minimem?.enabled === true;
|
|
14
|
-
process.stdout.write(isEnabled ? 'true' : 'false');
|
|
15
|
-
} catch { process.stdout.write('false'); }
|
|
16
|
-
" 2>/dev/null || echo "false")
|
|
17
|
-
elif [ -n "$SWARM_MINIMEM_ENABLED" ]; then
|
|
18
|
-
case "$(echo "$SWARM_MINIMEM_ENABLED" | tr '[:upper:]' '[:lower:]')" in
|
|
19
|
-
true|1|yes) ENABLED=true ;;
|
|
20
|
-
esac
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
if [ "$ENABLED" != "true" ]; then
|
|
24
|
-
# Not enabled — exit silently so Claude Code doesn't show an error
|
|
25
|
-
sleep 0.1
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
# Read provider from config (defaults to "auto")
|
|
30
|
-
PROVIDER="auto"
|
|
31
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
32
|
-
CONFIGURED_PROVIDER=$(node -e "
|
|
33
|
-
try {
|
|
34
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
35
|
-
const p = process.env.SWARM_MINIMEM_PROVIDER || c.minimem?.provider || '';
|
|
36
|
-
if (p) process.stdout.write(p);
|
|
37
|
-
} catch {}
|
|
38
|
-
" 2>/dev/null)
|
|
39
|
-
if [ -n "$CONFIGURED_PROVIDER" ]; then
|
|
40
|
-
PROVIDER="$CONFIGURED_PROVIDER"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
if [ -n "$SWARM_MINIMEM_PROVIDER" ]; then
|
|
45
|
-
PROVIDER="$SWARM_MINIMEM_PROVIDER"
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
# Discover memory directory: config dir > .swarm/minimem/ > cwd
|
|
49
|
-
MEMORY_DIR=""
|
|
50
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
51
|
-
CONFIGURED_DIR=$(node -e "
|
|
52
|
-
try {
|
|
53
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
54
|
-
const d = process.env.SWARM_MINIMEM_DIR || c.minimem?.dir || '';
|
|
55
|
-
if (d) process.stdout.write(d);
|
|
56
|
-
} catch {}
|
|
57
|
-
" 2>/dev/null)
|
|
58
|
-
if [ -n "$CONFIGURED_DIR" ]; then
|
|
59
|
-
MEMORY_DIR="$CONFIGURED_DIR"
|
|
60
|
-
fi
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
if [ -n "$SWARM_MINIMEM_DIR" ]; then
|
|
64
|
-
MEMORY_DIR="$SWARM_MINIMEM_DIR"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
if [ -z "$MEMORY_DIR" ]; then
|
|
68
|
-
if [ -d ".swarm/minimem" ]; then
|
|
69
|
-
MEMORY_DIR=".swarm/minimem"
|
|
70
|
-
else
|
|
71
|
-
MEMORY_DIR="."
|
|
72
|
-
fi
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# Check if global memory should also be searched
|
|
76
|
-
GLOBAL_ARG=""
|
|
77
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
78
|
-
USE_GLOBAL=$(node -e "
|
|
79
|
-
try {
|
|
80
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
81
|
-
const envGlobal = (process.env.SWARM_MINIMEM_GLOBAL || '').toLowerCase();
|
|
82
|
-
const isGlobal = ['true', '1', 'yes'].includes(envGlobal) || c.minimem?.global === true;
|
|
83
|
-
process.stdout.write(isGlobal ? 'true' : 'false');
|
|
84
|
-
} catch { process.stdout.write('false'); }
|
|
85
|
-
" 2>/dev/null || echo "false")
|
|
86
|
-
if [ "$USE_GLOBAL" = "true" ]; then
|
|
87
|
-
GLOBAL_ARG="--global"
|
|
88
|
-
fi
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Try installed minimem command
|
|
92
|
-
if command -v minimem &> /dev/null; then
|
|
93
|
-
exec minimem mcp --dir "$MEMORY_DIR" --provider "$PROVIDER" $GLOBAL_ARG
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
# minimem not installed — log to stderr and exit cleanly
|
|
97
|
-
echo "[minimem-mcp] minimem CLI not found. Install with: npm install -g minimem" >&2
|
|
98
|
-
exit 0
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Wrapper script to run opentasks MCP server
|
|
3
|
-
# Reads scope from swarm config, defaults to "tasks"
|
|
4
|
-
# Exits silently if opentasks is not enabled or not installed
|
|
5
|
-
|
|
6
|
-
# Check if opentasks is enabled in config
|
|
7
|
-
ENABLED=false
|
|
8
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
9
|
-
ENABLED=$(node -e "
|
|
10
|
-
try {
|
|
11
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
12
|
-
const envEnabled = (process.env.SWARM_OPENTASKS_ENABLED || '').toLowerCase();
|
|
13
|
-
const isEnabled = ['true', '1', 'yes'].includes(envEnabled) || c.opentasks?.enabled === true;
|
|
14
|
-
process.stdout.write(isEnabled ? 'true' : 'false');
|
|
15
|
-
} catch { process.stdout.write('false'); }
|
|
16
|
-
" 2>/dev/null || echo "false")
|
|
17
|
-
elif [ -n "$SWARM_OPENTASKS_ENABLED" ]; then
|
|
18
|
-
case "$(echo "$SWARM_OPENTASKS_ENABLED" | tr '[:upper:]' '[:lower:]')" in
|
|
19
|
-
true|1|yes) ENABLED=true ;;
|
|
20
|
-
esac
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
if [ "$ENABLED" != "true" ]; then
|
|
24
|
-
# Not enabled — exit silently so Claude Code doesn't show an error
|
|
25
|
-
# Sleep briefly then exit so the MCP transport doesn't see an immediate close
|
|
26
|
-
sleep 0.1
|
|
27
|
-
exit 0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
# Read scope from config
|
|
31
|
-
SCOPE="tasks"
|
|
32
|
-
if [ -f .swarm/claude-swarm/config.json ]; then
|
|
33
|
-
CONFIGURED_SCOPE=$(node -e "
|
|
34
|
-
try {
|
|
35
|
-
const c = JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json', 'utf-8'));
|
|
36
|
-
const s = c.opentasks?.scope || process.env.SWARM_OPENTASKS_SCOPE || '';
|
|
37
|
-
if (s) process.stdout.write(s);
|
|
38
|
-
} catch {}
|
|
39
|
-
" 2>/dev/null)
|
|
40
|
-
if [ -n "$CONFIGURED_SCOPE" ]; then
|
|
41
|
-
SCOPE="$CONFIGURED_SCOPE"
|
|
42
|
-
fi
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
if [ -n "$SWARM_OPENTASKS_SCOPE" ]; then
|
|
46
|
-
SCOPE="$SWARM_OPENTASKS_SCOPE"
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
# Build socket path arg if daemon socket exists
|
|
50
|
-
SOCKET_ARG=""
|
|
51
|
-
for SOCK in .swarm/opentasks/daemon.sock .opentasks/daemon.sock .git/opentasks/daemon.sock; do
|
|
52
|
-
if [ -S "$SOCK" ]; then
|
|
53
|
-
SOCKET_ARG="--socket $SOCK"
|
|
54
|
-
break
|
|
55
|
-
fi
|
|
56
|
-
done
|
|
57
|
-
|
|
58
|
-
# Try installed opentasks command
|
|
59
|
-
if command -v opentasks &> /dev/null; then
|
|
60
|
-
exec opentasks mcp --scope "$SCOPE" $SOCKET_ARG
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
# opentasks not installed — log to stderr and exit cleanly
|
|
64
|
-
echo "[opentasks-mcp] opentasks CLI not found. Install with: npm install -g opentasks" >&2
|
|
65
|
-
exit 0
|
package/scripts/dev-link.mjs
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* dev-link.mjs — Link/unlink local plugin for development
|
|
4
|
-
*
|
|
5
|
-
* Replaces the installed plugin cache directory with a symlink to the local
|
|
6
|
-
* repo. Claude Code resolves CLAUDE_PLUGIN_ROOT from the original cache path,
|
|
7
|
-
* so we must symlink there (editing installed_plugins.json alone doesn't work).
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* npm run dev:link — replace cache dir with symlink to local repo
|
|
11
|
-
* npm run dev:unlink — remove symlink, reinstall from marketplace
|
|
12
|
-
* npm run dev:status — check current link state
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import fs from "fs";
|
|
16
|
-
import path from "path";
|
|
17
|
-
import os from "os";
|
|
18
|
-
import { execSync } from "child_process";
|
|
19
|
-
import { fileURLToPath } from "url";
|
|
20
|
-
|
|
21
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
-
const PLUGIN_ROOT = path.resolve(path.dirname(__filename), "..");
|
|
23
|
-
const PLUGIN_KEY = "claude-code-swarm@claude-code-swarm";
|
|
24
|
-
const MARKETPLACE_KEY = "claude-code-swarm";
|
|
25
|
-
const PLUGINS_DIR = path.join(os.homedir(), ".claude", "plugins");
|
|
26
|
-
const CACHE_DIR = path.join(PLUGINS_DIR, "cache");
|
|
27
|
-
const INSTALLED_PLUGINS_PATH = path.join(PLUGINS_DIR, "installed_plugins.json");
|
|
28
|
-
const MARKETPLACES_PATH = path.join(PLUGINS_DIR, "known_marketplaces.json");
|
|
29
|
-
|
|
30
|
-
const action = process.argv[2];
|
|
31
|
-
|
|
32
|
-
function readJson(filePath) {
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
35
|
-
} catch {
|
|
36
|
-
console.error(`Could not read ${filePath}`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getPluginEntry() {
|
|
42
|
-
const data = readJson(INSTALLED_PLUGINS_PATH);
|
|
43
|
-
const entries = data.plugins?.[PLUGIN_KEY];
|
|
44
|
-
if (!entries?.length) {
|
|
45
|
-
console.error(`Plugin "${PLUGIN_KEY}" not found in installed_plugins.json`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
return { data, entry: entries[0] };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getMarketplacePath() {
|
|
52
|
-
const data = readJson(MARKETPLACES_PATH);
|
|
53
|
-
const marketplace = data[MARKETPLACE_KEY];
|
|
54
|
-
if (!marketplace?.installLocation) {
|
|
55
|
-
console.error(`Marketplace "${MARKETPLACE_KEY}" not found in known_marketplaces.json`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
return marketplace.installLocation;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isSymlink(p) {
|
|
62
|
-
try {
|
|
63
|
-
return fs.lstatSync(p).isSymbolicLink();
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Get the cache path that Claude Code actually uses for CLAUDE_PLUGIN_ROOT.
|
|
71
|
-
* This is the installPath from installed_plugins.json (usually under ~/.claude/plugins/cache/).
|
|
72
|
-
*/
|
|
73
|
-
function getCachePath() {
|
|
74
|
-
const { entry } = getPluginEntry();
|
|
75
|
-
// If installPath was already changed to local repo, look for the original
|
|
76
|
-
if (entry.installPath === PLUGIN_ROOT && entry._originalInstallPath) {
|
|
77
|
-
return entry._originalInstallPath;
|
|
78
|
-
}
|
|
79
|
-
return entry.installPath;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function link() {
|
|
83
|
-
const cachePath = getCachePath();
|
|
84
|
-
|
|
85
|
-
if (isSymlink(cachePath)) {
|
|
86
|
-
const target = fs.readlinkSync(cachePath);
|
|
87
|
-
if (target === PLUGIN_ROOT) {
|
|
88
|
-
console.log(`Already linked: ${cachePath} → ${PLUGIN_ROOT}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.error(`Already symlinked to a different target: ${target}`);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Remove the cached copy
|
|
96
|
-
if (fs.existsSync(cachePath)) {
|
|
97
|
-
fs.rmSync(cachePath, { recursive: true, force: true });
|
|
98
|
-
console.log(`Removed cached copy: ${cachePath}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Ensure parent directory exists
|
|
102
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
103
|
-
|
|
104
|
-
// Create symlink
|
|
105
|
-
fs.symlinkSync(PLUGIN_ROOT, cachePath, "dir");
|
|
106
|
-
console.log(`Linked: ${cachePath} → ${PLUGIN_ROOT}`);
|
|
107
|
-
console.log("\nRestart Claude Code for changes to take effect.");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function unlink() {
|
|
111
|
-
const cachePath = getCachePath();
|
|
112
|
-
|
|
113
|
-
if (!isSymlink(cachePath)) {
|
|
114
|
-
console.log(`Not linked (${cachePath} is not a symlink)`);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Remove symlink
|
|
119
|
-
fs.unlinkSync(cachePath);
|
|
120
|
-
console.log(`Removed symlink: ${cachePath}`);
|
|
121
|
-
|
|
122
|
-
// Reinstall from marketplace clone
|
|
123
|
-
const marketplacePath = getMarketplacePath();
|
|
124
|
-
const pluginDir = path.join(marketplacePath, ".claude-plugin");
|
|
125
|
-
|
|
126
|
-
if (!fs.existsSync(pluginDir)) {
|
|
127
|
-
console.error(`Marketplace clone missing .claude-plugin/ at ${marketplacePath}`);
|
|
128
|
-
console.error("Run 'claude plugins update' to reinstall.");
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Copy marketplace clone to cache
|
|
133
|
-
fs.cpSync(marketplacePath, cachePath, { recursive: true });
|
|
134
|
-
|
|
135
|
-
// Install production deps
|
|
136
|
-
try {
|
|
137
|
-
execSync("npm install --production --ignore-scripts", {
|
|
138
|
-
cwd: cachePath,
|
|
139
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
140
|
-
});
|
|
141
|
-
} catch {
|
|
142
|
-
console.warn("Warning: npm install failed, plugin may not work correctly");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const pkgJson = readJson(path.join(cachePath, "package.json"));
|
|
146
|
-
console.log(`Unlinked: reinstalled from marketplace at ${cachePath} (v${pkgJson.version})`);
|
|
147
|
-
console.log("\nRestart Claude Code for changes to take effect.");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function status() {
|
|
151
|
-
const cachePath = getCachePath();
|
|
152
|
-
|
|
153
|
-
if (isSymlink(cachePath)) {
|
|
154
|
-
const target = fs.readlinkSync(cachePath);
|
|
155
|
-
console.log(`LINKED: ${cachePath} → ${target}`);
|
|
156
|
-
} else if (fs.existsSync(cachePath)) {
|
|
157
|
-
const pkgPath = path.join(cachePath, "package.json");
|
|
158
|
-
const version = fs.existsSync(pkgPath) ? readJson(pkgPath).version : "?";
|
|
159
|
-
console.log(`NOT LINKED: using cached copy at ${cachePath} (v${version})`);
|
|
160
|
-
} else {
|
|
161
|
-
console.log(`MISSING: ${cachePath} does not exist`);
|
|
162
|
-
console.log(` Run 'npm run dev:link' to create symlink.`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
switch (action) {
|
|
167
|
-
case "link":
|
|
168
|
-
link();
|
|
169
|
-
break;
|
|
170
|
-
case "unlink":
|
|
171
|
-
unlink();
|
|
172
|
-
break;
|
|
173
|
-
case "status":
|
|
174
|
-
status();
|
|
175
|
-
break;
|
|
176
|
-
default:
|
|
177
|
-
console.error("Usage: dev-link.mjs <link|unlink|status>");
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|