@sdsrs/code-graph 0.4.3 → 0.4.5

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,6 +4,6 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.4.3",
7
+ "version": "0.4.5",
8
8
  "keywords": ["code-graph", "ast", "navigation", "mcp", "knowledge-graph"]
9
9
  }
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const { execSync } = require('child_process');
3
+ const { execFileSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const { CACHE_DIR, PLUGIN_ID, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
7
+ const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
8
8
 
9
9
  // ── Configuration ──────────────────────────────────────────
10
10
  const GITHUB_REPO = 'sdsrss/code-graph-mcp';
@@ -87,6 +87,21 @@ async function fetchLatestRelease() {
87
87
  } catch { return null; }
88
88
  }
89
89
 
90
+ // ── Helpers ────────────────────────────────────────────────
91
+
92
+ function copyDirSync(src, dst) {
93
+ fs.mkdirSync(dst, { recursive: true });
94
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
95
+ const srcPath = path.join(src, entry.name);
96
+ const dstPath = path.join(dst, entry.name);
97
+ if (entry.isDirectory()) {
98
+ copyDirSync(srcPath, dstPath);
99
+ } else {
100
+ fs.copyFileSync(srcPath, dstPath);
101
+ }
102
+ }
103
+ }
104
+
90
105
  // ── Download & Install ─────────────────────────────────────
91
106
 
92
107
  async function downloadAndInstall(latest) {
@@ -94,25 +109,31 @@ async function downloadAndInstall(latest) {
94
109
  try {
95
110
  fs.mkdirSync(tmpDir, { recursive: true });
96
111
 
97
- // 1. Download and extract tarball (plugin files)
98
- execSync(
99
- `curl -sL -H "Accept: application/vnd.github+json" "${latest.tarballUrl}" | tar xz -C "${tmpDir}" --strip-components=1`,
100
- { timeout: 30000, stdio: 'pipe' }
101
- );
112
+ // 1. Download tarball (safe: no shell interpolation)
113
+ const tarballPath = path.join(tmpDir, 'release.tar.gz');
114
+ execFileSync('curl', [
115
+ '-sL', '-o', tarballPath,
116
+ '-H', 'Accept: application/vnd.github+json',
117
+ latest.tarballUrl,
118
+ ], { timeout: 30000, stdio: 'pipe' });
119
+
120
+ // 2. Extract tarball
121
+ execFileSync('tar', [
122
+ 'xzf', tarballPath, '-C', tmpDir, '--strip-components=1',
123
+ ], { timeout: 15000, stdio: 'pipe' });
102
124
 
103
- // 2. Copy plugin files to cache
125
+ // 3. Copy plugin files to cache (cross-platform)
104
126
  const pluginSrc = path.join(tmpDir, 'claude-plugin');
105
127
  const pluginDst = path.join(
106
- os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph', latest.version
128
+ os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, 'code-graph', latest.version
107
129
  );
108
130
 
109
131
  if (fs.existsSync(pluginSrc)) {
110
132
  fs.mkdirSync(pluginDst, { recursive: true });
111
- // Copy recursively
112
- execSync(`cp -r "${pluginSrc}/." "${pluginDst}/"`, { stdio: 'pipe' });
133
+ copyDirSync(pluginSrc, pluginDst);
113
134
  }
114
135
 
115
- // 3. Update installed_plugins.json to point to new version
136
+ // 4. Update installed_plugins.json to point to new version
116
137
  const installedPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
117
138
  try {
118
139
  const installed = readJson(installedPath);
@@ -124,7 +145,7 @@ async function downloadAndInstall(latest) {
124
145
  }
125
146
  } catch { /* installed_plugins update failed — not fatal */ }
126
147
 
127
- // 4. Update install manifest with tag version
148
+ // 5. Update install manifest with tag version
128
149
  try {
129
150
  const manifest = readManifest();
130
151
  manifest.version = latest.version;
@@ -132,9 +153,9 @@ async function downloadAndInstall(latest) {
132
153
  writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
133
154
  } catch { /* manifest update failed — not fatal */ }
134
155
 
135
- // 5. Update npm binary (non-blocking, best-effort)
156
+ // 6. Update npm binary (non-blocking, best-effort)
136
157
  try {
137
- execSync(`npm install -g ${NPM_PACKAGE}@${latest.version}`, {
158
+ execFileSync('npm', ['install', '-g', `${NPM_PACKAGE}@${latest.version}`], {
138
159
  timeout: 60000,
139
160
  stdio: 'pipe',
140
161
  });
@@ -4,7 +4,9 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
- const PLUGIN_ID = 'code-graph@sdsrss';
7
+ const PLUGIN_ID = 'code-graph@sdsrss-code-graph';
8
+ const OLD_PLUGIN_ID = 'code-graph@sdsrss'; // Legacy ID — kept for migration cleanup
9
+ const MARKETPLACE_NAME = 'sdsrss-code-graph';
8
10
  const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
9
11
  const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..');
10
12
  const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
@@ -88,6 +90,45 @@ function unregisterStatuslineProvider(id) {
88
90
  return true;
89
91
  }
90
92
 
93
+ // --- Scope Conflict Detection ---
94
+
95
+ function checkScopeConflict() {
96
+ const installed = readJson(INSTALLED_PLUGINS_PATH);
97
+ if (!installed || !installed.plugins) return null;
98
+ for (const [key, entries] of Object.entries(installed.plugins)) {
99
+ if (key === PLUGIN_ID) continue;
100
+ if (key.startsWith('code-graph@')) {
101
+ return { existingId: key, scope: entries[0] && entries[0].scope, entries };
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ // --- Migration: clean up old PLUGIN_ID remnants ---
108
+
109
+ function migrateOldPluginId(settings) {
110
+ let changed = false;
111
+
112
+ // Clean old ID from enabledPlugins
113
+ if (settings.enabledPlugins && OLD_PLUGIN_ID in settings.enabledPlugins) {
114
+ delete settings.enabledPlugins[OLD_PLUGIN_ID];
115
+ changed = true;
116
+ }
117
+
118
+ // Clean old ID from installed_plugins.json
119
+ const installed = readJson(INSTALLED_PLUGINS_PATH);
120
+ if (installed && installed.plugins && OLD_PLUGIN_ID in installed.plugins) {
121
+ delete installed.plugins[OLD_PLUGIN_ID];
122
+ writeJsonAtomic(INSTALLED_PLUGINS_PATH, installed);
123
+ }
124
+
125
+ // Clean old cache path (was using 'sdsrss' instead of 'sdsrss-code-graph')
126
+ const oldCacheDir = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph');
127
+ try { fs.rmSync(oldCacheDir, { recursive: true, force: true }); } catch { /* ok */ }
128
+
129
+ return changed;
130
+ }
131
+
91
132
  // --- Install (idempotent) ---
92
133
 
93
134
  function install() {
@@ -96,6 +137,11 @@ function install() {
96
137
  const settings = readJson(SETTINGS_PATH) || {};
97
138
  let settingsChanged = false;
98
139
 
140
+ // 0. Migrate from old PLUGIN_ID
141
+ if (migrateOldPluginId(settings)) {
142
+ settingsChanged = true;
143
+ }
144
+
99
145
  // 1. StatusLine — composite approach
100
146
  // a. Capture existing statusline as a provider (if not already composite)
101
147
  // b. Register code-graph as a provider
@@ -114,20 +160,16 @@ function install() {
114
160
  // Register code-graph provider
115
161
  registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
116
162
 
117
- // 2. enabledPlugins add if missing
118
- if (!settings.enabledPlugins) settings.enabledPlugins = {};
119
- if (!(PLUGIN_ID in settings.enabledPlugins)) {
120
- settings.enabledPlugins[PLUGIN_ID] = true;
121
- settingsChanged = true;
122
- manifest.config.enabledPlugins = true;
123
- }
163
+ // NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
164
+ // Do NOT add enabledPlugins entries here — it causes phantom plugin entries
165
+ // when the ID doesn't match the marketplace name.
124
166
 
125
- // 3. Write settings atomically if changed
167
+ // 2. Write settings atomically if changed
126
168
  if (settingsChanged) {
127
169
  writeJsonAtomic(SETTINGS_PATH, settings);
128
170
  }
129
171
 
130
- // 4. Write manifest with version
172
+ // 3. Write manifest with version
131
173
  manifest.version = version;
132
174
  manifest.installedAt = manifest.installedAt || new Date().toISOString();
133
175
  manifest.updatedAt = new Date().toISOString();
@@ -161,10 +203,14 @@ function uninstall() {
161
203
  // else: other providers still using composite — leave it
162
204
  }
163
205
 
164
- // 2. Remove from enabledPlugins
165
- if (settings.enabledPlugins && PLUGIN_ID in settings.enabledPlugins) {
166
- delete settings.enabledPlugins[PLUGIN_ID];
167
- settingsChanged = true;
206
+ // 2. Remove both old and new IDs from enabledPlugins
207
+ if (settings.enabledPlugins) {
208
+ for (const id of [PLUGIN_ID, OLD_PLUGIN_ID]) {
209
+ if (id in settings.enabledPlugins) {
210
+ delete settings.enabledPlugins[id];
211
+ settingsChanged = true;
212
+ }
213
+ }
168
214
  }
169
215
 
170
216
  // 3. Write settings if changed
@@ -173,19 +219,30 @@ function uninstall() {
173
219
  }
174
220
  }
175
221
 
176
- // 4. Remove from installed_plugins.json
222
+ // 4. Remove both old and new IDs from installed_plugins.json
177
223
  const installedPlugins = readJson(INSTALLED_PLUGINS_PATH);
178
- if (installedPlugins && installedPlugins.plugins && PLUGIN_ID in installedPlugins.plugins) {
179
- delete installedPlugins.plugins[PLUGIN_ID];
180
- writeJsonAtomic(INSTALLED_PLUGINS_PATH, installedPlugins);
224
+ if (installedPlugins && installedPlugins.plugins) {
225
+ let ipChanged = false;
226
+ for (const id of [PLUGIN_ID, OLD_PLUGIN_ID]) {
227
+ if (id in installedPlugins.plugins) {
228
+ delete installedPlugins.plugins[id];
229
+ ipChanged = true;
230
+ }
231
+ }
232
+ if (ipChanged) writeJsonAtomic(INSTALLED_PLUGINS_PATH, installedPlugins);
181
233
  }
182
234
 
183
235
  // 5. Remove cache directory
184
236
  try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
185
237
 
186
- // 6. Remove plugin files from cache
187
- const pluginCacheDir = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph');
188
- try { fs.rmSync(pluginCacheDir, { recursive: true, force: true }); } catch { /* ok */ }
238
+ // 6. Remove plugin files from cache (both old and new paths)
239
+ const pluginCacheDirs = [
240
+ path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, 'code-graph'),
241
+ path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph'), // legacy
242
+ ];
243
+ for (const dir of pluginCacheDirs) {
244
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ok */ }
245
+ }
189
246
 
190
247
  return { settingsChanged };
191
248
  }
@@ -199,6 +256,11 @@ function update() {
199
256
  const settings = readJson(SETTINGS_PATH) || {};
200
257
  let settingsChanged = false;
201
258
 
259
+ // 0. Migrate from old PLUGIN_ID
260
+ if (migrateOldPluginId(settings)) {
261
+ settingsChanged = true;
262
+ }
263
+
202
264
  // 1. Update composite command path if version changed
203
265
  if (isOurComposite(settings)) {
204
266
  const cmd = compositeCommand();
@@ -211,23 +273,18 @@ function update() {
211
273
  // 2. Update code-graph provider in registry
212
274
  registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
213
275
 
214
- // 3. Ensure enabledPlugins entry exists
215
- if (!settings.enabledPlugins) settings.enabledPlugins = {};
216
- if (!(PLUGIN_ID in settings.enabledPlugins)) {
217
- settings.enabledPlugins[PLUGIN_ID] = true;
218
- settingsChanged = true;
219
- }
276
+ // NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
220
277
 
221
- // 4. Write settings if changed
278
+ // 3. Write settings if changed
222
279
  if (settingsChanged) {
223
280
  writeJsonAtomic(SETTINGS_PATH, settings);
224
281
  }
225
282
 
226
- // 5. Clear update-check cache (force re-check after update)
283
+ // 4. Clear update-check cache (force re-check after update)
227
284
  const updateCache = path.join(CACHE_DIR, 'update-check');
228
285
  try { fs.unlinkSync(updateCache); } catch { /* ok */ }
229
286
 
230
- // 6. Update manifest
287
+ // 5. Update manifest
231
288
  manifest.version = version;
232
289
  manifest.updatedAt = new Date().toISOString();
233
290
  writeManifest(manifest);
@@ -236,11 +293,11 @@ function update() {
236
293
  }
237
294
 
238
295
  module.exports = {
239
- install, uninstall, update,
296
+ install, uninstall, update, checkScopeConflict,
240
297
  readManifest, readJson, writeJsonAtomic,
241
298
  readRegistry, writeRegistry,
242
299
  getPluginVersion,
243
- PLUGIN_ID, CACHE_DIR, REGISTRY_FILE,
300
+ PLUGIN_ID, OLD_PLUGIN_ID, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
244
301
  };
245
302
 
246
303
  // CLI: node lifecycle.js <install|uninstall|update>
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
  const { execFileSync } = require('child_process');
4
4
  const { findBinary } = require('./find-binary');
5
- const { install, update, readManifest, getPluginVersion } = require('./lifecycle');
5
+ const { install, update, readManifest, getPluginVersion, checkScopeConflict } = require('./lifecycle');
6
6
  const { checkForUpdate } = require('./auto-update');
7
7
 
8
8
  const BIN = findBinary();
@@ -18,7 +18,16 @@ if (BIN) {
18
18
  } catch { /* timeout — silent */ }
19
19
  }
20
20
 
21
- // --- 2. Lifecycle: install or update config (idempotent) ---
21
+ // --- 2. Scope conflict warning ---
22
+ const conflict = checkScopeConflict();
23
+ if (conflict) {
24
+ process.stderr.write(
25
+ `[code-graph] Warning: conflicting install detected — ${conflict.existingId} (${conflict.scope || 'unknown'} scope). ` +
26
+ `Use /plugin to remove one to avoid config conflicts.\n`
27
+ );
28
+ }
29
+
30
+ // --- 3. Lifecycle: install or update config (idempotent) ---
22
31
  const manifest = readManifest();
23
32
  const currentVersion = getPluginVersion();
24
33
 
@@ -28,7 +37,7 @@ if (!manifest.version) {
28
37
  update();
29
38
  }
30
39
 
31
- // --- 3. Auto-update (throttled, non-blocking) ---
40
+ // --- 4. Auto-update (throttled, non-blocking) ---
32
41
  (async () => {
33
42
  const result = await checkForUpdate();
34
43
  if (result && result.updated) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,10 +33,10 @@
33
33
  "node": ">=16"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@sdsrs/code-graph-linux-x64": "0.4.3",
37
- "@sdsrs/code-graph-linux-arm64": "0.4.3",
38
- "@sdsrs/code-graph-darwin-x64": "0.4.3",
39
- "@sdsrs/code-graph-darwin-arm64": "0.4.3",
40
- "@sdsrs/code-graph-win32-x64": "0.4.3"
36
+ "@sdsrs/code-graph-linux-x64": "0.4.5",
37
+ "@sdsrs/code-graph-linux-arm64": "0.4.5",
38
+ "@sdsrs/code-graph-darwin-x64": "0.4.5",
39
+ "@sdsrs/code-graph-darwin-arm64": "0.4.5",
40
+ "@sdsrs/code-graph-win32-x64": "0.4.5"
41
41
  }
42
42
  }