@firedmosquito831/my-claude-code 6.63.0 → 6.64.0

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.
@@ -63,6 +63,18 @@ function skipReason(env) {
63
63
  }
64
64
 
65
65
  async function main() {
66
+ // Take back the `my-claude-code` command this package published up to
67
+ // 6.63.0, on EVERY global install -- before the opt-outs, because this is a
68
+ // migration and not an install. It is a console script of the wheel, and on
69
+ // Windows npm's global bin sits ahead of the uv tool bin directory on PATH,
70
+ // so the shim shadowed the real launcher and made `install.ps1` refuse to
71
+ // verify an install that had in fact worked. Somebody who sets
72
+ // MCC_NPM_SKIP_INSTALL=1 still wants the broken shim gone; npm does not
73
+ // reliably reap a bin its package has stopped declaring.
74
+ if (process.env.npm_config_global === "true") {
75
+ runtime.removeStaleGlobalShim({ log: note });
76
+ }
77
+
66
78
  const reason = skipReason(process.env);
67
79
  if (reason !== null) {
68
80
  note(reason);
@@ -287,6 +287,173 @@ function windowsInstallArgv(installer, directory) {
287
287
  return { command: installer, args: argv };
288
288
  }
289
289
 
290
+ /**
291
+ * The name this package used to publish, and must never publish again.
292
+ *
293
+ * `my-claude-code` is a console script of the WHEEL (`[project.scripts]`), and
294
+ * on Windows npm's global bin directory precedes `~/.local/bin` on PATH. So an
295
+ * `npm install -g` of 6.53.1 through 6.63.0 left `%APPDATA%\npm\my-claude-code.cmd`
296
+ * sitting in front of the real launcher -- and `install.ps1` verified its
297
+ * launchers by asking PATH, resolved npm's shim, concluded that a complete
298
+ * install had put its files somewhere illegal, and threw. Permanently: every
299
+ * later run of the one-liner failed the same way, on a machine where nothing
300
+ * was wrong.
301
+ *
302
+ * 6.64.0 publishes one bin, `mcc`, which no wheel entry point claims. This
303
+ * removes the leftover from the earlier versions, because npm does not
304
+ * reliably reap a bin its package stopped declaring, and because the machines
305
+ * that need it most are exactly the ones already broken.
306
+ */
307
+ const STALE_BIN_NAME = "my-claude-code";
308
+ const PACKAGE_NAME = "@firedmosquito831/my-claude-code";
309
+
310
+ /** npm's global bin directory, or null when this is not a global install. */
311
+ function npmGlobalBinDirectory(env, platform) {
312
+ const prefix = env.npm_config_global_prefix || env.npm_config_prefix;
313
+ if (!prefix) return null;
314
+ // On Windows the prefix IS the directory holding the shims; everywhere else
315
+ // they are in bin/ under it.
316
+ return platform === "win32" ? prefix : path.join(prefix, "bin");
317
+ }
318
+
319
+ /** Every shape npm writes a global bin in, for one name. */
320
+ function globalShimPaths(binDir, name, platform) {
321
+ if (platform === "win32") {
322
+ return [
323
+ path.join(binDir, name),
324
+ path.join(binDir, `${name}.cmd`),
325
+ path.join(binDir, `${name}.ps1`),
326
+ ];
327
+ }
328
+ return [path.join(binDir, name)];
329
+ }
330
+
331
+ /**
332
+ * Is this file a shim npm wrote for THIS package?
333
+ *
334
+ * A symlink's target and a .cmd shim's text both name the package directory
335
+ * they point into, so the check is the package name -- never the file name.
336
+ * Deleting a `my-claude-code` that belongs to somebody else would be exactly
337
+ * the kind of damage this whole change exists to stop.
338
+ */
339
+ function isShimForThisPackage(file) {
340
+ let text = null;
341
+ try {
342
+ const stat = fs.lstatSync(file);
343
+ text = stat.isSymbolicLink() ? fs.readlinkSync(file) : fs.readFileSync(file, "utf8");
344
+ } catch {
345
+ return false;
346
+ }
347
+ if (typeof text !== "string") return false;
348
+ return text.replace(/\\/g, "/").includes(PACKAGE_NAME);
349
+ }
350
+
351
+ /** Remove the `my-claude-code` shim an earlier version of this package left. */
352
+ function removeStaleGlobalShim(options) {
353
+ const env = options.env ?? process.env;
354
+ const platform = options.platform ?? process.platform;
355
+ const log = options.log ?? ((message) => console.log(`my-claude-code: ${message}`));
356
+ const binDir = options.binDir ?? npmGlobalBinDirectory(env, platform);
357
+ if (!binDir) return [];
358
+
359
+ const removed = [];
360
+ for (const file of globalShimPaths(binDir, STALE_BIN_NAME, platform)) {
361
+ if (!isShimForThisPackage(file)) continue;
362
+ try {
363
+ fs.rmSync(file, { force: true });
364
+ removed.push(file);
365
+ } catch (error) {
366
+ const message = error instanceof Error ? error.message : String(error);
367
+ log(
368
+ `could not remove the old ${STALE_BIN_NAME} command at ${file}: ${message}. ` +
369
+ `Remove it by hand, or run \`npm uninstall -g ${PACKAGE_NAME}\` and install again.`
370
+ );
371
+ }
372
+ }
373
+ if (removed.length > 0) {
374
+ log(
375
+ `removed the old ${STALE_BIN_NAME} command this package used to publish (${removed.join(", ")}). ` +
376
+ "It shadowed the launcher the installer provides, which is what made the installer refuse to verify itself."
377
+ );
378
+ }
379
+ return removed;
380
+ }
381
+
382
+ /**
383
+ * Where the installer's full output is kept for a hook that cannot show it,
384
+ * or null when nothing here can be written.
385
+ *
386
+ * `os.tmpdir()` is a guess -- it reads TMPDIR/TEMP/TMP and falls back to the
387
+ * system directory, and on a Windows runner with a stripped environment that
388
+ * fallback was `C:\Windows\temp`, which did not exist. So the directory is
389
+ * created and the path is probed HERE, where a failure is a missing log and
390
+ * not a crashed install.
391
+ */
392
+ function installerLogPath() {
393
+ const candidate = path.join(os.tmpdir(), `mcc-install-${process.pid}.log`);
394
+ try {
395
+ fs.mkdirSync(path.dirname(candidate), { recursive: true });
396
+ fs.writeFileSync(candidate, "");
397
+ return candidate;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Run the official installer, streaming its output into a log file.
405
+ *
406
+ * `npm install -g` swallows a postinstall's stdout, and on failure replays the
407
+ * WHOLE captured stream under an `npm error` prefix -- which is how a fresh
408
+ * machine came to read a page of ordinary uv progress as if every line were an
409
+ * error. So the full text goes to a file, only the installer's own step lines
410
+ * (`==> ...`) and its warnings reach the console, and the failure message
411
+ * names both the file and `--foreground-scripts`.
412
+ */
413
+ function runInstallerLogged(command, args, log, logPath) {
414
+ return new Promise((resolve, reject) => {
415
+ let stream = null;
416
+ if (logPath) {
417
+ try {
418
+ stream = fs.createWriteStream(logPath, { flags: "a" });
419
+ // A WriteStream reports a failed open ASYNCHRONOUSLY, as an 'error'
420
+ // event -- and an unhandled 'error' on a stream throws out of the
421
+ // event loop and kills the process. That is a log file taking an
422
+ // install down with it, which is the opposite of the point.
423
+ stream.on("error", () => {
424
+ stream = null;
425
+ });
426
+ } catch {
427
+ stream = null;
428
+ }
429
+ }
430
+ const child = childProcess.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
431
+ let pending = "";
432
+ const consume = (chunk) => {
433
+ const text = chunk.toString();
434
+ if (stream) stream.write(text);
435
+ pending += text;
436
+ const lines = pending.split(/\r?\n/);
437
+ pending = lines.pop() ?? "";
438
+ for (const line of lines) {
439
+ if (line.startsWith("==> ") || line.startsWith("WARNING:")) {
440
+ log(line);
441
+ }
442
+ }
443
+ };
444
+ child.stdout.on("data", consume);
445
+ child.stderr.on("data", consume);
446
+ child.on("error", (error) => {
447
+ if (stream) stream.end();
448
+ reject(new Error(`could not run ${command}: ${error.message}`));
449
+ });
450
+ child.on("close", (code) => {
451
+ if (stream) stream.end();
452
+ resolve(code ?? 1);
453
+ });
454
+ });
455
+ }
456
+
290
457
  function run(command, args, options) {
291
458
  const result = childProcess.spawnSync(command, args, { stdio: "inherit", ...options });
292
459
  if (result.error) throw new Error(`could not run ${command}: ${result.error.message}`);
@@ -425,10 +592,28 @@ async function performInstall(options) {
425
592
 
426
593
  if (decision.server) {
427
594
  const { command, args } = serverInstallerCommand(platform, decision.desktopFlag);
428
- const status = run(command, args);
595
+ const logPath = options.logPath ?? installerLogPath();
596
+ log(
597
+ logPath
598
+ ? `installing the server; the installer's full output goes to ${logPath}`
599
+ : "installing the server (no writable temporary directory, so the full output is not being kept)"
600
+ );
601
+ const status = await runInstallerLogged(command, args, log, logPath);
429
602
  if (status !== 0) {
603
+ // Say which half landed. This used to claim "Nothing was left
604
+ // half-installed by this hook" unconditionally -- and on the machine
605
+ // that prompted this change the server WAS installed and the desktop app
606
+ // was not, so the one sentence the user had was the false one.
430
607
  console.error(
431
- `my-claude-code: the installer exited ${status}. Nothing was left half-installed by this hook; rerun \`mcc install\` or install manually from https://github.com/FiredMosquito831/my-claude-code`
608
+ `my-claude-code: the server installer exited ${status}.\n` +
609
+ `my-claude-code: the desktop app was not attempted. The server may be partly installed: check with \`mcc-server --version\`, ` +
610
+ "and the installer names any command that is missing.\n" +
611
+ (logPath
612
+ ? `my-claude-code: the full output is in ${logPath}.\n`
613
+ : "my-claude-code: the full output could not be kept (no writable temporary directory).\n") +
614
+ "my-claude-code: retry the server with `npx @firedmosquito831/my-claude-code install --server-only`, the app with " +
615
+ "`npx @firedmosquito831/my-claude-code install --desktop-only`, or rerun " +
616
+ "`npm install -g --foreground-scripts @firedmosquito831/my-claude-code` to watch the installer live."
432
617
  );
433
618
  return status;
434
619
  }
@@ -465,7 +650,15 @@ async function performInstall(options) {
465
650
 
466
651
  module.exports = {
467
652
  DESKTOP_ASSETS,
653
+ PACKAGE_NAME,
468
654
  REPO_RAW,
655
+ STALE_BIN_NAME,
656
+ globalShimPaths,
657
+ installerLogPath,
658
+ isShimForThisPackage,
659
+ npmGlobalBinDirectory,
660
+ removeStaleGlobalShim,
661
+ runInstallerLogged,
469
662
  performInstall,
470
663
  serverInstallerCommand,
471
664
  HELP,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firedmosquito831/my-claude-code",
3
- "version": "6.63.0",
3
+ "version": "6.64.0",
4
4
  "description": "My Claude Code (MCC): route Claude Code and other coding agents to any model provider through one local proxy with a dashboard. This npm package installs and launches the Python server.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "author": "FiredMosquito831",
@@ -28,7 +28,6 @@
28
28
  "postinstall": "node bin/postinstall.js"
29
29
  },
30
30
  "bin": {
31
- "my-claude-code": "bin/my-claude-code.js",
32
31
  "mcc": "bin/my-claude-code.js"
33
32
  },
34
33
  "files": [