profilex-cli 0.2.3
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/LICENSE +1 -0
- package/README.md +16 -0
- package/bin/profilex.js +62 -0
- package/package.json +38 -0
- package/runtime/.gitkeep +1 -0
- package/scripts/postinstall.js +243 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# profilex-cli
|
|
2
|
+
|
|
3
|
+
Install ProfileX with npm or pnpm:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i -g profilex-cli
|
|
7
|
+
# or
|
|
8
|
+
pnpm add -g profilex-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package downloads the platform-specific ProfileX release binary during `postinstall`, verifies SHA256 checksums, verifies signed release metadata with Sigstore/cosign, and exposes the `profilex` command.
|
|
12
|
+
|
|
13
|
+
Configuration:
|
|
14
|
+
|
|
15
|
+
- `PROFILEX_REPO`: override GitHub repo for releases (default: `derekurban/profilex-cli`)
|
|
16
|
+
- `PROFILEX_VERIFY_SIGNATURES`: set `0` to disable signature verification (default: `1`)
|
package/bin/profilex.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const cp = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
function mapPlatform() {
|
|
9
|
+
switch (process.platform) {
|
|
10
|
+
case "win32":
|
|
11
|
+
return "windows";
|
|
12
|
+
case "darwin":
|
|
13
|
+
return "darwin";
|
|
14
|
+
case "linux":
|
|
15
|
+
return "linux";
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mapArch() {
|
|
22
|
+
switch (process.arch) {
|
|
23
|
+
case "x64":
|
|
24
|
+
return "amd64";
|
|
25
|
+
case "arm64":
|
|
26
|
+
return "arm64";
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function binaryPath() {
|
|
33
|
+
const os = mapPlatform();
|
|
34
|
+
const arch = mapArch();
|
|
35
|
+
const execName = os === "windows" ? "profilex.exe" : "profilex";
|
|
36
|
+
return path.join(__dirname, "..", "runtime", `${os}-${arch}`, execName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
let bin;
|
|
41
|
+
try {
|
|
42
|
+
bin = binaryPath();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`[profilex-npm] ${err.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(bin)) {
|
|
49
|
+
console.error("[profilex-npm] ProfileX binary is missing for this platform.");
|
|
50
|
+
console.error("[profilex-npm] Reinstall with: npm i -g profilex-cli");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const child = cp.spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
55
|
+
if (child.error) {
|
|
56
|
+
console.error(`[profilex-npm] Failed to launch binary: ${child.error.message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
process.exit(child.status ?? 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "profilex-cli",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "ProfileX CLI binary installer and launcher",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/derekurban/profilex-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/derekurban/profilex-cli",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/derekurban/profilex-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"profilex": "bin/profilex.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"postinstall": "node scripts/postinstall.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"scripts",
|
|
23
|
+
"runtime",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"profilex",
|
|
32
|
+
"claude",
|
|
33
|
+
"codex",
|
|
34
|
+
"cli",
|
|
35
|
+
"ai",
|
|
36
|
+
"profiles"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/runtime/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const https = require("node:https");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
const cp = require("node:child_process");
|
|
9
|
+
|
|
10
|
+
function mapPlatform() {
|
|
11
|
+
switch (process.platform) {
|
|
12
|
+
case "win32":
|
|
13
|
+
return "windows";
|
|
14
|
+
case "darwin":
|
|
15
|
+
return "darwin";
|
|
16
|
+
case "linux":
|
|
17
|
+
return "linux";
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapArch() {
|
|
24
|
+
switch (process.arch) {
|
|
25
|
+
case "x64":
|
|
26
|
+
return "amd64";
|
|
27
|
+
case "arm64":
|
|
28
|
+
return "arm64";
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readPackageVersion() {
|
|
35
|
+
const packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
37
|
+
return pkg.version;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTruthy(value, defaultValue = true) {
|
|
41
|
+
if (value == null || value === "") return defaultValue;
|
|
42
|
+
const lowered = String(value).toLowerCase();
|
|
43
|
+
return lowered === "1" || lowered === "true" || lowered === "yes" || lowered === "on";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fetchToFile(url, outPath) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const req = https.get(url, (res) => {
|
|
49
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
50
|
+
fetchToFile(res.headers.location, outPath).then(resolve).catch(reject);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (res.statusCode !== 200) {
|
|
54
|
+
reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const out = fs.createWriteStream(outPath);
|
|
58
|
+
res.pipe(out);
|
|
59
|
+
out.on("finish", () => {
|
|
60
|
+
out.close(resolve);
|
|
61
|
+
});
|
|
62
|
+
out.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
req.on("error", reject);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sha256File(filePath) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const hash = crypto.createHash("sha256");
|
|
71
|
+
const rs = fs.createReadStream(filePath);
|
|
72
|
+
rs.on("data", (d) => hash.update(d));
|
|
73
|
+
rs.on("error", reject);
|
|
74
|
+
rs.on("end", () => resolve(hash.digest("hex")));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function expectedChecksum(checksumsFile, assetName) {
|
|
79
|
+
const lines = fs.readFileSync(checksumsFile, "utf8").split(/\r?\n/);
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const m = line.match(/^([A-Fa-f0-9]{64})\s+(.+)$/);
|
|
82
|
+
if (!m) continue;
|
|
83
|
+
if (m[2].trim() === assetName) return m[1].toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function run(cmd, args) {
|
|
89
|
+
const res = cp.spawnSync(cmd, args, { stdio: "inherit" });
|
|
90
|
+
if (res.error) throw res.error;
|
|
91
|
+
if (res.status !== 0) throw new Error(`${cmd} exited with code ${res.status}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defaultCosignIdentityRegex(repo) {
|
|
95
|
+
const officialRepo = "derekurban/profilex-cli";
|
|
96
|
+
const legacyRepo = "derekurban2001/profilex-cli";
|
|
97
|
+
if (repo === officialRepo || repo === legacyRepo) {
|
|
98
|
+
return "^https://github.com/(derekurban/profilex-cli|derekurban2001/profilex-cli)/.github/workflows/release.yml@refs/tags/.*$";
|
|
99
|
+
}
|
|
100
|
+
return `^https://github.com/${repo}/.github/workflows/release.yml@refs/tags/.*$`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ensureCosign(tempDir, platform, arch) {
|
|
104
|
+
const existing = cp.spawnSync("cosign", ["version"], { stdio: "ignore" });
|
|
105
|
+
if (!existing.error && existing.status === 0) {
|
|
106
|
+
return "cosign";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cosignVersion = process.env.PROFILEX_COSIGN_VERSION || "v2.5.3";
|
|
110
|
+
const suffix = platform === "windows" ? ".exe" : "";
|
|
111
|
+
const asset = `cosign-${platform}-${arch}${suffix}`;
|
|
112
|
+
const outFile = path.join(tempDir, asset);
|
|
113
|
+
const url = `https://github.com/sigstore/cosign/releases/download/${cosignVersion}/${asset}`;
|
|
114
|
+
console.log(`[profilex-npm] cosign not found; downloading ${cosignVersion}`);
|
|
115
|
+
await fetchToFile(url, outFile);
|
|
116
|
+
if (platform !== "windows") {
|
|
117
|
+
fs.chmodSync(outFile, 0o755);
|
|
118
|
+
}
|
|
119
|
+
return outFile;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function verifyChecksumsSignature(tempDir, platform, arch, checksumsPath, sigPath, certPath, repo) {
|
|
123
|
+
if (!isTruthy(process.env.PROFILEX_VERIFY_SIGNATURES, true)) {
|
|
124
|
+
console.warn("[profilex-npm] Signature verification disabled via PROFILEX_VERIFY_SIGNATURES=0");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const identityRe =
|
|
128
|
+
process.env.PROFILEX_COSIGN_IDENTITY_RE ||
|
|
129
|
+
defaultCosignIdentityRegex(repo);
|
|
130
|
+
const oidcIssuer = process.env.PROFILEX_COSIGN_OIDC_ISSUER || "https://token.actions.githubusercontent.com";
|
|
131
|
+
const cosignBin = await ensureCosign(tempDir, platform, arch);
|
|
132
|
+
run(cosignBin, [
|
|
133
|
+
"verify-blob",
|
|
134
|
+
"--certificate",
|
|
135
|
+
certPath,
|
|
136
|
+
"--signature",
|
|
137
|
+
sigPath,
|
|
138
|
+
"--certificate-identity-regexp",
|
|
139
|
+
identityRe,
|
|
140
|
+
"--certificate-oidc-issuer",
|
|
141
|
+
oidcIssuer,
|
|
142
|
+
checksumsPath,
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function findBinary(rootDir, wantedName) {
|
|
147
|
+
const queue = [rootDir];
|
|
148
|
+
while (queue.length) {
|
|
149
|
+
const current = queue.shift();
|
|
150
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const full = path.join(current, entry.name);
|
|
153
|
+
if (entry.isDirectory()) {
|
|
154
|
+
queue.push(full);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (entry.isFile() && entry.name.toLowerCase() === wantedName.toLowerCase()) {
|
|
158
|
+
return full;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const version = readPackageVersion();
|
|
167
|
+
if (!version || version.includes("dev")) {
|
|
168
|
+
console.log("[profilex-npm] Development version detected; skipping binary download.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const repo = process.env.PROFILEX_REPO || "derekurban/profilex-cli";
|
|
173
|
+
const platform = mapPlatform();
|
|
174
|
+
const arch = mapArch();
|
|
175
|
+
const extension = platform === "windows" ? "zip" : "tar.gz";
|
|
176
|
+
const asset = `profilex_${version}_${platform}_${arch}.${extension}`;
|
|
177
|
+
const tag = `v${version}`;
|
|
178
|
+
const baseUrl = `https://github.com/${repo}/releases/download/${tag}`;
|
|
179
|
+
|
|
180
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "profilex-npm-"));
|
|
181
|
+
const assetPath = path.join(tempDir, asset);
|
|
182
|
+
const checksumsPath = path.join(tempDir, "checksums.txt");
|
|
183
|
+
const checksumsSigPath = path.join(tempDir, "checksums.txt.sig");
|
|
184
|
+
const checksumsCertPath = path.join(tempDir, "checksums.txt.pem");
|
|
185
|
+
const extractDir = path.join(tempDir, "extract");
|
|
186
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
187
|
+
|
|
188
|
+
console.log(`[profilex-npm] Downloading ${asset}`);
|
|
189
|
+
await fetchToFile(`${baseUrl}/${asset}`, assetPath);
|
|
190
|
+
await fetchToFile(`${baseUrl}/checksums.txt`, checksumsPath);
|
|
191
|
+
await fetchToFile(`${baseUrl}/checksums.txt.sig`, checksumsSigPath);
|
|
192
|
+
await fetchToFile(`${baseUrl}/checksums.txt.pem`, checksumsCertPath);
|
|
193
|
+
|
|
194
|
+
await verifyChecksumsSignature(
|
|
195
|
+
tempDir,
|
|
196
|
+
platform,
|
|
197
|
+
arch,
|
|
198
|
+
checksumsPath,
|
|
199
|
+
checksumsSigPath,
|
|
200
|
+
checksumsCertPath,
|
|
201
|
+
repo,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const expected = expectedChecksum(checksumsPath, asset);
|
|
205
|
+
if (!expected) {
|
|
206
|
+
throw new Error(`No checksum entry found for ${asset}`);
|
|
207
|
+
}
|
|
208
|
+
const actual = await sha256File(assetPath);
|
|
209
|
+
if (expected !== actual) {
|
|
210
|
+
throw new Error(`Checksum mismatch for ${asset}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (platform === "windows") {
|
|
214
|
+
run("powershell", [
|
|
215
|
+
"-NoProfile",
|
|
216
|
+
"-Command",
|
|
217
|
+
`Expand-Archive -Path '${assetPath}' -DestinationPath '${extractDir}' -Force`,
|
|
218
|
+
]);
|
|
219
|
+
} else {
|
|
220
|
+
run("tar", ["-xzf", assetPath, "-C", extractDir]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const binaryName = platform === "windows" ? "profilex.exe" : "profilex";
|
|
224
|
+
const foundBinary = findBinary(extractDir, binaryName);
|
|
225
|
+
if (!foundBinary) {
|
|
226
|
+
throw new Error(`Unable to find ${binaryName} in extracted archive`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const runtimeDir = path.join(__dirname, "..", "runtime", `${platform}-${arch}`);
|
|
230
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
231
|
+
const destBinary = path.join(runtimeDir, binaryName);
|
|
232
|
+
fs.copyFileSync(foundBinary, destBinary);
|
|
233
|
+
if (platform !== "windows") {
|
|
234
|
+
fs.chmodSync(destBinary, 0o755);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(`[profilex-npm] Installed ${binaryName} for ${platform}/${arch}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main().catch((err) => {
|
|
241
|
+
console.error(`[profilex-npm] Install failed: ${err.message}`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|