@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 +58 -12
- package/bin/my-claude-code.js +43 -40
- package/bin/postinstall.js +38 -61
- package/bin/runtime-install.js +484 -0
- package/package.json +1 -1
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
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
33
|
-
npx @firedmosquito831/my-claude-code desktop #
|
|
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).
|
package/bin/my-claude-code.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
);
|
package/bin/postinstall.js
CHANGED
|
@@ -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
|
|
6
|
-
//
|
|
7
|
-
// on first use. That is what this hook is
|
|
8
|
-
// place in this package that installs
|
|
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.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 --
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|