@ngcodes/ccpm 0.4.9 → 0.5.1
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 +82 -82
- package/bin/ccpm +5 -4
- package/install.js +89 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,13 +6,11 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@ngcodes/ccpm)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
ccpm (Claude Code Profile Manager)
|
|
9
|
+
ccpm (Claude Code Profile Manager) keeps multiple Claude Code profiles isolated on one machine. Each profile has its own credentials, settings, MCP servers, plugins, skills, and memory. Open two terminals, run two accounts.
|
|
10
10
|
|
|
11
11
|
## Why
|
|
12
12
|
|
|
13
|
-
Claude Code reads config from a single directory (`~/.claude`)
|
|
14
|
-
|
|
15
|
-
ccpm fixes this. Each profile gets its own config directory. When you run `ccpm run <profile>`, it sets `CLAUDE_CONFIG_DIR` to the right directory and launches Claude Code. Two terminals, two profiles, zero conflicts.
|
|
13
|
+
Claude Code reads its config from a single directory (`~/.claude`), so without ccpm you can only be signed into one account at a time. ccpm gives every profile its own config directory and sets `CLAUDE_CONFIG_DIR` when launching claude. Two terminals, two profiles, zero conflicts.
|
|
16
14
|
|
|
17
15
|
## Install
|
|
18
16
|
|
|
@@ -30,19 +28,19 @@ go install github.com/nitin-1926/claude-code-profile-manager/ccpm@latest
|
|
|
30
28
|
## Quick start
|
|
31
29
|
|
|
32
30
|
```bash
|
|
33
|
-
# Create profiles
|
|
34
|
-
ccpm add personal
|
|
35
|
-
ccpm add work
|
|
31
|
+
# Create profiles (each prompts for OAuth or API key)
|
|
32
|
+
ccpm add personal
|
|
33
|
+
ccpm add work
|
|
36
34
|
|
|
37
|
-
# Run
|
|
38
|
-
ccpm run personal #
|
|
39
|
-
ccpm run work #
|
|
35
|
+
# Run in parallel
|
|
36
|
+
ccpm run personal # terminal 1
|
|
37
|
+
ccpm run work # terminal 2
|
|
40
38
|
|
|
41
39
|
# Check status
|
|
42
40
|
ccpm list
|
|
43
41
|
```
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
Sample output:
|
|
46
44
|
|
|
47
45
|
```
|
|
48
46
|
NAME AUTH STATUS
|
|
@@ -50,113 +48,115 @@ personal oauth ✓ nitin@gmail.com
|
|
|
50
48
|
work api_key ✓ sk-ant-...7f2k ★
|
|
51
49
|
```
|
|
52
50
|
|
|
51
|
+
## Changelog
|
|
52
|
+
|
|
53
|
+
Release highlights and fixes: **[ccpm.dev/changelog](https://ccpm.dev/changelog)**.
|
|
54
|
+
|
|
53
55
|
## Key features
|
|
54
56
|
|
|
55
|
-
- **Parallel sessions**: run different Claude Code accounts in different terminals
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **Per-profile env vars**: `ccpm env set` persists them; `ccpm run --ccpm-env KEY=VAL` overlays one-shot overrides. Unknown flags (`--dangerously-skip-permissions`, `--model`, ...) flow through to claude with no `--` separator needed.
|
|
63
|
-
- **Sessions**: `ccpm sessions list <profile>` browses Claude Code's session files
|
|
64
|
-
- **Settings**: native-Claude-compatible merge — `~/.claude/settings.json` → profile fragment → project `.claude/settings.{json,local.json}` → enterprise managed-settings (org policy, highest precedence)
|
|
65
|
-
- **Encrypted vault**: AES-256-GCM encrypted credential backups with master key in your OS keychain
|
|
66
|
-
- **IDE support**: set the default profile for VS Code with `ccpm set-default`
|
|
67
|
-
- **Shell integration**: `ccpm use` sets the profile for your entire shell session
|
|
68
|
-
- **Cross-platform**: macOS Keychain, Linux Secret Service, Windows Credential Manager
|
|
57
|
+
- **Parallel sessions**: run different Claude Code accounts in different terminals at the same time.
|
|
58
|
+
- **OAuth + API key** per profile, with credentials in the OS keychain.
|
|
59
|
+
- **Per-profile assets**: skills, agents, commands, rules, hooks, plugins, MCP servers, and settings install globally, per-profile, or per-project.
|
|
60
|
+
- **MCP transports**: stdio, HTTP, SSE. `ccpm mcp auth <server> --profile <p>` completes OAuth in the right profile.
|
|
61
|
+
- **Permissions and hooks** without hand-editing JSON.
|
|
62
|
+
- **IDE default**: `ccpm set-default` pins a profile for direct `claude` launches (VS Code, Cursor, Antigravity). On macOS this is set system-wide via a LaunchAgent.
|
|
63
|
+
- **Encrypted vault**: AES-256-GCM credential backups with a master key in your OS keychain.
|
|
69
64
|
|
|
70
65
|
## Commands
|
|
71
66
|
|
|
72
|
-
| Command
|
|
73
|
-
|
|
|
74
|
-
| `ccpm add <name>`
|
|
75
|
-
| `ccpm run <name> [
|
|
76
|
-
| `ccpm use <name>`
|
|
77
|
-
| `ccpm list`
|
|
78
|
-
| `ccpm status`
|
|
79
|
-
| `ccpm
|
|
80
|
-
| `ccpm
|
|
81
|
-
| `ccpm
|
|
82
|
-
| `ccpm
|
|
83
|
-
| `ccpm
|
|
84
|
-
| `ccpm
|
|
85
|
-
| `ccpm
|
|
86
|
-
| `ccpm
|
|
87
|
-
| `ccpm
|
|
88
|
-
| `ccpm
|
|
89
|
-
| `ccpm
|
|
90
|
-
| `ccpm
|
|
91
|
-
| `ccpm
|
|
92
|
-
| `ccpm
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| --------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
69
|
+
| `ccpm add <name>` | Create a new profile (OAuth or API key) via an interactive wizard |
|
|
70
|
+
| `ccpm run <name> [args...]` | Launch claude with the profile (unknown flags forward to claude) |
|
|
71
|
+
| `ccpm use <name>` | Set the profile for the current shell session |
|
|
72
|
+
| `ccpm list` / `ls` | List profiles with auth status (`--json` for machine-readable output) |
|
|
73
|
+
| `ccpm status [name]` | System overview, or auth health for one profile |
|
|
74
|
+
| `ccpm rename <old> <new>` | Rename a profile (migrates keychain entries and plugin paths) |
|
|
75
|
+
| `ccpm remove <name>` / `rm` | Delete a profile (`--force` to skip confirm) |
|
|
76
|
+
| `ccpm clone <src> <new>` | Duplicate a profile (assets + settings + auth; `--no-auth` available) |
|
|
77
|
+
| `ccpm set-default [name]` / `ccpm unset-default` | Pin or clear the default profile for direct `claude` launches |
|
|
78
|
+
| `ccpm prompt` | Print the active profile name for a shell prompt (PS1 / starship) |
|
|
79
|
+
| `ccpm sync` | Re-apply global installs into one or all profiles |
|
|
80
|
+
| `ccpm doctor` | Health check: env, auth, drift, symlinks, cascade (`--fix` prunes symlinks) |
|
|
81
|
+
| `ccpm export / import-bundle` | Export a profile to a portable `.tar.gz` and restore it elsewhere |
|
|
82
|
+
| `ccpm consolidate` | Audit (and optionally `--fix`) asset drift across scopes |
|
|
83
|
+
| `ccpm default fingerprint update / check` | Record or diff the `~/.claude` drift baseline |
|
|
84
|
+
| `ccpm trust add / remove / list` | Grant/revoke trust for project `.claude/` hooks, permissions, MCP |
|
|
85
|
+
| `ccpm import default / from-profile` | Import assets from `~/.claude` or clone from another ccpm profile |
|
|
86
|
+
| `ccpm skill / agent / command / rule add|remove|list|link`| Manage per-profile or global Claude Code asset trees |
|
|
87
|
+
| `ccpm plugin list / enable / disable / install / remove` | Marketplaces + per-profile activation; `gc` cleans the shared cache |
|
|
88
|
+
| `ccpm hooks add / remove / list` | Manage hook entries in profile settings |
|
|
89
|
+
| `ccpm mcp add / remove / list / import / auth` | Manage MCP servers (stdio, http, sse) at global/profile/project scope |
|
|
90
|
+
| `ccpm env set / unset / list` | Persist env vars per profile (injected at `ccpm run`) |
|
|
91
|
+
| `ccpm permissions allow / ask / deny / remove / list / mode` | Manage `permissions.*` rules and defaultMode |
|
|
92
|
+
| `ccpm sessions list <profile>` | List Claude Code sessions stored in a profile |
|
|
93
|
+
| `ccpm settings set / get / apply / show / statusline / outputstyle` | Manage Claude Code settings per profile |
|
|
94
|
+
| `ccpm auth status / refresh / backup / restore` | Manage authentication and the encrypted vault |
|
|
95
|
+
| `ccpm config set / get` | `cascade_auto_adopt`, `check_default_drift`, `default_dir` (get-only) |
|
|
96
|
+
| `ccpm shell-init` | Print the shell hook (zsh / bash / fish / powershell) |
|
|
97
|
+
| `ccpm completion <shell>` | Generate a shell completion script (bash / zsh / fish / powershell) |
|
|
98
|
+
| `ccpm uninstall` | Remove all profiles, keychain entries, vault, and `~/.ccpm/` |
|
|
99
|
+
|
|
100
|
+
Full command reference and guides: **[ccpm.dev/docs](https://ccpm.dev/docs)**.
|
|
101
|
+
|
|
102
|
+
`ccpm run` intercepts four flags before forwarding to claude: `--ccpm-env KEY=VAL` (one-shot env override, repeatable), `--no-auto-adopt` (skip the host-asset cascade scan), `--help`, `--version`. Use `--` to forward `--help` or `--version` to claude.
|
|
93
103
|
|
|
94
104
|
## How it works
|
|
95
105
|
|
|
96
|
-
ccpm
|
|
106
|
+
1. `ccpm add` creates `~/.ccpm/profiles/<name>/` with its own config and credentials.
|
|
107
|
+
2. `ccpm run` merges shared settings/MCP fragments, sets `CLAUDE_CONFIG_DIR`, and execs `claude`.
|
|
108
|
+
3. Each terminal gets a completely isolated Claude Code instance.
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
2. `ccpm run` merges shared settings/MCP fragments, sets `CLAUDE_CONFIG_DIR`, and execs `claude`
|
|
100
|
-
3. Each terminal gets a completely isolated Claude Code instance
|
|
101
|
-
|
|
102
|
-
Skills and MCP servers can be installed globally (`--global`, stored in `~/.ccpm/share/`) or per-profile (`--profile <name>`). For settings, the cross-profile baseline is the native `~/.claude/settings.json` file (ccpm merges it into every profile at launch); use `ccpm settings set --profile <name>` for per-profile overrides. Per-repo overrides live in `./.claude/settings.json` and are honored automatically.
|
|
110
|
+
Assets install globally (`--global`, stored in `~/.ccpm/share/`) or per-profile (`--profile <name>`). For settings, the cross-profile baseline is the native `~/.claude/settings.json`; ccpm merges it into every profile at launch. Per-repo overrides in `./.claude/settings.json` are honored automatically.
|
|
103
111
|
|
|
104
112
|
No daemons. No patches. No magic.
|
|
105
113
|
|
|
106
114
|
## Privacy and security
|
|
107
115
|
|
|
108
|
-
ccpm is 100% local. It never makes network requests,
|
|
116
|
+
ccpm is 100% local. It never makes network requests, collects data, or phones home.
|
|
109
117
|
|
|
110
|
-
- API keys
|
|
111
|
-
- Vault backups use AES-256-GCM
|
|
112
|
-
- All data lives in `~/.ccpm
|
|
113
|
-
- No telemetry, analytics, or tracking
|
|
118
|
+
- API keys live in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager).
|
|
119
|
+
- Vault backups use AES-256-GCM with a master key in your OS keychain.
|
|
120
|
+
- All data lives in `~/.ccpm/`. No telemetry, no analytics, no tracking.
|
|
114
121
|
|
|
115
122
|
## Platform support
|
|
116
123
|
|
|
117
|
-
| Feature | macOS
|
|
118
|
-
| ------------------ | ---------------------------------------- |
|
|
119
|
-
| OAuth per-profile | Keychain entry namespaced by profile dir |
|
|
120
|
-
| API key storage | Keychain |
|
|
121
|
-
| Parallel sessions | Yes | Yes
|
|
122
|
-
| Shared
|
|
123
|
-
| Shell hook | zsh, bash, fish | zsh, bash, fish
|
|
124
|
+
| Feature | macOS ✓ verified | Windows ⚠ experimental | Linux ⚠ experimental |
|
|
125
|
+
| ------------------ | ---------------------------------------- | ----------------------------------------- | -------------------- |
|
|
126
|
+
| OAuth per-profile | Keychain entry namespaced by profile dir | wincred entry, same namespacing model | `.credentials.json` |
|
|
127
|
+
| API key storage | Keychain | Credential Manager | Secret Service |
|
|
128
|
+
| Parallel sessions | Yes | Yes | Yes |
|
|
129
|
+
| Shared asset dedup | Symlinks | Symlinks (Developer Mode) or copy | Symlinks |
|
|
130
|
+
| Shell hook | zsh, bash, fish | PowerShell | zsh, bash, fish |
|
|
131
|
+
| `set-default` (system-wide GUI) | LaunchAgent + `launchctl setenv` | Keychain / identity only | Keychain / identity only |
|
|
124
132
|
|
|
125
|
-
>
|
|
133
|
+
> Requires **Claude Code v2.1.56+** on macOS for OAuth isolation. `ccpm doctor` warns when your Claude Code is too old.
|
|
126
134
|
|
|
127
135
|
## MCP authentication model
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
2. **OAuth MCPs (isolated)** — Claude Code caches OAuth tokens inside `<CLAUDE_CONFIG_DIR>/.claude.json`, which is per-profile.
|
|
133
|
-
3. **Globally-cached MCPs (shared)** — MCPs that write to `~/.config/<service>` or a fixed-name keychain entry are shared across every profile. ccpm cannot isolate them without upstream changes.
|
|
137
|
+
1. **Env-var MCPs** (e.g. `GITHUB_TOKEN`): isolated per profile. Configure with `ccpm mcp add <name> --env KEY=VALUE --profile <p>`.
|
|
138
|
+
2. **OAuth MCPs**: isolated, because tokens cache inside `<CLAUDE_CONFIG_DIR>/.claude.json` which is per-profile.
|
|
139
|
+
3. **Globally-cached MCPs** (servers that write to `~/.config/<service>` or a non-namespaced OS keychain entry): **shared** across profiles. ccpm cannot isolate them.
|
|
134
140
|
|
|
135
141
|
## Known limitations
|
|
136
142
|
|
|
137
|
-
- **
|
|
138
|
-
- **Windows without Developer Mode**: ccpm falls back to copying shared assets instead of symlinking
|
|
139
|
-
- **Globally-cached MCP servers** (see the MCP auth model
|
|
140
|
-
- **Linux
|
|
143
|
+
- **IDE extensions ignore `CLAUDE_CONFIG_DIR`**: VS Code, Cursor, and Antigravity launch claude directly. Use `ccpm set-default` to pin a profile for them. macOS is system-wide; Linux and Windows keep keychain and identity sync but no LaunchAgent equivalent yet.
|
|
144
|
+
- **Windows without Developer Mode**: ccpm falls back to copying shared assets instead of symlinking and writes a marker at `~/.ccpm/.windows-copy-fallback`.
|
|
145
|
+
- **Globally-cached MCP servers** cannot be isolated per profile (see the MCP auth model).
|
|
146
|
+
- **Headless Linux**: `go-keyring` requires D-Bus and a secret service. API-key profiles need one running.
|
|
141
147
|
|
|
142
148
|
## Build from source
|
|
143
149
|
|
|
144
150
|
```bash
|
|
145
151
|
git clone https://github.com/nitin-1926/claude-code-profile-manager.git
|
|
146
|
-
cd claude-code-profile-manager/
|
|
152
|
+
cd claude-code-profile-manager/ccpm
|
|
147
153
|
go build -o ccpm .
|
|
148
154
|
./ccpm --version
|
|
149
155
|
```
|
|
150
156
|
|
|
151
157
|
## Contributing
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
1. Fork the repo
|
|
156
|
-
2. Create a feature branch (`git checkout -b feature/your-feature`)
|
|
157
|
-
3. Make your changes
|
|
158
|
-
4. Run tests (`cd ccpm && go test ./...`)
|
|
159
|
-
5. Open a pull request
|
|
159
|
+
Open an issue first to discuss what you want to change, then fork, branch, test (`cd ccpm && go test ./...`), and open a PR.
|
|
160
160
|
|
|
161
161
|
## License
|
|
162
162
|
|
package/bin/ccpm
CHANGED
|
@@ -10,11 +10,12 @@ const binary = path.join(__dirname, isWin ? "ccpm-native.exe" : "ccpm-native");
|
|
|
10
10
|
if (!fs.existsSync(binary)) {
|
|
11
11
|
console.error("ccpm: native binary not found at " + binary);
|
|
12
12
|
console.error(
|
|
13
|
-
"The postinstall
|
|
14
|
-
);
|
|
15
|
-
console.error(
|
|
16
|
-
"Or download manually: https://github.com/nitin-1926/claude-code-profile-manager/releases"
|
|
13
|
+
"The postinstall download was skipped (e.g. --ignore-scripts) or failed (e.g. offline). Fix it with any of:"
|
|
17
14
|
);
|
|
15
|
+
console.error(" • Reinstall WITHOUT --ignore-scripts: npm i -g @ngcodes/ccpm");
|
|
16
|
+
console.error(" • Re-run the download in-place: node \"" + path.join(__dirname, "..", "install.js") + "\"");
|
|
17
|
+
console.error(" • Install via curl: curl -fsSL https://raw.githubusercontent.com/nitin-1926/claude-code-profile-manager/main/scripts/install.sh | sh");
|
|
18
|
+
console.error(" • Or download manually: https://github.com/nitin-1926/claude-code-profile-manager/releases");
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
21
|
|
package/install.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Downloads the correct ccpm binary for the current platform during npm install
|
|
3
3
|
|
|
4
4
|
const { execSync } = require("child_process");
|
|
5
|
+
const crypto = require("crypto");
|
|
5
6
|
const fs = require("fs");
|
|
6
7
|
const path = require("path");
|
|
7
8
|
const https = require("https");
|
|
@@ -9,6 +10,12 @@ const https = require("https");
|
|
|
9
10
|
const REPO = "nitin-1926/claude-code-profile-manager";
|
|
10
11
|
const BINARY = "ccpm";
|
|
11
12
|
|
|
13
|
+
// Pin the download to the version of this exact npm package. Querying
|
|
14
|
+
// releases/latest would let a freshly cut release ship a binary that doesn't
|
|
15
|
+
// match the installed package version (and could be raced by an attacker who
|
|
16
|
+
// can publish a release). require()ing package.json keeps the two in lockstep.
|
|
17
|
+
const VERSION = require("./package.json").version;
|
|
18
|
+
|
|
12
19
|
function getPlatform() {
|
|
13
20
|
const platform = process.platform;
|
|
14
21
|
const arch = process.arch;
|
|
@@ -27,27 +34,45 @@ function getPlatform() {
|
|
|
27
34
|
return { os, arch: cpu };
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
// Fetch a small text resource (the checksums manifest), following redirects.
|
|
38
|
+
function fetchText(url) {
|
|
31
39
|
return new Promise((resolve, reject) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const json = JSON.parse(data);
|
|
39
|
-
resolve(json.tag_name.replace(/^v/, ""));
|
|
40
|
-
} catch {
|
|
41
|
-
// Fallback to package.json version
|
|
42
|
-
const pkg = require("./package.json");
|
|
43
|
-
resolve(pkg.version);
|
|
40
|
+
const follow = (url) => {
|
|
41
|
+
https.get(url, { headers: { "User-Agent": "ccpm-npm" } }, (res) => {
|
|
42
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
43
|
+
follow(res.headers.location);
|
|
44
|
+
return;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (res.statusCode !== 200) {
|
|
47
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let data = "";
|
|
51
|
+
res.on("data", (chunk) => (data += chunk));
|
|
52
|
+
res.on("end", () => resolve(data));
|
|
53
|
+
res.on("error", reject);
|
|
54
|
+
}).on("error", reject);
|
|
55
|
+
};
|
|
56
|
+
follow(url);
|
|
48
57
|
});
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
function sha256OfFile(filePath) {
|
|
61
|
+
const hash = crypto.createHash("sha256");
|
|
62
|
+
hash.update(fs.readFileSync(filePath));
|
|
63
|
+
return hash.digest("hex");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse goreleaser's checksums.txt ("<sha256> <filename>" per line) and return
|
|
67
|
+
// the expected hash for archiveName, or null if it isn't listed.
|
|
68
|
+
function expectedChecksum(manifest, archiveName) {
|
|
69
|
+
for (const line of manifest.split("\n")) {
|
|
70
|
+
const m = line.match(/^([0-9a-fA-F]{64})\s+(\S+)\s*$/);
|
|
71
|
+
if (m && m[2] === archiveName) return m[1].toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
51
76
|
async function download(url, dest) {
|
|
52
77
|
return new Promise((resolve, reject) => {
|
|
53
78
|
const follow = (url) => {
|
|
@@ -85,10 +110,13 @@ function warnIfUnverifiedPlatform(os) {
|
|
|
85
110
|
async function main() {
|
|
86
111
|
const { os, arch } = getPlatform();
|
|
87
112
|
warnIfUnverifiedPlatform(os);
|
|
88
|
-
const version =
|
|
113
|
+
const version = VERSION;
|
|
89
114
|
|
|
90
115
|
const ext = os === "windows" ? "zip" : "tar.gz";
|
|
91
|
-
const
|
|
116
|
+
const archiveName = `${BINARY}_${os}_${arch}.${ext}`;
|
|
117
|
+
const baseUrl = `https://github.com/${REPO}/releases/download/v${version}`;
|
|
118
|
+
const url = `${baseUrl}/${archiveName}`;
|
|
119
|
+
const checksumsUrl = `${baseUrl}/checksums.txt`;
|
|
92
120
|
|
|
93
121
|
console.log(`Installing ccpm v${version} for ${os}/${arch}...`);
|
|
94
122
|
|
|
@@ -102,7 +130,49 @@ async function main() {
|
|
|
102
130
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
103
131
|
|
|
104
132
|
const archivePath = path.join(tmpDir, `archive.${ext}`);
|
|
105
|
-
|
|
133
|
+
try {
|
|
134
|
+
await download(url, archivePath);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`failed to download ${url}: ${err.message}\n` +
|
|
138
|
+
` Is there a published release for v${version}? See https://github.com/${REPO}/releases`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Verify the archive against the SHA-256 published in the SAME release's
|
|
143
|
+
// checksums.txt. Fail closed: a missing manifest, a missing line, or a
|
|
144
|
+
// mismatch deletes the partial download and aborts — we never install an
|
|
145
|
+
// unverified binary that would run with the user's privileges.
|
|
146
|
+
let manifest;
|
|
147
|
+
try {
|
|
148
|
+
manifest = await fetchText(checksumsUrl);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
151
|
+
throw new Error(
|
|
152
|
+
`failed to download ${checksumsUrl}: ${err.message}\n` +
|
|
153
|
+
` Refusing to install without a checksum to verify against.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log("Verifying SHA-256...");
|
|
158
|
+
const expected = expectedChecksum(manifest, archiveName);
|
|
159
|
+
if (!expected) {
|
|
160
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
+
throw new Error(
|
|
162
|
+
`${archiveName} not listed in checksums.txt — possible tampering or missing release asset.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const actual = sha256OfFile(archivePath);
|
|
166
|
+
if (expected !== actual) {
|
|
167
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
168
|
+
throw new Error(
|
|
169
|
+
`checksum mismatch for ${archiveName}.\n` +
|
|
170
|
+
` expected: ${expected}\n` +
|
|
171
|
+
` actual: ${actual}\n` +
|
|
172
|
+
` Refusing to install a tampered binary.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
console.log(` sha256 ok (${expected})`);
|
|
106
176
|
|
|
107
177
|
if (ext === "zip") {
|
|
108
178
|
execSync(`unzip -o -q "${archivePath}" -d "${tmpDir}"`, { stdio: "inherit" });
|