@ganglia/cli 0.9.190 → 0.9.192

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/bin/gng.js +167 -2
  2. package/package.json +3 -3
package/bin/gng.js CHANGED
@@ -6,8 +6,8 @@
6
6
  // silently. At runtime we locate that package and spawn its binary.
7
7
 
8
8
  const { spawnSync } = require('child_process');
9
- const path = require('path');
10
9
 
10
+ const WRAPPER_VERSION = require('../package.json').version;
11
11
  const PLATFORM = `${process.platform}-${process.arch}`;
12
12
 
13
13
  const PKG = {
@@ -41,6 +41,171 @@ try {
41
41
  process.exit(1);
42
42
  }
43
43
 
44
+ const argv = process.argv.slice(2);
45
+
46
+ // ── Stale-binary notice ────────────────────────────────────────────────────
47
+ // The wrapper is pure JS and republishes on every release; a platform package
48
+ // only republishes when that platform's binary is rebuilt (today: linux). So
49
+ // `npx @ganglia/cli@latest` can hand a user a wrapper at X and a binary from
50
+ // an older release — which reads as "npm keeps installing the old version"
51
+ // with nothing on screen to explain it. Say it out loud, once, on the
52
+ // commands a human is watching (never on the stdio protocols).
53
+ const PROTOCOL_CMDS = new Set(['mcp', 'serve', 'daemon', 'watch']);
54
+ let binaryVersion = null;
55
+ try {
56
+ binaryVersion = require(`${PKG}/package.json`).version;
57
+ } catch (_) { /* version unknown — skip the notice */ }
58
+
59
+ if (binaryVersion && binaryVersion !== WRAPPER_VERSION && !PROTOCOL_CMDS.has(argv[0])) {
60
+ console.error(
61
+ `[ganglia] ${PKG} is at ${binaryVersion}, but @ganglia/cli is ${WRAPPER_VERSION} — ` +
62
+ `no ${WRAPPER_VERSION} binary has been published for ${PLATFORM} yet, so \`gng --version\` ` +
63
+ `reports ${binaryVersion}.`,
64
+ );
65
+ }
66
+
67
+ // ── install / uninstall: register through the claude CLI from here ─────────
68
+ // `gng install` shells out to `claude mcp add`, and where `--scope` may sit
69
+ // in that command line is not stable across claude releases: 2.1.x parses
70
+ // `add <name> <cmd> --scope user -- <args>` but rejects the same flag placed
71
+ // before the positionals ("error: unknown option '--scope'") whenever a `--`
72
+ // tail is present; other builds want the flags first. The binary is the part
73
+ // we can't rebuild for every platform, so own the registration here and try
74
+ // both orders instead of betting the install on one of them.
75
+ if ((argv[0] === 'install' || argv[0] === 'uninstall') && !argv.includes('-h') && !argv.includes('--help')) {
76
+ process.exit(argv[0] === 'install' ? runInstall(argv) : runUninstall(argv));
77
+ }
78
+
79
+ function optValue(args, name, fallback) {
80
+ const i = args.indexOf(`--${name}`);
81
+ if (i !== -1 && args[i + 1] !== undefined) return args[i + 1];
82
+ const inline = args.find((a) => a.startsWith(`--${name}=`));
83
+ return inline ? inline.slice(name.length + 3) : fallback;
84
+ }
85
+
86
+ function claude(args) {
87
+ return spawnSync('claude', args, { encoding: 'utf8', env: process.env });
88
+ }
89
+
90
+ function claudeMissing(r) {
91
+ return r.error && r.error.code === 'ENOENT';
92
+ }
93
+
94
+ function help(args) {
95
+ const r = spawnSync(binary, args, { encoding: 'utf8', env: process.env });
96
+ return `${r.stdout || ''}${r.stderr || ''}`;
97
+ }
98
+
99
+ // The binary's own MCP entry point: older builds took `--redis-port`, current
100
+ // ones `--graph-port`. Ask the binary we are about to register which it knows.
101
+ function portFlag() {
102
+ return help(['--help']).includes('--graph-port') ? '--graph-port' : '--redis-port';
103
+ }
104
+
105
+ // Path form: current builds take the project dir positionally, older ones only
106
+ // via `-p`. Prefer the positional — `-p` anywhere on the line breaks claude's
107
+ // own parse (see below).
108
+ function pathArgs() {
109
+ return /Usage: gng mcp .*\[PATH\]/.test(help(['mcp', '--help'])) ? ['.'] : ['-p', '.'];
110
+ }
111
+
112
+ function runInstall(args) {
113
+ const global = args.includes('--global');
114
+ const port = optValue(args, 'redis-port', '6381');
115
+ const compress = optValue(args, 'compress', 'smart');
116
+ const scopeArgs = global ? ['--scope', 'user'] : [];
117
+
118
+ console.log('');
119
+ console.log(' Ganglia · MCP Install');
120
+ console.log('');
121
+ console.log(` scope ${global ? 'global (all projects)' : 'current project'}`);
122
+ console.log(` compression ${compress}`);
123
+ console.log('');
124
+
125
+ // Drop any existing entry first — the current name and the legacy
126
+ // "code-graph" one — so re-running install is idempotent.
127
+ for (const name of ['ganglia', 'code-graph']) {
128
+ const r = claude(['mcp', 'remove', ...scopeArgs, name]);
129
+ if (claudeMissing(r)) return notFound();
130
+ }
131
+
132
+ // No `-p` in the server's own args: claude scans the whole command line for
133
+ // its global `-p/--print` flag before `mcp add` parses, and that re-parse
134
+ // rejects our `--scope` wherever it sits. `gng mcp` takes the project path
135
+ // positionally (default "."), so a bare `.` does the same job.
136
+ const serverArgs = ['--', portFlag(), String(port), 'mcp', ...pathArgs(), '--compress', compress];
137
+
138
+ // Order 1 works on claude 2.1.x; order 2 is what older builds expect.
139
+ const forms = [
140
+ ['mcp', 'add', 'ganglia', binary, ...scopeArgs, ...serverArgs],
141
+ ['mcp', 'add', ...scopeArgs, 'ganglia', binary, ...serverArgs],
142
+ ];
143
+
144
+ let r;
145
+ let ok = false;
146
+ for (const form of forms) {
147
+ r = claude(form);
148
+ if (claudeMissing(r)) return notFound();
149
+ if (r.status !== 0) continue;
150
+ // A zero exit isn't proof: a CLI that took the flag positionally echoes it
151
+ // back as part of the server's command line, which means it landed in the
152
+ // args instead of setting the scope. Undo that and try the other order.
153
+ if (`${r.stdout || ''}`.includes('--scope')) {
154
+ claude(['mcp', 'remove', ...scopeArgs, 'ganglia']);
155
+ continue;
156
+ }
157
+ ok = true;
158
+ break;
159
+ }
160
+
161
+ if (!ok) {
162
+ const err = `${r.stdout || ''}${r.stderr || ''}`.trim();
163
+ console.error(` ✗ Installation failed: ${err}`);
164
+ console.error('');
165
+ console.error(' Register the server directly instead (writes ~/.claude.json):');
166
+ console.error(` ${binary} register`);
167
+ console.error('');
168
+ return 1;
169
+ }
170
+
171
+ console.log(' ✓ MCP server registered (ganglia)');
172
+ console.log('');
173
+ console.log(' Restart your AI client to activate.');
174
+ console.log(' Then: cd your-project && gng init (installs per-project hooks)');
175
+ console.log('');
176
+ return 0;
177
+ }
178
+
179
+ function runUninstall(args) {
180
+ const global = args.includes('--global');
181
+ const scopeArgs = global ? ['--scope', 'user'] : [];
182
+
183
+ console.log('Uninstalling the Ganglia MCP server...');
184
+ console.log(` Scope: ${global ? 'global' : 'current project'}`);
185
+
186
+ let removed = false;
187
+ for (const name of ['ganglia', 'code-graph']) {
188
+ const r = claude(['mcp', 'remove', ...scopeArgs, name]);
189
+ if (claudeMissing(r)) return notFound();
190
+ if (r.status === 0) removed = true;
191
+ }
192
+
193
+ console.log(removed
194
+ ? '\n✓ MCP server uninstalled.'
195
+ : '\n✓ MCP server was not registered (nothing to remove).');
196
+
197
+ // Per-project hooks are the binary's business — it knows which files it
198
+ // wrote. Best-effort; an old binary without the flag just no-ops.
199
+ spawnSync(binary, ['hooks', '--off'], { stdio: 'ignore', env: process.env });
200
+ return 0;
201
+ }
202
+
203
+ function notFound() {
204
+ console.error(" ✗ 'claude' not found — install the CLI and add it to PATH.");
205
+ console.error('');
206
+ return 1;
207
+ }
208
+
44
209
  // spawnSync is used intentionally:
45
210
  // - It blocks the Node process synchronously, so there is no event loop
46
211
  // running and no async child/close event handlers can fire.
@@ -49,7 +214,7 @@ try {
49
214
  // the Rust process, which reaps it via libc::waitpid in a background thread.
50
215
  // - Do NOT switch to spawn() with a close-handler; that would make the
51
216
  // wrapper exit when any grandchild exits, collapsing the MCP stdio bridge.
52
- const result = spawnSync(binary, process.argv.slice(2), {
217
+ const result = spawnSync(binary, argv, {
53
218
  stdio: 'inherit',
54
219
  env: process.env,
55
220
  windowsHide: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglia/cli",
3
- "version": "0.9.190",
3
+ "version": "0.9.192",
4
4
  "description": "Ganglia — MCP tools + Code Mode for every major AI coding assistant (Claude Code, Cursor, Windsurf, Cline, OpenCode, Continue, Zed, Codex CLI, Gemini CLI)",
5
5
  "keywords": [
6
6
  "mcp",
@@ -37,8 +37,8 @@
37
37
  "arm64"
38
38
  ],
39
39
  "optionalDependencies": {
40
- "@ganglia/cli-linux-x64": "0.9.190",
41
- "@ganglia/cli-linux-arm64": "0.9.190",
40
+ "@ganglia/cli-linux-x64": "0.9.192",
41
+ "@ganglia/cli-linux-arm64": "0.9.192",
42
42
  "@ganglia/cli-darwin-x64": "0.9.78",
43
43
  "@ganglia/cli-darwin-arm64": "0.9.78",
44
44
  "@ganglia/cli-win32-x64": "0.9.78"