@specpin/cli 0.0.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 +54 -0
- package/bin/specpin.mjs +37 -0
- package/package.json +39 -0
- package/scripts/download.mjs +94 -0
- package/scripts/postinstall.mjs +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @specpin/cli
|
|
2
|
+
|
|
3
|
+
The Specpin sidecar CLI, distributed via npm. Installing this package downloads
|
|
4
|
+
the prebuilt Go binary matching your OS and CPU from the matching GitHub Release
|
|
5
|
+
and exposes it as the `specpin` command.
|
|
6
|
+
|
|
7
|
+
Specpin pins business specifications onto the elements of a running web UI. The
|
|
8
|
+
sidecar serves your repo's `.specs/` over localhost so the Specpin browser
|
|
9
|
+
extension can match and render them. See the
|
|
10
|
+
[project README](https://github.com/lamngockhuong/specpin).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Global:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g @specpin/cli
|
|
18
|
+
# or
|
|
19
|
+
pnpm add -g @specpin/cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
One-off, no install:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @specpin/cli serve
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
specpin init # scaffold .specs/ in the current repo
|
|
32
|
+
specpin serve # serve .specs/ over localhost (prints the bearer token)
|
|
33
|
+
specpin validate # lint the spec corpus (CI-friendly)
|
|
34
|
+
specpin --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
- The npm package version matches the CLI release version. Postinstall fetches
|
|
40
|
+
`specpin-<os>-<arch>` from `cli-v<version>` on GitHub Releases and verifies its
|
|
41
|
+
SHA-256 against the published `checksums.txt`.
|
|
42
|
+
- If postinstall is skipped (`--ignore-scripts`) or offline, the binary is
|
|
43
|
+
fetched and verified the first time you run `specpin`.
|
|
44
|
+
|
|
45
|
+
### Supported platforms
|
|
46
|
+
|
|
47
|
+
| OS | Architectures |
|
|
48
|
+
| ------- | -------------- |
|
|
49
|
+
| Linux | amd64, arm64 |
|
|
50
|
+
| macOS | amd64, arm64 |
|
|
51
|
+
| Windows | amd64 |
|
|
52
|
+
|
|
53
|
+
On other platforms, build from source: see
|
|
54
|
+
[`apps/cli`](https://github.com/lamngockhuong/specpin/tree/main/apps/cli).
|
package/bin/specpin.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher for the `specpin` command. Execs the platform-matched Go binary,
|
|
3
|
+
// forwarding args, stdio, and exit code. If the binary is missing (e.g. install
|
|
4
|
+
// ran with --ignore-scripts, or postinstall hit a network error), it is fetched
|
|
5
|
+
// and verified on first use.
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { binaryPath, ensureBinary } from "../scripts/download.mjs";
|
|
10
|
+
|
|
11
|
+
async function resolveBinary() {
|
|
12
|
+
const path = binaryPath();
|
|
13
|
+
if (existsSync(path)) return path;
|
|
14
|
+
console.error("specpin: downloading binary (first run)...");
|
|
15
|
+
return ensureBinary();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let bin;
|
|
19
|
+
try {
|
|
20
|
+
bin = await resolveBinary();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(err.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
27
|
+
child.on("error", (err) => {
|
|
28
|
+
console.error(`specpin: failed to launch binary (${err.message})`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
child.on("exit", (code, signal) => {
|
|
32
|
+
if (signal) {
|
|
33
|
+
process.kill(process.pid, signal);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
process.exit(code ?? 0);
|
|
37
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@specpin/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Specpin sidecar CLI: init + serve the .specs/ knowledge layer for the Specpin browser extension. Installs the matching prebuilt Go binary.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"specpin",
|
|
7
|
+
"cli",
|
|
8
|
+
"sidecar",
|
|
9
|
+
"specs",
|
|
10
|
+
"documentation"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/lamngockhuong/specpin/tree/main/apps/cli",
|
|
13
|
+
"bugs": "https://github.com/lamngockhuong/specpin/issues",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/lamngockhuong/specpin.git",
|
|
18
|
+
"directory": "apps/cli/npm"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"specpin": "bin/specpin.mjs"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"scripts",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Resolves the platform-matched specpin binary and downloads it from the GitHub
|
|
2
|
+
// Release that matches this package's version (tag `cli-v<version>`), verifying
|
|
3
|
+
// the published SHA-256 checksum before it is trusted. Shared by the postinstall
|
|
4
|
+
// step and the bin launcher's lazy fallback (so `--ignore-scripts` installs still
|
|
5
|
+
// self-heal on first run).
|
|
6
|
+
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const REPO = "lamngockhuong/specpin";
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkgRoot = join(here, "..");
|
|
15
|
+
|
|
16
|
+
// process.platform / process.arch -> Go release naming used by release-cli.yml.
|
|
17
|
+
const GOOS = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
18
|
+
const GOARCH = { x64: "amd64", arm64: "arm64" };
|
|
19
|
+
|
|
20
|
+
// Combos actually published by the release matrix.
|
|
21
|
+
const SUPPORTED = new Set([
|
|
22
|
+
"linux-amd64",
|
|
23
|
+
"linux-arm64",
|
|
24
|
+
"darwin-amd64",
|
|
25
|
+
"darwin-arm64",
|
|
26
|
+
"windows-amd64",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export function getTarget() {
|
|
30
|
+
const goos = GOOS[process.platform];
|
|
31
|
+
const goarch = GOARCH[process.arch];
|
|
32
|
+
if (!goos || !goarch || !SUPPORTED.has(`${goos}-${goarch}`)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`specpin: unsupported platform ${process.platform}/${process.arch}. ` +
|
|
35
|
+
"Prebuilt binaries cover linux/darwin (amd64, arm64) and windows (amd64). " +
|
|
36
|
+
`Build from source instead: https://github.com/${REPO}/tree/main/apps/cli`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const ext = goos === "windows" ? ".exe" : "";
|
|
40
|
+
return { goos, goarch, asset: `specpin-${goos}-${goarch}${ext}`, ext };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function binaryPath() {
|
|
44
|
+
const { ext } = getTarget();
|
|
45
|
+
return join(pkgRoot, "binary", `specpin${ext}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function version() {
|
|
49
|
+
return JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fetchBuffer(url) {
|
|
53
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`specpin: download failed (${res.status} ${res.statusText}) for ${url}`);
|
|
56
|
+
}
|
|
57
|
+
return Buffer.from(await res.arrayBuffer());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse a `sha256␠␠filename` line out of checksums.txt for the wanted asset.
|
|
61
|
+
function expectedSha(checksums, asset) {
|
|
62
|
+
for (const line of checksums.split("\n")) {
|
|
63
|
+
const [sha, name] = line.trim().split(/\s+/);
|
|
64
|
+
if (name === asset) return sha;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`specpin: ${asset} not listed in checksums.txt`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Download the matched binary into binary/ and verify its checksum. No-op if a
|
|
70
|
+
// verified binary is already present (unless force).
|
|
71
|
+
export async function ensureBinary({ force = false } = {}) {
|
|
72
|
+
const out = binaryPath();
|
|
73
|
+
if (!force && existsSync(out)) return out;
|
|
74
|
+
|
|
75
|
+
const { asset } = getTarget();
|
|
76
|
+
const v = version();
|
|
77
|
+
const base = `https://github.com/${REPO}/releases/download/cli-v${v}`;
|
|
78
|
+
|
|
79
|
+
const [bin, checksums] = await Promise.all([
|
|
80
|
+
fetchBuffer(`${base}/${asset}`),
|
|
81
|
+
fetchBuffer(`${base}/checksums.txt`).then((b) => b.toString("utf8")),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const want = expectedSha(checksums, asset);
|
|
85
|
+
const got = createHash("sha256").update(bin).digest("hex");
|
|
86
|
+
if (got !== want) {
|
|
87
|
+
throw new Error(`specpin: checksum mismatch for ${asset}\n expected ${want}\n got ${got}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
91
|
+
writeFileSync(out, bin);
|
|
92
|
+
chmodSync(out, 0o755);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Runs after `npm install`: fetches the platform-matched specpin binary so the
|
|
2
|
+
// `specpin` command works immediately. Failures here are non-fatal, the bin
|
|
3
|
+
// launcher retries the download on first run, so installs under restricted
|
|
4
|
+
// networks (or `--ignore-scripts`) still self-heal later.
|
|
5
|
+
|
|
6
|
+
import { ensureBinary } from "./download.mjs";
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const out = await ensureBinary();
|
|
10
|
+
console.log(`specpin: installed binary at ${out}`);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
console.warn(`specpin: postinstall could not fetch the binary (${err.message}).`);
|
|
13
|
+
console.warn("specpin: it will be downloaded automatically the first time you run `specpin`.");
|
|
14
|
+
}
|