@devstationlabs/cli 0.1.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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@devstationlabs/cli",
3
+ "version": "0.1.1",
4
+ "description": "The DevStation coding agent, in your terminal.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "devstation": "./bin/devstation.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "scripts/",
13
+ "devstation.js",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/linoxbt/dev-shipyard.git"
22
+ },
23
+ "homepage": "https://devstation.online",
24
+ "keywords": [
25
+ "coding-agent",
26
+ "cli",
27
+ "ai",
28
+ "devstation"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "postinstall": "node scripts/postinstall.mjs"
35
+ }
36
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import { targetFor } from "./target.mjs";
8
+
9
+ // Fetch the standalone binary for this machine, once, at install time.
10
+ //
11
+ // The alternative was what this package used to do: ship JavaScript and shell
12
+ // out to Bun, because the project index is SQLite through `bun:sqlite`. That
13
+ // made "npm install -g" work only on machines that already had Bun, which is
14
+ // not what installing a CLI from npm is supposed to mean.
15
+ //
16
+ // Publishing five ~90MB platform packages as optionalDependencies -- the
17
+ // esbuild approach, and the better one when the binaries are small -- would put
18
+ // nearly half a gigabyte on the registry for every release. So this downloads
19
+ // the one binary this machine needs from the GitHub release that matches the
20
+ // package version, and verifies it against the published SHA256SUMS before
21
+ // putting it anywhere. An unverified binary is not installed at all.
22
+ //
23
+ // If it cannot run, it does not fail the install: `npm i` failing on a network
24
+ // blip is worse than a clear message the first time you run the command. The
25
+ // launcher says exactly what happened and how to fix it by hand.
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const root = join(here, "..");
29
+ const require = createRequire(import.meta.url);
30
+ const { version } = require(join(root, "package.json"));
31
+
32
+ const REPO = process.env.DEVSTATION_REPO ?? "linoxbt/dev-shipyard";
33
+ const TAG = process.env.DEVSTATION_VERSION ?? `v${version}`;
34
+ const BASE = `https://github.com/${REPO}/releases/download/${TAG}`;
35
+
36
+ function note(message) {
37
+ process.stderr.write(`devstation: ${message}\n`);
38
+ }
39
+
40
+ async function get(url) {
41
+ const res = await fetch(url, { redirect: "follow" });
42
+ if (!res.ok) throw new Error(`${res.status} from ${url}`);
43
+ return res;
44
+ }
45
+
46
+ async function main() {
47
+ if (process.env.DEVSTATION_SKIP_DOWNLOAD === "1") return;
48
+
49
+ const target = targetFor();
50
+ if (!target) {
51
+ note(
52
+ `no prebuilt binary for ${process.platform}-${process.arch}. ` +
53
+ "The command will fall back to Bun if you have it.",
54
+ );
55
+ return;
56
+ }
57
+
58
+ const destination = join(here, target);
59
+ if (existsSync(destination)) return;
60
+
61
+ // Checksums first: a binary that arrives before anything can vouch for it is
62
+ // a binary that gets run before anything can vouch for it.
63
+ const sums = await (await get(`${BASE}/SHA256SUMS`)).text();
64
+ const expected = sums
65
+ .split("\n")
66
+ .map((line) => line.trim().split(/\s+/))
67
+ .find(([, name]) => name === target)?.[0];
68
+ if (!expected) throw new Error(`no checksum published for ${target} in ${TAG}`);
69
+
70
+ const body = Buffer.from(await (await get(`${BASE}/${target}`)).arrayBuffer());
71
+ const actual = createHash("sha256").update(body).digest("hex");
72
+ if (actual !== expected) {
73
+ throw new Error(
74
+ `checksum mismatch for ${target}\n expected: ${expected}\n actual: ${actual}`,
75
+ );
76
+ }
77
+
78
+ // Written beside the target and renamed, so an interrupted install cannot
79
+ // leave a half-written binary that looks complete.
80
+ mkdirSync(here, { recursive: true });
81
+ const partial = `${destination}.partial`;
82
+ writeFileSync(partial, body);
83
+ chmodSync(partial, 0o755);
84
+ renameSync(partial, destination);
85
+ try {
86
+ unlinkSync(`${destination}.partial`);
87
+ } catch {
88
+ // Already renamed away; nothing to clean up.
89
+ }
90
+ }
91
+
92
+ main().catch((error) => {
93
+ note(
94
+ `could not install the binary (${error instanceof Error ? error.message : error}).\n` +
95
+ " The command will explain what to do when you run it. Nothing is broken yet.",
96
+ );
97
+ // Deliberately not a failed install.
98
+ });
@@ -0,0 +1,22 @@
1
+ // Which release asset this machine needs.
2
+ //
3
+ // Shared by the installer and the launcher so they cannot disagree about the
4
+ // name: one of them guessing differently would download a file the other never
5
+ // looks for, and the symptom would be a reinstall on every run.
6
+
7
+ export const TARGETS = {
8
+ "linux-x64": "devstation-linux-x64",
9
+ "linux-arm64": "devstation-linux-arm64",
10
+ "darwin-arm64": "devstation-darwin-arm64",
11
+ "darwin-x64": "devstation-darwin-x64",
12
+ "win32-x64": "devstation-windows-x64.exe",
13
+ };
14
+
15
+ export function targetFor(platform = process.platform, arch = process.arch) {
16
+ return TARGETS[`${platform}-${arch}`] ?? null;
17
+ }
18
+
19
+ /** Where the installed binary lives inside the package. */
20
+ export function binaryPath(here, target) {
21
+ return `${here}/${target}`;
22
+ }