coordigent 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
1
+ # Coordigent npm launcher
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Runs Coordigent's `coord` CLI from npm:
4
+
5
+ ```sh
6
+ npx coordigent connect # once, or:
7
+ npm i -g coordigent && coordigent connect
8
+ ```
9
+
10
+ (Not on npm yet: `package.json` stays `"private": true` until the trusted publisher is set up;
11
+ see [Publishing](#publishing).)
12
+
13
+ The package holds one Node file and no binaries. On first run it downloads `coord` and
14
+ `coord-mcp` for this machine from the server's `/install/<artifact>`, checks each against the
15
+ SHA-256 published in `/install/version`, and puts them where the installers do. After that, and on
16
+ every later run, it runs `coord` with the same arguments and exits with its exit code. Because the
17
+ binaries live where `install.sh` puts them, `coord update` and every MCP config keep working
18
+ unchanged.
19
+
20
+ - **Platforms:** macOS arm64 and x64 (an x64 Node under Rosetta gets the arm64 build), glibc Linux
21
+ x64 and arm64, Windows x64. Anything else, and Alpine/musl, exits 1 before downloading.
22
+ - **Where:** `~/.tower/bin`, or `%LOCALAPPDATA%\Coordigent\bin` on Windows (as `install.ps1`).
23
+ `TOWER_BIN_DIR` overrides both. The directory is not added to `PATH`; `coordigent <command>` is.
24
+ - **Which server:** `COORDIGENT_SERVER_URL`, then `TOWER_SERVER_URL`, then the `serverUrl` saved in
25
+ `~/.tower/config.json`, then `https://coordigent.com`: the order `coord` itself uses. A machine
26
+ with no config yet gets one naming that server, with no token, as `install.sh` writes.
27
+ - **Checks:** both binaries are downloaded and checked before either is written, so a refusal leaves
28
+ nothing installed. A digest mismatch, a `429` from the per-address download limit ("Try again in
29
+ N minute(s)") and an unreachable server each exit 1 with one message. A server that publishes no
30
+ checksum installs with an "installing unverified" warning, as `install.sh` does. What the
31
+ checksums do and do not protect against is in
32
+ [docs/operations.md](../../docs/operations.md#installer).
33
+ - **No install scripts.** Nothing runs at `npm install`; `--ignore-scripts` changes nothing.
34
+
35
+ ## Maintaining it
36
+
37
+ Tests: `bun test packaging/npm` from the repo root (a local fake server stands in for
38
+ `/install`). CI runs `npm pack --dry-run` on it (`.github/workflows/ci.yml`, job `npm-package`)
39
+ and fails if the tarball holds anything but `bin/coordigent.js`, `README.md` and `package.json`,
40
+ or gains an install-time script. Both that check and the release checks are
41
+ `scripts/release-checks.js`, which is not shipped.
42
+
43
+ ## Publishing
44
+
45
+ `.github/workflows/publish-npm.yml` publishes on a tag `npm-v<version>` (the `v*` tags publish the
46
+ container image and never touch npm), or by hand from Actions with an existing tag. It uses npm
47
+ trusted publishing: no `NPM_TOKEN`; npm trades the job's GitHub OIDC token for a one-time
48
+ credential, and `npm publish --access public` publishes it (no provenance: npm only attests public source repos, and this repo is private). Before
49
+ publishing, `scripts/release-checks.js release <tag>` refuses, with the reason, when:
50
+
51
+ - the tag is not exactly `npm-v` plus `package.json`'s `version`;
52
+ - the name is not `coordigent`;
53
+ - `package.json` still has `"private": true`. It stays until the steps below are done, so
54
+ until then every run of the workflow fails at that check and publishes nothing.
55
+
56
+ ### Turning it on (Ali, once)
57
+
58
+ 1. On npmjs.com, sign in as the account that will own `coordigent`, with two-factor
59
+ authentication on. (`npm view coordigent` returned 404 on 2026-10-17: the name was free.)
60
+ 2. npmjs.com sets a trusted publisher on the package's own settings page, so the package must
61
+ exist first. If **coordigent** is not yet on npm, publish the current version once by hand:
62
+ in a clean checkout of `main`, `cd packaging/npm`, delete the `"private": true` line locally
63
+ (do not commit it), run `npm publish --access public` and approve with 2FA, then
64
+ `git checkout package.json`. That one version has no provenance; every later one does.
65
+ 3. Open **npmjs.com → coordigent → Settings → Trusted Publisher**, choose **GitHub Actions**, and
66
+ enter: organization or user `startupproj`, repository `coordigent`, workflow filename
67
+ `publish-npm.yml`, environment empty. Save.
68
+ 4. On the same page, under **Publishing access**, choose **Require two-factor authentication and
69
+ disallow tokens**. Trusted publishing still works; a leaked token cannot publish.
70
+ 5. Open a PR that removes `"private": true` from `packaging/npm/package.json`, and bumps
71
+ `version` if step 2 published the current one. Merge it.
72
+ 6. Tag the merge commit and push the tag:
73
+ `git tag npm-v<version> <commit> && git push origin npm-v<version>`. The workflow publishes;
74
+ the version page on npmjs.com then shows its provenance, linked to that run.
75
+
76
+ Each later release: bump `version` in a PR, merge, push `npm-v<version>`.
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ // The npm launcher: `npx coordigent <args>` or `npm i -g` and `coordigent <args>`.
3
+ //
4
+ // It ships no binaries. On first run it downloads coord and coord-mcp for this
5
+ // machine from the server's /install/<artifact>, checks each against the SHA-256
6
+ // in /install/version the way scripts/install.sh does, and puts them where the
7
+ // installers do (~/.tower/bin, or %LOCALAPPDATA%\Coordigent\bin on Windows). Then,
8
+ // and on every later run, it runs `coord <args>`. Because the binaries live where
9
+ // the installers put them, `coord update` and every MCP config keep working.
10
+ //
11
+ // Plain Node with no dependencies and no install scripts: the download happens at
12
+ // run time, so `npm i --ignore-scripts` installs a working launcher.
13
+ "use strict";
14
+
15
+ const { spawn, execFileSync } = require("node:child_process");
16
+ const { createHash } = require("node:crypto");
17
+ const { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } = require("node:fs");
18
+ const { homedir } = require("node:os");
19
+ const { join } = require("node:path");
20
+
21
+ const HOSTED_SERVER_URL = "https://coordigent.com";
22
+ const BINARIES = ["coord", "coord-mcp"];
23
+
24
+ /** A failure the person can act on: printed as one message, exit 1, no stack. */
25
+ class LauncherError extends Error {}
26
+
27
+ function firstNonEmpty(...values) {
28
+ return values.find((value) => typeof value === "string" && value.trim() !== "");
29
+ }
30
+
31
+ function towerDir(env) {
32
+ return firstNonEmpty(env.TOWER_DIR) ?? join(homedir(), ".tower");
33
+ }
34
+
35
+ function savedServer(env) {
36
+ try {
37
+ return JSON.parse(readFileSync(join(towerDir(env), "config.json"), "utf8")).serverUrl;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ /** The server coord itself would use: COORDIGENT_SERVER_URL, TOWER_SERVER_URL, the saved config, the hosted one. */
44
+ function resolveServer(env = process.env) {
45
+ const url = firstNonEmpty(env.COORDIGENT_SERVER_URL, env.TOWER_SERVER_URL, savedServer(env)) ?? HOSTED_SERVER_URL;
46
+ return url.trim().replace(/\/+$/, "");
47
+ }
48
+
49
+ /** Where the installers put the binaries: TOWER_BIN_DIR, else ~/.tower/bin (install.sh) or %LOCALAPPDATA%\Coordigent\bin (install.ps1). */
50
+ function resolveBinDir(env = process.env, platform = process.platform) {
51
+ const override = firstNonEmpty(env.TOWER_BIN_DIR);
52
+ if (override) return override;
53
+ if (platform === "win32") {
54
+ return join(firstNonEmpty(env.LOCALAPPDATA) ?? join(homedir(), "AppData", "Local"), "Coordigent", "bin");
55
+ }
56
+ return join(homedir(), ".tower", "bin");
57
+ }
58
+
59
+ function underRosetta() {
60
+ try {
61
+ return execFileSync("sysctl", ["-n", "sysctl.proc_translated"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() === "1";
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function hasGlibc() {
68
+ return Boolean(process.report?.getReport?.().header?.glibcVersionRuntime);
69
+ }
70
+
71
+ /** The published artifact suffix for this machine; the same choices install.sh and install.ps1 make. */
72
+ function resolvePlatform({ platform = process.platform, arch = process.arch, rosetta = underRosetta, glibc = hasGlibc } = {}) {
73
+ const key = `${platform}-${arch}`;
74
+ switch (key) {
75
+ case "darwin-arm64":
76
+ return "darwin-arm64";
77
+ // An x64 Node under Rosetta on an Apple Silicon Mac: the native build is the right one.
78
+ case "darwin-x64":
79
+ return rosetta() ? "darwin-arm64" : "darwin-x64";
80
+ case "linux-x64":
81
+ case "linux-arm64":
82
+ if (!glibc()) {
83
+ throw new LauncherError(
84
+ "prebuilt Linux binaries require glibc; Alpine/musl is not supported.\n" +
85
+ "Use a glibc-based Linux distribution, or run Coordigent from source with Bun."
86
+ );
87
+ }
88
+ return key;
89
+ case "win32-x64":
90
+ return "windows-x64";
91
+ default:
92
+ throw new LauncherError(
93
+ `no prebuilt binary for ${key}.\n` +
94
+ "Supported: macOS arm64 and x64, Linux x64 and arm64 (glibc), Windows x64. Build from source with 'bun run build:cli'."
95
+ );
96
+ }
97
+ }
98
+
99
+ function exeSuffix(platform) {
100
+ return platform.startsWith("windows-") ? ".exe" : "";
101
+ }
102
+
103
+ async function request(fetchImpl, url, server) {
104
+ try {
105
+ return await fetchImpl(url);
106
+ } catch (error) {
107
+ throw new LauncherError(
108
+ `could not reach ${server} (${error?.cause?.code ?? error?.message ?? error}).\n` +
109
+ "Check the network, or point at another server with COORDIGENT_SERVER_URL. Nothing was installed."
110
+ );
111
+ }
112
+ }
113
+
114
+ /** The published digests, or {} when the server publishes none (an older server, or one without binaries). */
115
+ async function fetchChecksums(fetchImpl, server) {
116
+ try {
117
+ const response = await fetchImpl(`${server}/install/version`);
118
+ if (!response.ok) return {};
119
+ const checksums = (await response.json())?.checksums;
120
+ return checksums && typeof checksums === "object" ? checksums : {};
121
+ } catch {
122
+ return {};
123
+ }
124
+ }
125
+
126
+ function downloadFailure(response, url, platform) {
127
+ if (response.status === 429) {
128
+ const seconds = Number.parseInt(response.headers.get("retry-after") ?? "", 10);
129
+ const when = Number.isFinite(seconds) ? `in ${Math.max(1, Math.ceil(seconds / 60))} minute(s)` : "later";
130
+ return new LauncherError(`could not download ${url}\nToo many downloads from this address. Try again ${when}.`);
131
+ }
132
+ if (response.status === 404 && platform.startsWith("windows-")) {
133
+ return new LauncherError(`could not download ${url}\nWindows build not available yet.`);
134
+ }
135
+ return new LauncherError(
136
+ `could not download ${url} (HTTP ${response.status})\nThe server may not be publishing binaries. See docs/operations.md.`
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Downloads and checks every binary before writing any of them, so a refusal
142
+ * halfway leaves nothing behind. Nothing unverified is ever made executable.
143
+ */
144
+ async function install({ server, binDir, platform, fetchImpl, log }) {
145
+ const checksums = await fetchChecksums(fetchImpl, server);
146
+ const downloads = [];
147
+ for (const name of BINARIES) {
148
+ const artifact = `${name}-${platform}${exeSuffix(platform)}`;
149
+ const url = `${server}/install/${artifact}`;
150
+ log(`Downloading ${name} (${platform})`);
151
+ const response = await request(fetchImpl, url, server);
152
+ if (!response.ok) throw downloadFailure(response, url, platform);
153
+ const bytes = Buffer.from(await response.arrayBuffer());
154
+
155
+ const expected = checksums[artifact];
156
+ if (typeof expected === "string" && expected !== "") {
157
+ const actual = createHash("sha256").update(bytes).digest("hex");
158
+ if (actual !== expected.toLowerCase()) {
159
+ throw new LauncherError(
160
+ `${artifact} does not match the checksum the server published.\n` +
161
+ ` expected ${expected}\n got ${actual}\n` +
162
+ "Nothing was installed. This is what a tampered or truncated download looks like."
163
+ );
164
+ }
165
+ } else {
166
+ log(`coordigent: the server published no checksum for ${artifact}; installing unverified.`);
167
+ }
168
+ downloads.push({ name, bytes });
169
+ }
170
+
171
+ mkdirSync(binDir, { recursive: true });
172
+ for (const { name, bytes } of downloads) {
173
+ const target = join(binDir, `${name}${exeSuffix(platform)}`);
174
+ const tmp = `${target}.download`;
175
+ writeFileSync(tmp, bytes, { mode: 0o755 });
176
+ chmodSync(tmp, 0o755);
177
+ renameSync(tmp, target);
178
+ }
179
+ log(`Installed coord and coord-mcp to ${binDir}`);
180
+ }
181
+
182
+ /** Like install.sh: a machine with no config yet records the server the binaries came from, with no token. */
183
+ function rememberServer(env, server) {
184
+ const config = join(towerDir(env), "config.json");
185
+ if (existsSync(config)) return;
186
+ mkdirSync(towerDir(env), { recursive: true });
187
+ writeFileSync(config, `{\n "serverUrl": "${server}"\n}\n`, { mode: 0o600, flag: "wx" });
188
+ }
189
+
190
+ function run(coord, args) {
191
+ const child = spawn(coord, args, { stdio: "inherit" });
192
+ // Ctrl-C reaches coord from the terminal already; stay alive until it exits.
193
+ process.on("SIGINT", () => {});
194
+ for (const signal of ["SIGTERM", "SIGHUP"]) process.on(signal, () => child.kill(signal));
195
+ child.on("error", (error) => {
196
+ process.stderr.write(`coordigent: could not run ${coord}: ${error.message}\n`);
197
+ process.exit(1);
198
+ });
199
+ child.on("exit", (code, signal) => {
200
+ if (signal) {
201
+ process.removeAllListeners(signal);
202
+ process.kill(process.pid, signal);
203
+ }
204
+ process.exit(code ?? 1);
205
+ });
206
+ }
207
+
208
+ async function main(args = process.argv.slice(2), env = process.env) {
209
+ const log = (line) => process.stderr.write(`${line}\n`);
210
+ try {
211
+ const binDir = resolveBinDir(env);
212
+ // The platform is resolved before anything is fetched or written, like install.sh.
213
+ const platform = resolvePlatform();
214
+ const suffix = exeSuffix(platform);
215
+ const coord = join(binDir, `coord${suffix}`);
216
+ if (!BINARIES.every((name) => existsSync(join(binDir, `${name}${suffix}`)))) {
217
+ const server = resolveServer(env);
218
+ await install({ server, binDir, platform, fetchImpl: fetch, log });
219
+ try {
220
+ rememberServer(env, server);
221
+ } catch {
222
+ // Optional, as in install.sh: coord login records the server too.
223
+ }
224
+ }
225
+ run(coord, args);
226
+ } catch (error) {
227
+ if (!(error instanceof LauncherError)) throw error;
228
+ log(`coordigent: ${error.message}`);
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ module.exports = { resolvePlatform, resolveServer, resolveBinDir };
234
+
235
+ if (require.main === module) main();
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "coordigent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Runs Coordigent's coord CLI: downloads the coord and coord-mcp binaries for this machine on first run, checks their SHA-256, and runs coord.",
5
+ "homepage": "https://coordigent.com",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/startupproj/coordigent.git",
9
+ "directory": "packaging/npm"
10
+ },
11
+ "bin": {
12
+ "coordigent": "bin/coordigent.js"
13
+ },
14
+ "files": [
15
+ "bin/coordigent.js"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "os": [
21
+ "darwin",
22
+ "linux",
23
+ "win32"
24
+ ],
25
+ "cpu": [
26
+ "x64",
27
+ "arm64"
28
+ ]
29
+ }