@firedmosquito831/my-claude-code 6.57.0 → 6.58.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.
package/README.md CHANGED
@@ -8,29 +8,71 @@ The server is a Python package. This npm package is a thin wrapper over the
8
8
  project's own digest-verified install script — it does not reimplement it and
9
9
  it does not vendor a second copy of the server.
10
10
 
11
- ## Install everything (server, every command, desktop app)
11
+ ## Install what this machine actually needs
12
12
 
13
13
  ```sh
14
14
  npm install -g @firedmosquito831/my-claude-code
15
15
  ```
16
16
 
17
- A global install runs the official installer with the desktop flag
18
- (`install.ps1 -Desktop` on Windows, `install.sh --desktop` elsewhere). When it
19
- finishes you have exactly what the one-line installer gives you: the server,
20
- every `mcc-*` command on your PATH, and the desktop app — plus `my-claude-code`
21
- and `mcc` as extra aliases from this package. If the installer fails, `npm
22
- install -g` fails with it; nothing is left half-installed.
17
+ A global install looks at the machine first and then installs the latest
18
+ release of the right shape:
23
19
 
24
- Skip that and take the launcher only with `MCC_NPM_SKIP_INSTALL=1 npm install -g
25
- @firedmosquito831/my-claude-code`. A local install, an `npx` run, `CI` being set,
26
- and `--ignore-scripts` all skip it too, each printing one line saying so.
20
+ - **the server, always**, through the official installer (`install.ps1` /
21
+ `install.sh`), which brings `uv`, Python 3.14 and every `mcc-*` command;
22
+ - **the native desktop application, where there is a desktop session** —
23
+ Windows and macOS always, Linux only with `DISPLAY` or `WAYLAND_DISPLAY`.
24
+ Over SSH, in CI, on a headless server or in WSL without a display it
25
+ installs the server alone, without even the installer's desktop flag, so no
26
+ shortcuts are created for a screen that does not exist.
27
+
28
+ It prints one line saying what it decided and why before it does anything.
29
+
30
+ | Platform | What the desktop half installs |
31
+ | --- | --- |
32
+ | Windows | `MyClaudeCode-Setup-windows-x86_64.exe` run `/VERYSILENT`, per-user, no UAC prompt |
33
+ | macOS | the universal `.dmg`, mounted with `hdiutil`, copied into `~/Applications`, quarantine cleared (it says so) |
34
+ | Linux with `dpkg` | the `.deb`, downloaded and verified — it **prints** `sudo dpkg -i …` rather than escalating, unless you pass `--yes-sudo` |
35
+ | Linux without `dpkg` | the tarball's own `install-desktop.sh`, per-user, no root |
36
+
37
+ Every one of those downloads is checked against
38
+ `SHA256SUMS-desktop-shell.txt` from the same release **before** it is run,
39
+ mounted or unpacked; a file whose digest does not match is deleted and nothing
40
+ is executed.
41
+
42
+ This is the OS-native shape on purpose. `mcc-desktop` also downloads the same
43
+ verified Tauri shell on first launch, into your config home — that needs no
44
+ installer and no root, but leaves no Start Menu entry, `.desktop` file or
45
+ Applications icon. Typing `npm install -g` asks for an installed application,
46
+ so that is what you get; `mcc-desktop` stays as the zero-install fallback.
47
+
48
+ ### Choosing for yourself
49
+
50
+ ```sh
51
+ npx @firedmosquito831/my-claude-code install --help # the list, installs nothing
52
+ npx @firedmosquito831/my-claude-code install --server-only
53
+ npx @firedmosquito831/my-claude-code install --desktop-only
54
+ npx @firedmosquito831/my-claude-code install --yes-sudo # Linux: run dpkg for me
55
+ MCC_NPM_INSTALL=server npm install -g @firedmosquito831/my-claude-code
56
+ ```
57
+
58
+ `MCC_NPM_INSTALL` takes `server`, `desktop`, `both` or `none` and is the
59
+ channel for Dockerfiles and provisioning scripts; a command-line flag beats it.
60
+ Skip the hook entirely and take the launcher only with
61
+ `MCC_NPM_SKIP_INSTALL=1 npm install -g @firedmosquito831/my-claude-code`. A
62
+ local install, an `npx` run, `CI` being set, and `--ignore-scripts` all skip it
63
+ too, each printing one line saying so.
64
+
65
+ If the server installs and the desktop half fails — a flaky network, a 404 —
66
+ the install stays green and tells you how to retry just that half. A failing
67
+ *server* install fails `npm install -g` with it; nothing is left
68
+ half-installed.
27
69
 
28
70
  ## Run without installing globally
29
71
 
30
72
  ```sh
31
73
  npx @firedmosquito831/my-claude-code # install if needed, then start the server
32
- npx @firedmosquito831/my-claude-code install # install or update only
33
- npx @firedmosquito831/my-claude-code desktop # start the desktop window
74
+ npx @firedmosquito831/my-claude-code install # install or update, deciding as above
75
+ npx @firedmosquito831/my-claude-code desktop # open the desktop app, installing it first if missing
34
76
  npx @firedmosquito831/my-claude-code claude # launch Claude Code through the proxy
35
77
  npx @firedmosquito831/my-claude-code help # list every mcc-* command
36
78
  ```
@@ -47,6 +89,10 @@ npm uninstall -g @firedmosquito831/my-claude-code # removes only this launcher
47
89
 
48
90
  Run `mcc uninstall` **first**: npm removes the launcher it installed and nothing
49
91
  else, so uninstalling the package on its own leaves the Python server in place.
92
+ The desktop application is uninstalled the way its platform expects — Add or
93
+ remove programs on Windows, deleting it from `~/Applications` on macOS,
94
+ `sudo dpkg -r my-claude-code-desktop` or `install-desktop.sh --uninstall` on
95
+ Linux.
50
96
 
51
97
  Requirements: Node 18+, and on Windows PowerShell 5.1+ (the installer brings
52
98
  `uv` and Python itself).
@@ -11,13 +11,19 @@
11
11
  // npx my-claude-code desktop -> install if needed, then run mcc-desktop
12
12
  // npx my-claude-code <mcc-cmd> -> any mcc-* command, e.g. `claude`, `help`
13
13
  // npx my-claude-code --version -> versions of this launcher and the server
14
+ //
15
+ // `install` is the only subcommand that decides anything: it asks
16
+ // `runtime-install.js` what this platform, architecture and session actually
17
+ // want and installs the latest release of that shape. `--version` still
18
+ // installs nothing at all.
14
19
 
15
- const { spawnSync } = require("node:child_process");
20
+ const childProcess = require("node:child_process");
16
21
  const os = require("node:os");
17
22
  const path = require("node:path");
18
23
 
24
+ const runtime = require("./runtime-install.js");
25
+
19
26
  const LAUNCHER_VERSION = require("../package.json").version;
20
- const REPO_RAW = "https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts";
21
27
  const IS_WINDOWS = process.platform === "win32";
22
28
 
23
29
  // Every published command lives in uv's tool bin dir. PATH may not carry it in
@@ -34,9 +40,9 @@ function candidateBinDirs() {
34
40
 
35
41
  function resolveCommand(name) {
36
42
  const exe = IS_WINDOWS ? `${name}.exe` : name;
37
- const probe = spawnSync(IS_WINDOWS ? "where" : "which", [exe], { encoding: "utf8" });
38
- if (probe.status === 0 && probe.stdout.trim()) {
39
- return probe.stdout.trim().split(/\r?\n/)[0];
43
+ const probe = childProcess.spawnSync(IS_WINDOWS ? "where" : "which", [exe], { encoding: "utf8" });
44
+ if (probe.status === 0 && String(probe.stdout ?? "").trim()) {
45
+ return String(probe.stdout).trim().split(/\r?\n/)[0];
40
46
  }
41
47
  const fs = require("node:fs");
42
48
  for (const dir of candidateBinDirs()) {
@@ -46,35 +52,16 @@ function resolveCommand(name) {
46
52
  return null;
47
53
  }
48
54
 
49
- function runInstaller() {
50
- console.error("my-claude-code: server not found, running the official installer...");
51
- let result;
52
- if (IS_WINDOWS) {
53
- const script = `& ([scriptblock]::Create((irm "${REPO_RAW}/install.ps1")))`;
54
- result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
55
- stdio: "inherit",
56
- });
57
- } else {
58
- result = spawnSync("sh", ["-c", `curl -fsSL "${REPO_RAW}/install.sh" | sh`], { stdio: "inherit" });
59
- }
60
- if (result.error) {
61
- console.error(`my-claude-code: could not start the installer: ${result.error.message}`);
62
- return 1;
63
- }
64
- return result.status ?? 1;
55
+ /** The runtime-aware install: server always, desktop app where there is one. */
56
+ function runInstaller(argv) {
57
+ console.error("my-claude-code: running the official installer...");
58
+ return runtime.performInstall({ argv: argv ?? [] });
65
59
  }
66
60
 
67
61
  function runUninstaller() {
68
62
  console.error("my-claude-code: running the official uninstaller (removes the server, its commands and the config home)...");
69
- let result;
70
- if (IS_WINDOWS) {
71
- const script = `& ([scriptblock]::Create((irm "${REPO_RAW}/uninstall.ps1")))`;
72
- result = spawnSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
73
- stdio: "inherit",
74
- });
75
- } else {
76
- result = spawnSync("sh", ["-c", `curl -fsSL "${REPO_RAW}/uninstall.sh" | sh`], { stdio: "inherit" });
77
- }
63
+ const { command, args } = runtime.serverInstallerCommand(process.platform, false, "uninstall");
64
+ const result = childProcess.spawnSync(command, args, { stdio: "inherit" });
78
65
  if (result.error) {
79
66
  console.error(`my-claude-code: could not start the uninstaller: ${result.error.message}`);
80
67
  return 1;
@@ -85,9 +72,9 @@ function runUninstaller() {
85
72
  const USAGE = `my-claude-code ${LAUNCHER_VERSION} -- launcher for the MCC proxy server and desktop app
86
73
 
87
74
  my-claude-code install the server if missing, then start it (mcc-server)
88
- my-claude-code desktop install if missing, then open the desktop app (mcc-desktop
89
- downloads the verified desktop shell on first launch)
90
- my-claude-code install install or update the server from the official installer
75
+ my-claude-code desktop install if missing, then open the desktop app
76
+ my-claude-code install install the latest release of whatever this machine
77
+ needs -- see \`install --help\` for the overrides
91
78
  my-claude-code uninstall remove the server, its commands and the config home
92
79
  my-claude-code <cmd> ... run any mcc-* command, e.g. claude, codex, opencode, help
93
80
  my-claude-code --version launcher and server versions
@@ -96,7 +83,7 @@ Everything else is passed to mcc-server unchanged. Installs use the digest-verif
96
83
  scripts from https://github.com/FiredMosquito831/my-claude-code`;
97
84
 
98
85
  function exec(command, args) {
99
- const result = spawnSync(command, args, { stdio: "inherit" });
86
+ const result = childProcess.spawnSync(command, args, { stdio: "inherit" });
100
87
  if (result.error) {
101
88
  console.error(`my-claude-code: could not run ${command}: ${result.error.message}`);
102
89
  return 1;
@@ -104,7 +91,7 @@ function exec(command, args) {
104
91
  return result.status ?? 1;
105
92
  }
106
93
 
107
- function main() {
94
+ async function main() {
108
95
  const argv = process.argv.slice(2);
109
96
  const first = argv[0];
110
97
 
@@ -116,13 +103,13 @@ function main() {
116
103
  return 0;
117
104
  }
118
105
 
119
- if (first === "--help" || first === "-h" || first === "help" && !resolveCommand("mcc-help")) {
106
+ if (first === "--help" || first === "-h" || (first === "help" && !resolveCommand("mcc-help"))) {
120
107
  console.log(USAGE);
121
108
  return 0;
122
109
  }
123
110
 
124
111
  if (first === "install" || first === "update") {
125
- return runInstaller();
112
+ return runInstaller(argv.slice(1));
126
113
  }
127
114
 
128
115
  if (first === "uninstall") {
@@ -133,14 +120,22 @@ function main() {
133
120
  // Anything that is not a known mcc-* command is an argument for mcc-server.
134
121
  let target = "mcc-server";
135
122
  let rest = argv;
136
- if (first && !first.startsWith("-") && resolveCommand(`mcc-${first}`)) {
123
+ if (first === "desktop") {
124
+ // Named explicitly rather than left to the probe below: on a machine with
125
+ // nothing installed `resolveCommand("mcc-desktop")` is null, and falling
126
+ // through would hand the word "desktop" to mcc-server as an argument.
127
+ target = "mcc-desktop";
128
+ rest = argv.slice(1);
129
+ } else if (first && !first.startsWith("-") && resolveCommand(`mcc-${first}`)) {
137
130
  target = `mcc-${first}`;
138
131
  rest = argv.slice(1);
139
132
  }
140
133
 
141
134
  let resolved = resolveCommand(target);
142
135
  if (!resolved) {
143
- const status = runInstaller();
136
+ // `desktop` on a machine with nothing installed means "install what this
137
+ // machine needs, then open the app" -- the same decision `install` makes.
138
+ const status = await runInstaller(first === "desktop" ? [] : ["--server-only"]);
144
139
  if (status !== 0) return status;
145
140
  resolved = resolveCommand(target);
146
141
  if (!resolved) {
@@ -151,4 +146,12 @@ function main() {
151
146
  return exec(resolved, rest);
152
147
  }
153
148
 
154
- process.exit(main());
149
+ main().then(
150
+ (status) => {
151
+ process.exitCode = status;
152
+ },
153
+ (error) => {
154
+ console.error(`my-claude-code: ${error && error.message}`);
155
+ process.exitCode = 1;
156
+ }
157
+ );
@@ -2,19 +2,29 @@
2
2
  "use strict";
3
3
 
4
4
  // `npm install -g @firedmosquito831/my-claude-code` must leave the machine in
5
- // the same state the one-line installer does -- the server, every `mcc-*`
6
- // command, and the desktop app -- and not merely drop a launcher that installs
7
- // on first use. That is what this hook is for, and it is deliberately the only
8
- // place in this package that installs anything.
5
+ // the state the one-line installer does -- the server, every `mcc-*` command,
6
+ // and, where there is a screen to draw on, the desktop application -- and not
7
+ // merely drop a launcher that installs on first use. That is what this hook is
8
+ // for, and it is deliberately the only place in this package that installs
9
+ // anything during `npm install`.
9
10
  //
10
- // It does NOT reimplement the installer. It runs the official, digest-verified
11
- // script from the repository with the desktop flag the installer already has
12
- // (`install.ps1 -Desktop`, `install.sh --desktop`), streams its output, and
13
- // exits with its status. A failed install therefore fails `npm install -g`,
14
- // which is the honest outcome: npm should not report success for a package
15
- // whose whole job is to put a server on your PATH.
11
+ // It does NOT reimplement the installer. The server half runs the repository's
12
+ // own digest-verified script (`install.ps1 -Desktop`, `install.sh --desktop`),
13
+ // streams its output, and exits with its status. A failed server install
14
+ // therefore fails `npm install -g`, which is the honest outcome: npm should
15
+ // not report success for a package whose whole job is to put a server on your
16
+ // PATH.
16
17
  //
17
- // Four situations must NOT install:
18
+ // What is new since 6.57.1 is that the hook no longer assumes a desktop. It
19
+ // asks `runtime-install.decide()` -- platform, arch, DISPLAY/WAYLAND_DISPLAY,
20
+ // SSH, CI, and the explicit overrides -- and on a VPS, in a container or in a
21
+ // WSL shell with no display it installs the server WITHOUT the `-Desktop`
22
+ // flag, so no shortcuts are created for a screen that does not exist. Where
23
+ // there is a desktop it also installs the OS-native application (setup.exe,
24
+ // .dmg into ~/Applications, .deb/tarball), each verified against the release's
25
+ // own SHA256SUMS-desktop-shell.txt before it is run.
26
+ //
27
+ // Four situations must NOT install at all:
18
28
  //
19
29
  // * a local `npm install` or an `npx` run (`npm_config_global` is not
20
30
  // "true"). `npx @firedmosquito831/my-claude-code --version` has to stay
@@ -29,11 +39,7 @@
29
39
  // Every one of them prints a single line saying what happened and why, then
30
40
  // exits 0. Silence here is indistinguishable from a broken hook.
31
41
 
32
- const { spawnSync } = require("node:child_process");
33
-
34
- const REPO_RAW =
35
- "https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts";
36
- const IS_WINDOWS = process.platform === "win32";
42
+ const runtime = require("./runtime-install.js");
37
43
 
38
44
  function note(message) {
39
45
  console.log(`my-claude-code: ${message}`);
@@ -56,31 +62,7 @@ function skipReason(env) {
56
62
  return null;
57
63
  }
58
64
 
59
- /** The installer command for this platform, with the desktop flag on. */
60
- function installerCommand() {
61
- if (IS_WINDOWS) {
62
- // The scriptblock form, not `-File`: there is no file to point at when the
63
- // script is fetched over the network. `& ([scriptblock]::Create(...))
64
- // -Desktop` binds `-Desktop` to the script's own `param()` block, which a
65
- // plain `irm ... | iex` cannot do.
66
- return {
67
- command: "powershell",
68
- args: [
69
- "-NoProfile",
70
- "-ExecutionPolicy",
71
- "Bypass",
72
- "-Command",
73
- `& ([scriptblock]::Create((irm "${REPO_RAW}/install.ps1"))) -Desktop`,
74
- ],
75
- };
76
- }
77
- return {
78
- command: "sh",
79
- args: ["-c", `curl -fsSL "${REPO_RAW}/install.sh" | sh -s -- --desktop`],
80
- };
81
- }
82
-
83
- function main() {
65
+ async function main() {
84
66
  const reason = skipReason(process.env);
85
67
  if (reason !== null) {
86
68
  note(reason);
@@ -88,30 +70,25 @@ function main() {
88
70
  }
89
71
 
90
72
  note(
91
- "global install -- running the official installer with the desktop app (`" +
92
- (IS_WINDOWS ? "install.ps1 -Desktop" : "install.sh --desktop") +
93
- "`). Set MCC_NPM_SKIP_INSTALL=1 to skip this."
73
+ "global install -- deciding what this machine needs. Set MCC_NPM_SKIP_INSTALL=1 to skip this, " +
74
+ "or MCC_NPM_INSTALL=server to take the server only."
94
75
  );
95
76
 
96
- const { command, args } = installerCommand();
97
- const result = spawnSync(command, args, { stdio: "inherit" });
98
- if (result.error) {
99
- console.error(
100
- `my-claude-code: could not start the installer: ${result.error.message}`
101
- );
102
- return 1;
103
- }
104
- const status = result.status ?? 1;
77
+ // No argv: `npm install -g` passes none. Overrides here are the environment
78
+ // variable, which is the only channel a package manager or Dockerfile has.
79
+ const status = await runtime.performInstall({ argv: [], log: note });
105
80
  if (status === 0) {
106
- note(
107
- "the server, every mcc-* command and the desktop app are installed. Open a new terminal so PATH picks them up."
108
- );
109
- } else {
110
- console.error(
111
- `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`
112
- );
81
+ note("done. Open a new terminal so PATH picks up the mcc-* commands.");
113
82
  }
114
83
  return status;
115
84
  }
116
85
 
117
- process.exit(main());
86
+ main().then(
87
+ (status) => {
88
+ process.exitCode = status;
89
+ },
90
+ (error) => {
91
+ console.error(`my-claude-code: the install hook failed: ${error && error.message}`);
92
+ process.exitCode = 1;
93
+ }
94
+ );
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+
3
+ // What `npm install -g` and `npx … install` decide, and how they carry it out.
4
+ //
5
+ // Both entry points face the same question: this machine is a Windows laptop,
6
+ // a Mac, a Linux desktop, a VPS reached over SSH, a CI runner or a WSL shell
7
+ // with no display -- which halves of MCC does it actually want? Getting that
8
+ // wrong is expensive in both directions. Installing a desktop app on a
9
+ // headless VPS leaves launcher shortcuts nobody can click and a download
10
+ // nobody asked for; installing only the server on a laptop makes `npm install
11
+ // -g` quietly worse than the one-line installer it wraps.
12
+ //
13
+ // So the decision is one pure function (`decide`) over platform, arch,
14
+ // environment and argv, it is testable without a network or an installer, and
15
+ // whoever runs it prints one line saying what it chose and why.
16
+ //
17
+ // The desktop half installs the OS-NATIVE shape: the Inno setup on Windows,
18
+ // the .dmg into ~/Applications on macOS, the .deb (or the tarball's own
19
+ // per-user installer) on Linux. That is deliberately not what `mcc-desktop`
20
+ // does -- `mcc-desktop` downloads the same Tauri shell into the config home on
21
+ // first launch, which needs no installer and no root but also leaves no Start
22
+ // Menu entry, no .desktop file and no Applications icon. Someone who typed
23
+ // `npm install -g` asked for an installed application, so that is what they
24
+ // get, and the config-home copy stays as the zero-install fallback.
25
+ //
26
+ // Every download is verified against `SHA256SUMS-desktop-shell.txt` from the
27
+ // same release before anything is run, mounted or unpacked. A file whose
28
+ // digest does not match is deleted, not quarantined: there is no case where
29
+ // keeping it helps and several where a later run picking it up hurts.
30
+
31
+ const childProcess = require("node:child_process");
32
+ const crypto = require("node:crypto");
33
+ const fs = require("node:fs");
34
+ const os = require("node:os");
35
+ const path = require("node:path");
36
+
37
+ const RELEASES = "https://github.com/FiredMosquito831/my-claude-code/releases/latest/download";
38
+ const SUMS_ASSET = "SHA256SUMS-desktop-shell.txt";
39
+
40
+ /** Release asset names, by `process.platform` and `process.arch`. */
41
+ const DESKTOP_ASSETS = {
42
+ // The Windows shell is x86_64 only; an arm64 Windows machine runs it under
43
+ // the OS's own emulation, which is why arm64 maps to the same file rather
44
+ // than to "unsupported".
45
+ win32: { x64: "MyClaudeCode-Setup-windows-x86_64.exe", arm64: "MyClaudeCode-Setup-windows-x86_64.exe" },
46
+ // One universal dmg covers both Mac architectures.
47
+ darwin: { x64: "MyClaudeCode-macos-universal.dmg", arm64: "MyClaudeCode-macos-universal.dmg" },
48
+ // Linux ships x86_64 only. arm64 gets the server and an honest sentence.
49
+ linux: { x64: { deb: "MyClaudeCode-linux-x86_64.deb", tarball: "MyClaudeCode-linux-x86_64.tar.gz" } },
50
+ };
51
+
52
+ const OVERRIDE_FLAGS = ["--server-only", "--desktop-only", "--no-desktop", "--yes-sudo"];
53
+
54
+ const HELP = `my-claude-code install -- install the latest MCC for this machine
55
+
56
+ By default it installs the server (always) and, when this machine has a
57
+ desktop session, the desktop application in its native form: the setup.exe on
58
+ Windows, the .dmg into ~/Applications on macOS, the .deb (or the tarball's
59
+ per-user installer) on Linux. On a headless box, over SSH, in CI or in WSL
60
+ without a display it installs the server only and says so.
61
+
62
+ --server-only install the server, never the desktop app
63
+ --desktop-only install the desktop app, leave the server alone
64
+ --no-desktop alias for --server-only
65
+ --yes-sudo on Linux, run \`sudo dpkg -i\` instead of printing it
66
+ --help this text
67
+
68
+ MCC_NPM_INSTALL=server|desktop|both|none the same choice as an environment
69
+ variable, for images and provisioning scripts
70
+
71
+ Every desktop download is checked against ${SUMS_ASSET} from
72
+ the same release before it is run, and deleted if the digest does not match.`;
73
+
74
+ /**
75
+ * Does this machine have a desktop session to install an application into?
76
+ *
77
+ * Windows and macOS always do -- there is no headless variant of either that
78
+ * npm can reach. Linux is the interesting case: a display server is the thing
79
+ * that distinguishes a workstation from the VPS, container or WSL shell that
80
+ * this same command runs in far more often. CI and SSH override everything:
81
+ * both mean "no human is looking at this screen", whatever the platform says.
82
+ */
83
+ function displayState(platform, env) {
84
+ if (env.CI) return { desktop: false, why: "CI is set" };
85
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) {
86
+ return { desktop: false, why: "this is an SSH session" };
87
+ }
88
+ if (platform === "win32") return { desktop: true, why: "Windows always has a session" };
89
+ if (platform === "darwin") return { desktop: true, why: "macOS always has a session" };
90
+ if (platform === "linux") {
91
+ if (env.WAYLAND_DISPLAY) return { desktop: true, why: `WAYLAND_DISPLAY=${env.WAYLAND_DISPLAY}` };
92
+ if (env.DISPLAY) return { desktop: true, why: `DISPLAY=${env.DISPLAY}` };
93
+ const wsl = env.WSL_DISTRO_NAME ? "WSL without a display" : "no DISPLAY or WAYLAND_DISPLAY";
94
+ return { desktop: false, why: wsl };
95
+ }
96
+ return { desktop: false, why: `${platform} has no desktop build` };
97
+ }
98
+
99
+ /** The overrides present in `argv`, and any argument that is not one. */
100
+ function parseFlags(argv) {
101
+ const flags = new Set();
102
+ const unknown = [];
103
+ let help = false;
104
+ for (const argument of argv) {
105
+ if (argument === "--help" || argument === "-h") {
106
+ help = true;
107
+ } else if (OVERRIDE_FLAGS.includes(argument)) {
108
+ flags.add(argument);
109
+ } else {
110
+ unknown.push(argument);
111
+ }
112
+ }
113
+ return { flags, unknown, help };
114
+ }
115
+
116
+ /**
117
+ * What to install, and the one line that explains it.
118
+ *
119
+ * Precedence, highest first: the command-line flags (the person is typing
120
+ * right now), `MCC_NPM_INSTALL` (an image or a provisioning script decided
121
+ * earlier), then the detected runtime. Nothing here touches the disk or the
122
+ * network, which is what makes the whole matrix testable.
123
+ */
124
+ function decide(options) {
125
+ const platform = options.platform ?? process.platform;
126
+ const arch = options.arch ?? process.arch;
127
+ const env = options.env ?? process.env;
128
+ const { flags, help, unknown } = parseFlags(options.argv ?? []);
129
+
130
+ const state = displayState(platform, env);
131
+ let server = true;
132
+ let desktop = state.desktop;
133
+ let why = state.why;
134
+ let source = "detected";
135
+
136
+ const mode = String(env.MCC_NPM_INSTALL ?? "").toLowerCase();
137
+ if (mode) {
138
+ if (!["server", "desktop", "both", "none"].includes(mode)) {
139
+ return {
140
+ error: `MCC_NPM_INSTALL=${env.MCC_NPM_INSTALL} is not one of server, desktop, both, none`,
141
+ server: false,
142
+ desktop: false,
143
+ help,
144
+ unknown,
145
+ };
146
+ }
147
+ server = mode === "server" || mode === "both";
148
+ desktop = mode === "desktop" || mode === "both";
149
+ why = `MCC_NPM_INSTALL=${mode}`;
150
+ source = "env";
151
+ }
152
+
153
+ if (flags.has("--desktop-only")) {
154
+ server = false;
155
+ desktop = true;
156
+ why = "--desktop-only";
157
+ source = "flag";
158
+ } else if (flags.has("--server-only") || flags.has("--no-desktop")) {
159
+ server = true;
160
+ desktop = false;
161
+ why = flags.has("--server-only") ? "--server-only" : "--no-desktop";
162
+ source = "flag";
163
+ }
164
+
165
+ // A desktop this release has no asset for is not a decision anyone can act
166
+ // on, so it degrades to the server rather than failing the install.
167
+ // Only asked when a desktop app is actually on the cards: resolving the
168
+ // Linux asset shells out to `command -v dpkg`, and a server-only install has
169
+ // no business probing the package manager.
170
+ const asset = desktop ? desktopAsset(platform, arch) : null;
171
+ if (desktop && asset === null) {
172
+ desktop = false;
173
+ why = `${why}, but there is no desktop build for ${platform}/${arch}`;
174
+ source = "unsupported";
175
+ }
176
+
177
+ const chose = !server && !desktop
178
+ ? "nothing"
179
+ : desktop && server
180
+ ? "server + desktop app"
181
+ : desktop
182
+ ? "desktop app only"
183
+ : "server only";
184
+ return {
185
+ server,
186
+ desktop,
187
+ // The server installer's `-Desktop`/`--desktop` flag adds the `mcc-desktop`
188
+ // launcher and its shortcuts. A headless box has nowhere to put them.
189
+ desktopFlag: desktop,
190
+ sudo: flags.has("--yes-sudo"),
191
+ asset,
192
+ help,
193
+ unknown,
194
+ source,
195
+ reason: `installing ${chose} on ${platform}/${arch} (${why})`,
196
+ };
197
+ }
198
+
199
+ /** The release asset for this runtime, or null when there is no build. */
200
+ function desktopAsset(platform, arch) {
201
+ const forPlatform = DESKTOP_ASSETS[platform];
202
+ if (!forPlatform) return null;
203
+ const entry = forPlatform[arch];
204
+ if (!entry) return null;
205
+ if (typeof entry === "string") return { kind: platform === "win32" ? "exe" : "dmg", name: entry };
206
+ // Linux: the .deb when dpkg can install it, the tarball otherwise. `dpkg`
207
+ // existing is the honest test -- a Fedora box has neither the command nor a
208
+ // use for the file.
209
+ const dpkg = childProcess.spawnSync("sh", ["-c", "command -v dpkg"], { encoding: "utf8" });
210
+ if (dpkg.status === 0 && String(dpkg.stdout ?? "").trim()) {
211
+ return { kind: "deb", name: entry.deb };
212
+ }
213
+ return { kind: "tarball", name: entry.tarball };
214
+ }
215
+
216
+ /** `releases/latest/download/<name>` -- no version in the URL, by design. */
217
+ function assetUrl(name) {
218
+ return `${RELEASES}/${name}`;
219
+ }
220
+
221
+ async function fetchBuffer(url) {
222
+ const response = await fetch(url, { redirect: "follow" });
223
+ if (!response.ok) {
224
+ throw new Error(`${url} -> HTTP ${response.status}`);
225
+ }
226
+ return Buffer.from(await response.arrayBuffer());
227
+ }
228
+
229
+ /** `{ filename: sha256 }` parsed from the `<64 hex><two spaces><name>` file. */
230
+ function parseSums(text) {
231
+ const table = {};
232
+ for (const line of text.split(/\r?\n/)) {
233
+ const match = /^([0-9a-f]{64})\s\s(\S+)$/.exec(line.trim());
234
+ if (match) table[match[2]] = match[1];
235
+ }
236
+ return table;
237
+ }
238
+
239
+ function sha256(buffer) {
240
+ return crypto.createHash("sha256").update(buffer).digest("hex");
241
+ }
242
+
243
+ /**
244
+ * Download one release asset into `directory`, verified against the release's
245
+ * own checksum file, and return its path.
246
+ *
247
+ * The verification happens in memory, before a single byte reaches a path any
248
+ * other process could execute. A mismatch deletes whatever was written and
249
+ * throws: an unverified installer is not a degraded install, it is one that
250
+ * must not happen.
251
+ */
252
+ async function downloadVerified(name, directory, log) {
253
+ const sumsText = (await fetchBuffer(assetUrl(SUMS_ASSET))).toString("utf8");
254
+ const sums = parseSums(sumsText);
255
+ const expected = sums[name];
256
+ if (!expected) {
257
+ throw new Error(`${SUMS_ASSET} on the latest release does not list ${name}`);
258
+ }
259
+ log(`downloading ${name} from the latest release...`);
260
+ const payload = await fetchBuffer(assetUrl(name));
261
+ const actual = sha256(payload);
262
+ const target = path.join(directory, name);
263
+ if (actual !== expected) {
264
+ try {
265
+ fs.rmSync(target, { force: true });
266
+ } catch {
267
+ // Nothing was written yet in the normal case; removing it is best effort.
268
+ }
269
+ throw new Error(
270
+ `${name} failed its SHA-256 check (expected ${expected}, got ${actual}). Nothing was run and the file was deleted.`
271
+ );
272
+ }
273
+ fs.mkdirSync(directory, { recursive: true });
274
+ fs.writeFileSync(target, payload);
275
+ log(`verified ${name} (sha256 ${actual})`);
276
+ return target;
277
+ }
278
+
279
+ /** Windows: the Inno setup, silent and per-user, so nothing prompts for UAC. */
280
+ function windowsInstallArgv(installer, directory) {
281
+ // `PrivilegesRequired=lowest` in the .iss means this never elevates.
282
+ // /SUPPRESSMSGBOXES and /NORESTART matter because npm's postinstall has no
283
+ // console to answer a dialog with. The .iss marks its [Run] entry
284
+ // `skipifsilent`, so this installs the app without launching it.
285
+ const argv = ["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"];
286
+ if (directory) argv.push(`/DIR=${directory}`);
287
+ return { command: installer, args: argv };
288
+ }
289
+
290
+ function run(command, args, options) {
291
+ const result = childProcess.spawnSync(command, args, { stdio: "inherit", ...options });
292
+ if (result.error) throw new Error(`could not run ${command}: ${result.error.message}`);
293
+ return result.status ?? 1;
294
+ }
295
+
296
+ /**
297
+ * Install the downloaded desktop app for this platform.
298
+ *
299
+ * Returns 0 when the application is installed, and 0 with a printed command
300
+ * when Linux needs a root the user did not offer -- an install that stopped to
301
+ * ask is not a failure, and failing here would fail an `npm install -g` whose
302
+ * server half already succeeded.
303
+ */
304
+ function installDesktopFrom(file, decision, platform, log) {
305
+ if (decision.asset.kind === "exe") {
306
+ const { command, args } = windowsInstallArgv(file, process.env.MCC_NPM_DESKTOP_DIR);
307
+ const status = run(command, args);
308
+ if (status !== 0) throw new Error(`the desktop installer exited ${status}`);
309
+ log("the desktop app is installed (Start Menu -> My Claude Code).");
310
+ return 0;
311
+ }
312
+
313
+ if (decision.asset.kind === "dmg") {
314
+ const applications = path.join(os.homedir(), "Applications");
315
+ const mount = path.join(os.tmpdir(), `mcc-dmg-${process.pid}`);
316
+ fs.mkdirSync(applications, { recursive: true });
317
+ run("hdiutil", ["attach", "-nobrowse", "-quiet", "-mountpoint", mount, file]);
318
+ try {
319
+ const app = fs.readdirSync(mount).find((entry) => entry.endsWith(".app"));
320
+ if (!app) throw new Error("the .dmg contains no .app bundle");
321
+ const destination = path.join(applications, app);
322
+ fs.rmSync(destination, { recursive: true, force: true });
323
+ run("cp", ["-R", path.join(mount, app), destination]);
324
+ // The shell is not notarised, so Gatekeeper would refuse the copy on
325
+ // first open with a dialog that offers no way forward. Clearing the
326
+ // quarantine bit here is the same trust decision the user already made
327
+ // by running this installer -- but it is not one to make silently.
328
+ run("xattr", ["-dr", "com.apple.quarantine", destination]);
329
+ log(`cleared com.apple.quarantine on ${destination} so macOS will open the unsigned app.`);
330
+ } finally {
331
+ run("hdiutil", ["detach", "-quiet", mount]);
332
+ }
333
+ return 0;
334
+ }
335
+
336
+ if (decision.asset.kind === "deb") {
337
+ if (!decision.sudo) {
338
+ // Escalating without being asked is how a package manager earns a
339
+ // reputation. The file is downloaded and verified; the last step is one
340
+ // line the user can read before they type it.
341
+ log(`the .deb is verified and ready. Finish the desktop app with:\n\n sudo dpkg -i ${file}\n\n(or rerun with --yes-sudo). The server is installed either way.`);
342
+ return 0;
343
+ }
344
+ const status = run("sudo", ["dpkg", "-i", file]);
345
+ if (status !== 0) throw new Error(`sudo dpkg -i exited ${status}`);
346
+ log("the desktop app is installed.");
347
+ return 0;
348
+ }
349
+
350
+ // The tarball's own installer is per-user and needs no root at all.
351
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), "mcc-desktop-"));
352
+ run("tar", ["-xzf", file, "-C", staging]);
353
+ const status = run("sh", [path.join(staging, "install-desktop.sh")]);
354
+ if (status !== 0) throw new Error(`install-desktop.sh exited ${status}`);
355
+ log("the desktop app is installed for this user (no root needed).");
356
+ return 0;
357
+ }
358
+
359
+ const REPO_RAW = "https://raw.githubusercontent.com/FiredMosquito831/my-claude-code/main/scripts";
360
+
361
+ /**
362
+ * The official install script for this platform, with the desktop flag only
363
+ * when there is a desktop to put shortcuts on.
364
+ *
365
+ * This package has never carried a second copy of the installer and must not
366
+ * start: the script it fetches is the one that verifies the release wheel's
367
+ * digest, installs `uv` and provisions Python.
368
+ */
369
+ function serverInstallerCommand(platform, withDesktop, script) {
370
+ const name = script ?? "install";
371
+ if (platform === "win32") {
372
+ // The scriptblock form, not `irm … | iex`: only a scriptblock can bind
373
+ // `-Desktop` to the script's own param() block.
374
+ const flag = withDesktop ? " -Desktop" : "";
375
+ return {
376
+ command: "powershell",
377
+ args: [
378
+ "-NoProfile",
379
+ "-ExecutionPolicy",
380
+ "Bypass",
381
+ "-Command",
382
+ `& ([scriptblock]::Create((irm "${REPO_RAW}/${name}.ps1")))${flag}`,
383
+ ],
384
+ };
385
+ }
386
+ const flag = withDesktop ? " -s -- --desktop" : "";
387
+ return { command: "sh", args: ["-c", `curl -fsSL "${REPO_RAW}/${name}.sh" | sh${flag}`] };
388
+ }
389
+
390
+ /**
391
+ * Decide, say so in one line, then install what was decided.
392
+ *
393
+ * The desktop half is deliberately not allowed to fail the whole run when the
394
+ * server half succeeded: someone who typed `npm install -g` on a laptop with a
395
+ * flaky network still wants the server they now have, and the app is one
396
+ * `npx … install --desktop-only` away. It does fail the run when the desktop
397
+ * app was the only thing asked for.
398
+ */
399
+ async function performInstall(options) {
400
+ const log = options.log ?? ((message) => console.log(`my-claude-code: ${message}`));
401
+ const platform = options.platform ?? process.platform;
402
+ const decision = decide({ ...options, platform });
403
+
404
+ if (decision.help) {
405
+ console.log(HELP);
406
+ return 0;
407
+ }
408
+ if (decision.error) {
409
+ console.error(`my-claude-code: ${decision.error}`);
410
+ return 1;
411
+ }
412
+ if (decision.unknown.length > 0) {
413
+ console.error(
414
+ `my-claude-code: unknown option ${decision.unknown[0]}. Run \`install --help\` for the list.`
415
+ );
416
+ return 1;
417
+ }
418
+
419
+ // The one line the spec asks for: what, where, and why.
420
+ log(decision.reason);
421
+ if (!decision.server && !decision.desktop) {
422
+ log("nothing to do (MCC_NPM_INSTALL=none).");
423
+ return 0;
424
+ }
425
+
426
+ if (decision.server) {
427
+ const { command, args } = serverInstallerCommand(platform, decision.desktopFlag);
428
+ const status = run(command, args);
429
+ if (status !== 0) {
430
+ 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`
432
+ );
433
+ return status;
434
+ }
435
+ }
436
+
437
+ if (decision.desktop) {
438
+ // MCC_NPM_DOWNLOAD_DIR keeps the verified artefact somewhere the caller
439
+ // chose, which is what makes a real download provable without a real
440
+ // install; without it the file lands in a temp directory.
441
+ const directory =
442
+ options.downloadDir ??
443
+ process.env.MCC_NPM_DOWNLOAD_DIR ??
444
+ fs.mkdtempSync(path.join(os.tmpdir(), "mcc-npm-"));
445
+ try {
446
+ const file = await downloadVerified(decision.asset.name, directory, log);
447
+ installDesktopFrom(file, decision, platform, log);
448
+ } catch (error) {
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ if (!decision.server) {
451
+ console.error(`my-claude-code: the desktop install failed: ${message}`);
452
+ return 1;
453
+ }
454
+ console.error(
455
+ `my-claude-code: the server is installed, but the desktop app is not: ${message}\n` +
456
+ "my-claude-code: retry it on its own with `npx @firedmosquito831/my-claude-code install --desktop-only`, " +
457
+ "or run `mcc-desktop`, which downloads the same verified shell into your config home."
458
+ );
459
+ return 0;
460
+ }
461
+ }
462
+
463
+ return 0;
464
+ }
465
+
466
+ module.exports = {
467
+ DESKTOP_ASSETS,
468
+ REPO_RAW,
469
+ performInstall,
470
+ serverInstallerCommand,
471
+ HELP,
472
+ RELEASES,
473
+ SUMS_ASSET,
474
+ assetUrl,
475
+ decide,
476
+ desktopAsset,
477
+ displayState,
478
+ downloadVerified,
479
+ installDesktopFrom,
480
+ parseFlags,
481
+ parseSums,
482
+ sha256,
483
+ windowsInstallArgv,
484
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firedmosquito831/my-claude-code",
3
- "version": "6.57.0",
3
+ "version": "6.58.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",