@keywaysh/cli 0.2.0 → 0.3.2
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 +18 -207
- package/bin/keyway +25 -0
- package/package.json +19 -55
- package/scripts/install.js +216 -0
- package/scripts/postinstall.js +157 -0
- package/dist/auth-64V3RWUK.js +0 -12
- package/dist/chunk-IVZM2JTT.js +0 -101
- package/dist/cli.js +0 -3047
package/README.md
CHANGED
|
@@ -1,236 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
<h1>Keyway CLI</h1>
|
|
3
|
-
<p><strong>Sync .env files across your team in 30 seconds.</strong></p>
|
|
4
|
-
<p>Stop sharing secrets on Slack. One command and everyone's in sync.</p>
|
|
5
|
-
<br/>
|
|
1
|
+
# @keywaysh/cli
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/@keywaysh/cli)
|
|
9
|
-
[](https://opensource.org/licenses/MIT)
|
|
10
|
-
[](https://keyway.sh)
|
|
3
|
+
GitHub-native secrets management. If you have repo access, you get secret access.
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
<a href="https://keyway.sh">Website</a> •
|
|
14
|
-
<a href="https://docs.keyway.sh">Docs</a> •
|
|
15
|
-
<a href="https://keyway.sh/security">Security</a> •
|
|
16
|
-
<a href="https://status.keyway.sh">Status</a>
|
|
17
|
-
</div>
|
|
5
|
+
## Installation
|
|
18
6
|
|
|
19
|
-
<br/>
|
|
20
|
-
|
|
21
|
-
## Quick Start (30 seconds)
|
|
22
|
-
|
|
23
|
-
**1. Setup your project:**
|
|
24
7
|
```bash
|
|
25
8
|
npm install -g @keywaysh/cli
|
|
26
|
-
keyway init
|
|
27
9
|
```
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
keyway pull
|
|
32
|
-
```
|
|
11
|
+
Or with npx:
|
|
33
12
|
|
|
34
|
-
**3. Deploy to Vercel:**
|
|
35
13
|
```bash
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
<br/>
|
|
40
|
-
|
|
41
|
-
<details>
|
|
42
|
-
<summary><strong>See it in action</strong></summary>
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
$ keyway init
|
|
46
|
-
✓ Logged in as @nicolas
|
|
47
|
-
✓ Detected repo: acme/backend
|
|
48
|
-
✓ Vault created
|
|
49
|
-
? Push your .env to the vault? Yes
|
|
50
|
-
✓ 12 secrets encrypted and stored
|
|
51
|
-
|
|
52
|
-
$ keyway pull
|
|
53
|
-
✓ .env synced (12 secrets)
|
|
14
|
+
npx @keywaysh/cli pull
|
|
54
15
|
```
|
|
55
16
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<br/>
|
|
59
|
-
|
|
60
|
-
## Why Keyway?
|
|
61
|
-
|
|
62
|
-
Your secrets are probably in:
|
|
63
|
-
|
|
64
|
-
- 💬 Slack DMs
|
|
65
|
-
- 📝 Notion pages
|
|
66
|
-
- 📧 Old emails
|
|
67
|
-
- 🤷 The laptop of the dev who left
|
|
68
|
-
|
|
69
|
-
**Keyway fixes that.** If you have GitHub repo access → you have secret access. No invites, no dashboards, no YAML.
|
|
70
|
-
|
|
71
|
-
<br/>
|
|
72
|
-
|
|
73
|
-
## Install
|
|
17
|
+
## Quick Start
|
|
74
18
|
|
|
75
19
|
```bash
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# pnpm
|
|
80
|
-
pnpm add -g @keywaysh/cli
|
|
81
|
-
|
|
82
|
-
# yarn
|
|
83
|
-
yarn global add @keywaysh/cli
|
|
84
|
-
|
|
85
|
-
# bun
|
|
86
|
-
bun add -g @keywaysh/cli
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
<br/>
|
|
90
|
-
|
|
91
|
-
## Commands
|
|
92
|
-
|
|
93
|
-
| Command | What it does |
|
|
94
|
-
|---------|--------------|
|
|
95
|
-
| `keyway login` | Authenticate with GitHub |
|
|
96
|
-
| `keyway init` | Create a vault for this repo |
|
|
97
|
-
| `keyway push` | Upload `.env` to vault |
|
|
98
|
-
| `keyway pull` | Download secrets to `.env` |
|
|
99
|
-
| `keyway sync vercel` | Deploy secrets to Vercel |
|
|
100
|
-
| `keyway doctor` | Debug your setup |
|
|
101
|
-
|
|
102
|
-
<br/>
|
|
20
|
+
# Sign in with GitHub
|
|
21
|
+
keyway login
|
|
103
22
|
|
|
104
|
-
|
|
23
|
+
# Initialize vault for current repo
|
|
24
|
+
keyway init
|
|
105
25
|
|
|
106
|
-
|
|
107
|
-
# Push your local .env
|
|
26
|
+
# Push secrets from .env
|
|
108
27
|
keyway push
|
|
109
28
|
|
|
110
|
-
#
|
|
111
|
-
keyway push --env production
|
|
112
|
-
|
|
113
|
-
# Pull secrets
|
|
114
|
-
keyway pull
|
|
115
|
-
|
|
116
|
-
# Pull from production
|
|
117
|
-
keyway pull --env production
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
<br/>
|
|
121
|
-
|
|
122
|
-
### Sync to Providers
|
|
123
|
-
|
|
124
|
-
Deploy your secrets to Vercel, Netlify, or Railway with one command:
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
keyway sync vercel
|
|
128
|
-
keyway sync netlify
|
|
129
|
-
keyway sync railway
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
First time: OAuth flow. After that: instant sync.
|
|
133
|
-
|
|
134
|
-
<br/>
|
|
135
|
-
|
|
136
|
-
## Real-World Workflows
|
|
137
|
-
|
|
138
|
-
**New dev joins the team:**
|
|
139
|
-
```bash
|
|
140
|
-
git clone git@github.com:acme/app.git
|
|
141
|
-
cd app
|
|
29
|
+
# Pull secrets to .env
|
|
142
30
|
keyway pull
|
|
143
|
-
npm run dev # Ready to code
|
|
144
31
|
```
|
|
145
32
|
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
# Update .env locally, then:
|
|
149
|
-
keyway push
|
|
150
|
-
# Done. Everyone pulls the update.
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Deploy to production:**
|
|
154
|
-
```bash
|
|
155
|
-
keyway push --env production
|
|
156
|
-
keyway sync vercel
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
<br/>
|
|
160
|
-
|
|
161
|
-
## Security
|
|
162
|
-
|
|
163
|
-
Keyway is a **major upgrade from Slack** without the complexity of Vault or AWS Secrets Manager.
|
|
33
|
+
## Alternative Installation
|
|
164
34
|
|
|
165
|
-
|
|
166
|
-
|---------|--------|
|
|
167
|
-
| AES-256-GCM encryption | ✅ |
|
|
168
|
-
| Encrypted at rest | ✅ |
|
|
169
|
-
| TLS 1.3 everywhere | ✅ |
|
|
170
|
-
| GitHub-based access control | ✅ |
|
|
171
|
-
| No access to your code | ✅ |
|
|
172
|
-
| SOC2 / enterprise compliance | ❌ |
|
|
173
|
-
|
|
174
|
-
**We never track secret values.** Only metadata (command usage, repo names).
|
|
175
|
-
|
|
176
|
-
→ [Full security details](https://keyway.sh/security)
|
|
177
|
-
|
|
178
|
-
<br/>
|
|
179
|
-
|
|
180
|
-
## Who is this for?
|
|
181
|
-
|
|
182
|
-
**Perfect for:**
|
|
183
|
-
- Solo devs
|
|
184
|
-
- Small teams (2-20)
|
|
185
|
-
- Side projects
|
|
186
|
-
- Agencies
|
|
187
|
-
- Early-stage startups
|
|
188
|
-
|
|
189
|
-
**Not for:**
|
|
190
|
-
- Banks, governments, enterprises needing SOC2/zero-trust
|
|
191
|
-
- (Try Vault, Doppler, or AWS Secrets Manager)
|
|
192
|
-
|
|
193
|
-
<br/>
|
|
194
|
-
|
|
195
|
-
## Configuration
|
|
35
|
+
Using the install script:
|
|
196
36
|
|
|
197
37
|
```bash
|
|
198
|
-
|
|
199
|
-
export KEYWAY_API_URL=https://your-api.com
|
|
200
|
-
|
|
201
|
-
# Disable telemetry
|
|
202
|
-
export KEYWAY_DISABLE_TELEMETRY=1
|
|
38
|
+
curl -fsSL https://keyway.sh/install.sh | sh
|
|
203
39
|
```
|
|
204
40
|
|
|
205
|
-
|
|
41
|
+
## Documentation
|
|
206
42
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
| Error | Fix |
|
|
210
|
-
|-------|-----|
|
|
211
|
-
| "Not in a git repository" | Run `git init && git remote add origin ...` |
|
|
212
|
-
| "Vault not found" | Run `keyway init` |
|
|
213
|
-
| "No access to repository" | Check you're a GitHub collaborator |
|
|
214
|
-
|
|
215
|
-
Run `keyway doctor` for full diagnostics.
|
|
216
|
-
|
|
217
|
-
<br/>
|
|
218
|
-
|
|
219
|
-
## Links
|
|
220
|
-
|
|
221
|
-
- **Website:** [keyway.sh](https://keyway.sh)
|
|
222
|
-
- **Docs:** [docs.keyway.sh](https://docs.keyway.sh)
|
|
223
|
-
- **Issues:** [GitHub Issues](https://github.com/keywaysh/cli/issues)
|
|
224
|
-
- **Email:** hello@keyway.sh
|
|
225
|
-
|
|
226
|
-
<br/>
|
|
43
|
+
Visit [docs.keyway.sh](https://docs.keyway.sh) for full documentation.
|
|
227
44
|
|
|
228
45
|
## License
|
|
229
46
|
|
|
230
|
-
MIT
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
<div align="center">
|
|
235
|
-
<sub>Built with ☕ by <a href="https://keyway.sh">Keyway</a></sub>
|
|
236
|
-
</div>
|
|
47
|
+
MIT
|
package/bin/keyway
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
const binaryName = process.platform === "win32" ? "keyway-bin.exe" : "keyway-bin";
|
|
8
|
+
const binaryPath = path.join(__dirname, binaryName);
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(binaryPath)) {
|
|
11
|
+
console.error("keyway binary not found. Try reinstalling: npm install @keywaysh/cli");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
env: process.env,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error("Failed to run keyway:", result.error.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.exit(result.status || 0);
|
package/package.json
CHANGED
|
@@ -1,69 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keywaysh/cli",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "GitHub-native secrets management CLI",
|
|
6
5
|
"bin": {
|
|
7
|
-
"keyway": "./
|
|
6
|
+
"keyway": "./bin/keyway"
|
|
8
7
|
},
|
|
9
|
-
"main": "./dist/cli.js",
|
|
10
|
-
"files": [
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
8
|
"scripts": {
|
|
14
|
-
"
|
|
15
|
-
"dev:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 KEYWAY_API_URL=https://localhost/api pnpm exec tsx src/cli.ts",
|
|
16
|
-
"build": "pnpm exec tsup",
|
|
17
|
-
"build:watch": "pnpm exec tsup --watch",
|
|
18
|
-
"prepublishOnly": "pnpm run build",
|
|
19
|
-
"test": "pnpm exec vitest run",
|
|
20
|
-
"test:watch": "pnpm exec vitest",
|
|
21
|
-
"test:coverage": "pnpm exec vitest run --coverage",
|
|
22
|
-
"release": "npm version patch && git push && git push --tags",
|
|
23
|
-
"release:minor": "npm version minor && git push && git push --tags",
|
|
24
|
-
"release:major": "npm version major && git push && git push --tags"
|
|
9
|
+
"postinstall": "node scripts/install.js"
|
|
25
10
|
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"secrets",
|
|
28
|
-
"env",
|
|
29
|
-
"keyway",
|
|
30
|
-
"cli",
|
|
31
|
-
"devops"
|
|
32
|
-
],
|
|
33
|
-
"author": "Nicolas Ritouet",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"homepage": "https://keyway.sh",
|
|
36
11
|
"repository": {
|
|
37
12
|
"type": "git",
|
|
38
|
-
"url": "https://github.com/keywaysh/cli.git"
|
|
13
|
+
"url": "git+https://github.com/keywaysh/cli-go.git"
|
|
39
14
|
},
|
|
15
|
+
"homepage": "https://keyway.sh",
|
|
40
16
|
"bugs": {
|
|
41
|
-
"url": "https://github.com/keywaysh/cli/issues"
|
|
17
|
+
"url": "https://github.com/keywaysh/cli-go/issues"
|
|
42
18
|
},
|
|
43
|
-
"
|
|
19
|
+
"keywords": [
|
|
20
|
+
"secrets",
|
|
21
|
+
"cli",
|
|
22
|
+
"github",
|
|
23
|
+
"env",
|
|
24
|
+
"environment-variables",
|
|
25
|
+
"dotenv",
|
|
26
|
+
"secrets-management"
|
|
27
|
+
],
|
|
28
|
+
"author": "Keyway <hello@keyway.sh>",
|
|
29
|
+
"license": "MIT",
|
|
44
30
|
"engines": {
|
|
45
|
-
"node": ">=
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"@octokit/rest": "^22.0.1",
|
|
49
|
-
"balanced-match": "^3.0.1",
|
|
50
|
-
"commander": "^14.0.0",
|
|
51
|
-
"conf": "^15.0.2",
|
|
52
|
-
"libsodium-wrappers": "^0.7.15",
|
|
53
|
-
"open": "^11.0.0",
|
|
54
|
-
"picocolors": "^1.1.1",
|
|
55
|
-
"posthog-node": "^3.5.0",
|
|
56
|
-
"@clack/prompts": "^0.9.1"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@types/balanced-match": "^3.0.2",
|
|
60
|
-
"@types/libsodium-wrappers": "^0.7.14",
|
|
61
|
-
"@types/node": "^24.2.0",
|
|
62
|
-
"@vitest/coverage-v8": "^3.0.0",
|
|
63
|
-
"msw": "^2.12.4",
|
|
64
|
-
"tsup": "^8.5.0",
|
|
65
|
-
"tsx": "^4.20.3",
|
|
66
|
-
"typescript": "^5.9.2",
|
|
67
|
-
"vitest": "^3.2.4"
|
|
31
|
+
"node": ">=14"
|
|
68
32
|
}
|
|
69
33
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync, spawnSync } = require("child_process");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
|
|
10
|
+
const VERSION = require("../package.json").version;
|
|
11
|
+
const REPO = "keywaysh/cli-go";
|
|
12
|
+
const BINARY_NAME = "keyway";
|
|
13
|
+
|
|
14
|
+
const PLATFORMS = {
|
|
15
|
+
"darwin-arm64": "darwin_arm64",
|
|
16
|
+
"darwin-x64": "darwin_amd64",
|
|
17
|
+
"linux-arm64": "linux_arm64",
|
|
18
|
+
"linux-x64": "linux_amd64",
|
|
19
|
+
"win32-x64": "windows_amd64",
|
|
20
|
+
"win32-arm64": "windows_arm64",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getPlatformKey() {
|
|
24
|
+
return `${process.platform}-${process.arch}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getBinaryPath() {
|
|
28
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
29
|
+
// Use keyway-bin to avoid conflict with the wrapper script
|
|
30
|
+
const binaryName = process.platform === "win32" ? `${BINARY_NAME}-bin.exe` : `${BINARY_NAME}-bin`;
|
|
31
|
+
return path.join(binDir, binaryName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fetch(url) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const request = https.get(url, (response) => {
|
|
37
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
38
|
+
// Follow redirect
|
|
39
|
+
fetch(response.headers.location).then(resolve).catch(reject);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (response.statusCode !== 200) {
|
|
44
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const chunks = [];
|
|
49
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
50
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
51
|
+
response.on("error", reject);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
request.on("error", reject);
|
|
55
|
+
request.setTimeout(60000, () => {
|
|
56
|
+
request.destroy();
|
|
57
|
+
reject(new Error("Request timeout"));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function downloadChecksum() {
|
|
63
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
|
|
64
|
+
try {
|
|
65
|
+
const data = await fetch(url);
|
|
66
|
+
return data.toString();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn("Warning: Could not download checksums file");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function verifyChecksum(buffer, checksums, filename) {
|
|
74
|
+
if (!checksums) return true;
|
|
75
|
+
|
|
76
|
+
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
77
|
+
const expectedLine = checksums.split("\n").find((line) => line.includes(filename));
|
|
78
|
+
|
|
79
|
+
if (!expectedLine) {
|
|
80
|
+
console.warn(`Warning: No checksum found for ${filename}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const expectedHash = expectedLine.split(/\s+/)[0];
|
|
85
|
+
if (hash !== expectedHash) {
|
|
86
|
+
throw new Error(`Checksum mismatch for ${filename}\nExpected: ${expectedHash}\nGot: ${hash}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function extractTarGz(buffer, destDir) {
|
|
93
|
+
const tmpFile = path.join(os.tmpdir(), `keyway-${Date.now()}.tar.gz`);
|
|
94
|
+
fs.writeFileSync(tmpFile, buffer);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" });
|
|
98
|
+
} finally {
|
|
99
|
+
fs.unlinkSync(tmpFile);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function extractZip(buffer, destDir) {
|
|
104
|
+
const tmpFile = path.join(os.tmpdir(), `keyway-${Date.now()}.zip`);
|
|
105
|
+
fs.writeFileSync(tmpFile, buffer);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Try powershell on Windows
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
execSync(
|
|
111
|
+
`powershell -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${destDir}' -Force"`,
|
|
112
|
+
{ stdio: "pipe" }
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" });
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
fs.unlinkSync(tmpFile);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function install() {
|
|
123
|
+
const platformKey = getPlatformKey();
|
|
124
|
+
const target = PLATFORMS[platformKey];
|
|
125
|
+
|
|
126
|
+
if (!target) {
|
|
127
|
+
console.error(`\nUnsupported platform: ${platformKey}`);
|
|
128
|
+
console.error(`Supported platforms: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
129
|
+
console.error(`\nYou can install manually from: https://github.com/${REPO}/releases`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
134
|
+
const binaryPath = getBinaryPath();
|
|
135
|
+
|
|
136
|
+
// Check if binary already exists and is correct version
|
|
137
|
+
if (fs.existsSync(binaryPath)) {
|
|
138
|
+
try {
|
|
139
|
+
const result = spawnSync(binaryPath, ["--version"], { encoding: "utf8" });
|
|
140
|
+
if (result.stdout && result.stdout.includes(VERSION)) {
|
|
141
|
+
console.log(`keyway v${VERSION} already installed`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
// Binary exists but couldn't run, reinstall
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isWindows = process.platform === "win32";
|
|
150
|
+
const ext = isWindows ? "zip" : "tar.gz";
|
|
151
|
+
const filename = `${BINARY_NAME}_${VERSION}_${target}.${ext}`;
|
|
152
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${filename}`;
|
|
153
|
+
|
|
154
|
+
console.log(`Downloading keyway v${VERSION} for ${target}...`);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Download checksums and binary in parallel
|
|
158
|
+
const [checksums, archiveBuffer] = await Promise.all([downloadChecksum(), fetch(url)]);
|
|
159
|
+
|
|
160
|
+
// Verify checksum
|
|
161
|
+
verifyChecksum(archiveBuffer, checksums, filename);
|
|
162
|
+
|
|
163
|
+
// Create bin directory
|
|
164
|
+
if (!fs.existsSync(binDir)) {
|
|
165
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Extract to temp dir first, then move binary
|
|
169
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "keyway-"));
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
if (isWindows) {
|
|
173
|
+
await extractZip(archiveBuffer, tmpDir);
|
|
174
|
+
} else {
|
|
175
|
+
await extractTarGz(archiveBuffer, tmpDir);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find and move the binary (archive contains 'keyway', we save as 'keyway-bin')
|
|
179
|
+
const archiveBinaryName = isWindows ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
180
|
+
const extractedBinary = path.join(tmpDir, archiveBinaryName);
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
183
|
+
throw new Error(`Binary not found in archive: ${archiveBinaryName}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove old binary if exists
|
|
187
|
+
if (fs.existsSync(binaryPath)) {
|
|
188
|
+
fs.unlinkSync(binaryPath);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fs.copyFileSync(extractedBinary, binaryPath);
|
|
192
|
+
|
|
193
|
+
// Make executable on Unix
|
|
194
|
+
if (!isWindows) {
|
|
195
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`Successfully installed keyway v${VERSION}`);
|
|
199
|
+
} finally {
|
|
200
|
+
// Cleanup temp directory
|
|
201
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(`\nFailed to install keyway: ${error.message}`);
|
|
205
|
+
console.error(`\nYou can install manually from: https://github.com/${REPO}/releases`);
|
|
206
|
+
console.error(`Or use: curl -fsSL https://keyway.sh/install.sh | sh`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Only run if called directly (not required as module)
|
|
212
|
+
if (require.main === module) {
|
|
213
|
+
install();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { install, getBinaryPath };
|