@trajectonomics/cli 0.0.0-stage → 0.0.1-bootstrap.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trajectonomics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # trajectonomics
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
+ The trajectonomics command line. The sources are Bun-only; a released binary needs nothing.
4
+
5
+ ## Install
6
+
7
+ Linux and macOS, one line (verifies the release checksum, installs `traj` to `~/.local/bin`,
8
+ or `$TRAJ_INSTALL_DIR`, and prints what it installed):
9
+
10
+ ```bash
11
+ curl -fsSL https://github.com/trajectonomics/cli/releases/latest/download/install.sh | sh
12
+ ```
13
+
14
+ Windows, one line (installs to `%LOCALAPPDATA%\traj\bin`, adds it to the user `PATH` once,
15
+ and needs a new shell afterwards):
16
+
17
+ ```powershell
18
+ irm https://github.com/trajectonomics/cli/releases/latest/download/install.ps1 | iex
19
+ ```
20
+
21
+ npm, on any machine with Node 20 or later, or with Bun (`npx` needs no Bun; the package
22
+ fetches the release binary on first run and caches it under `~/.traj/bin`):
23
+
24
+ ```bash
25
+ npx @trajectonomics/cli@latest --version
26
+ bunx @trajectonomics/cli@latest --version
27
+ ```
28
+
29
+ Which channel: all three end at the same compiled binary and the same version. Use the
30
+ script for a machine that should keep `traj` on `PATH`, npm when you already have Node or
31
+ Bun and want one command. Pin a release with `TRAJ_VERSION=X.Y.Z` for the script, or
32
+ `@X.Y.Z` for npm.
33
+
34
+ Uninstall by deleting the one file: `~/.local/bin/traj`, `%LOCALAPPDATA%\traj\bin`,
35
+ `~/.traj/bin` (the download cache), or nothing at all for `npx`.
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ bun install
41
+ bun run dev -- --help # run from source
42
+ bun test
43
+ bun run build # single-file binary at dist/traj
44
+ bun run lint # Biome, zero warnings
45
+ bun run format # Biome format + import order, writes files
46
+ bun run check # lint + typecheck + test + build, what CI runs
47
+ ```
48
+
49
+ From a checkout, run the CLI with `bun run dev -- …`. The npm launcher refuses the
50
+ `0.0.0` a checkout carries, because that version is never a release; set
51
+ `TRAJ_BINARY_PATH` to run a binary you built with `bun run build`.
52
+
53
+ ## Docs
54
+
55
+ - [`GLOSSARY.md`](GLOSSARY.md): the project's vocabulary.
56
+ - [`docs/adr/`](docs/adr/): design decisions and why.
57
+ - [`docs/runbooks/releasing.md`](docs/runbooks/releasing.md): cutting and repairing a release.
58
+ - [`ROADMAP.md`](ROADMAP.md): phases and recorded next steps.
59
+
60
+ ## Adding a command
61
+
62
+ Create `src/commands/<name>.ts` (or `src/commands/<group>/<name>.ts` for a
63
+ nested one) with `defineCommand`: a summary, an input schema, an output schema,
64
+ examples, the error codes it raises, and a `run` that returns `ok(data)` or
65
+ `fail(code)`. Register it in `src/commands/index.ts`, inside a `defineGroup` if
66
+ it is nested. Fields of the input schema that are not positionals become flags
67
+ automatically (`fromDate` is `--from-date`). A new failure is a new entry in the
68
+ registry in `src/errors.ts` with its exit class. Commands never format output or
69
+ exit; `traj help <path>` and `traj schema <path>` are generated from the same
70
+ schemas that validate them. Test through `run(argv, io, env)`, which needs no
71
+ process. See ADR 0015.
@@ -0,0 +1,9 @@
1
+ /** The launcher's pure module, which runs on Node as plain JavaScript. */
2
+ export declare const RELEASE_BASE: string
3
+ export declare const SUPPORTED: string[]
4
+ export declare function assetName(platform: string, arch: string): string
5
+ export declare function checksumFor(checksums: string, name: string): string
6
+ export declare function sha256(bytes: Uint8Array): string
7
+ export declare function verifyChecksum(checksums: string, name: string, bytes: Uint8Array): string
8
+ export declare function exitStatus(code: number | null, signal: string | null): number
9
+ export declare function releaseUrl(version: string, name: string, base?: string): string
@@ -0,0 +1,64 @@
1
+ /** Platform to release asset, checksums and release URLs. Plain JavaScript and `node:` only. */
2
+ import { createHash } from "node:crypto"
3
+ import { constants } from "node:os"
4
+
5
+ /** The repo's releases root: one release's assets sit under `download/v<version>`. */
6
+ export const RELEASE_BASE = "https://github.com/trajectonomics/cli/releases"
7
+
8
+ /** `process.platform`-`process.arch` to asset name. */
9
+ const ASSETS = {
10
+ "darwin-arm64": "traj-darwin-arm64",
11
+ "darwin-x64": "traj-darwin-x64",
12
+ "linux-x64": "traj-linux-x64",
13
+ "linux-arm64": "traj-linux-arm64",
14
+ "win32-x64": "traj-windows-x64.exe",
15
+ }
16
+
17
+ /** The `platform/arch` pairs a release has a binary for, in message order. */
18
+ export const SUPPORTED = Object.keys(ASSETS).map((key) => key.replace("-", "/"))
19
+
20
+ /** The one asset a release has for this platform and architecture. */
21
+ export function assetName(platform, arch) {
22
+ const asset = ASSETS[`${platform}-${arch}`]
23
+ if (asset === undefined) {
24
+ throw new Error(
25
+ `traj has no build for ${platform}/${arch}. Supported: ${SUPPORTED.join(", ")}.`,
26
+ )
27
+ }
28
+ return asset
29
+ }
30
+
31
+ /** `"<sha256> <name>"` per line, as the release job writes it. */
32
+ export function checksumFor(checksums, name) {
33
+ for (const line of checksums.split("\n")) {
34
+ const [digest, listed] = line.trim().split(/\s+/)
35
+ if (listed === name || listed === `*${name}`) return String(digest).toLowerCase()
36
+ }
37
+ throw new Error(`checksums.txt has no entry for ${name}.`)
38
+ }
39
+
40
+ export function sha256(bytes) {
41
+ return createHash("sha256").update(bytes).digest("hex")
42
+ }
43
+
44
+ /** The listed digest, after proving it is the digest of these bytes. */
45
+ export function verifyChecksum(checksums, name, bytes) {
46
+ const listed = checksumFor(checksums, name)
47
+ const actual = sha256(bytes)
48
+ if (actual !== listed) {
49
+ throw new Error(`checksum mismatch for ${name}: expected ${listed}, got ${actual}`)
50
+ }
51
+ return actual
52
+ }
53
+
54
+ /**
55
+ * What a shell reports for a signalled child: `128 + signal`. A normal exit passes
56
+ * its own code through.
57
+ */
58
+ export function exitStatus(code, signal) {
59
+ return signal === null ? (code ?? 1) : 128 + (constants.signals[signal] ?? 0)
60
+ }
61
+
62
+ export function releaseUrl(version, name, base = RELEASE_BASE) {
63
+ return `${base}/download/v${version}/${name}`
64
+ }
package/bin/traj.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /** The npm package's command: install the release binary for this package's version, then run it. */
3
+ import { spawn } from "node:child_process"
4
+ import {
5
+ chmodSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs"
13
+ import { homedir } from "node:os"
14
+ import { dirname, join } from "node:path"
15
+ import { setTimeout as sleep } from "node:timers/promises"
16
+ import { assetName, exitStatus, releaseUrl, verifyChecksum } from "./launcher.js"
17
+
18
+ const RETRY_MS = 5 * 60 * 1000
19
+ const MAX_WAIT_MS = 30 * 1000
20
+ const FORWARDED = ["SIGHUP", "SIGINT", "SIGTERM"]
21
+
22
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
23
+
24
+ /** A launcher failure is one plain line on stderr and exit 1, never a JSON envelope. */
25
+ function fail(message) {
26
+ process.stderr.write(`traj: ${message}\n`)
27
+ process.exitCode = 1
28
+ }
29
+
30
+ function run(binary, args) {
31
+ const child = spawn(binary, args, { stdio: "inherit" })
32
+ child.on("error", (error) => fail(`cannot run ${binary}: ${error.message}`))
33
+ for (const signal of FORWARDED) process.on(signal, () => child.kill(signal))
34
+ child.on("exit", (code, signal) => {
35
+ process.exitCode = exitStatus(code, signal)
36
+ })
37
+ }
38
+
39
+ /** One asset's bytes; only a 404 retries, because npm publishes minutes before the assets land. */
40
+ async function download(url) {
41
+ const deadline = Date.now() + RETRY_MS
42
+ let said = false
43
+ for (let wait = 1000; ; wait = Math.min(wait * 2, MAX_WAIT_MS)) {
44
+ const res = await fetch(url)
45
+ if (res.ok) return new Uint8Array(await res.arrayBuffer())
46
+ if (res.status !== 404) throw new Error(`${url} answered ${res.status}`)
47
+ if (Date.now() + wait > deadline) {
48
+ throw new Error(`${url} is not there yet; the release may still be building. Retry shortly.`)
49
+ }
50
+ if (!said) {
51
+ said = true
52
+ process.stderr.write(`traj: ${url} is not there yet; retrying for up to five minutes.\n`)
53
+ }
54
+ await sleep(wait)
55
+ }
56
+ }
57
+
58
+ /** A complete file at the cache path, never a half-written one: same directory, then a rename. */
59
+ function install(bytes, dest) {
60
+ mkdirSync(dirname(dest), { recursive: true })
61
+ const temp = join(dirname(dest), `.traj-${process.pid}-${Date.now()}`)
62
+ try {
63
+ writeFileSync(temp, bytes, { mode: 0o755 })
64
+ chmodSync(temp, 0o755)
65
+ try {
66
+ renameSync(temp, dest)
67
+ } catch (error) {
68
+ // A concurrent run won the race; its file is complete, so use it and drop ours.
69
+ if (!existsSync(dest)) throw error
70
+ }
71
+ } finally {
72
+ rmSync(temp, { force: true })
73
+ }
74
+ }
75
+
76
+ async function main(args) {
77
+ const chosen = process.env["TRAJ_BINARY_PATH"]
78
+ if (chosen !== undefined) return run(chosen, args)
79
+ if (version === "" || version === "0.0.0") {
80
+ return fail("this is a checkout, not a release: run `bun run dev --` or set TRAJ_BINARY_PATH.")
81
+ }
82
+ const name = assetName(process.platform, process.arch)
83
+ const home = process.env["TRAJ_HOME"] || join(homedir(), ".traj")
84
+ const exe = process.platform === "win32" ? ".exe" : ""
85
+ const dest = join(home, "bin", `traj-${version}${exe}`)
86
+ if (!existsSync(dest)) {
87
+ const base = process.env["TRAJ_DOWNLOAD_BASE_URL"] || undefined
88
+ process.stderr.write(`traj: downloading ${name} v${version} to ${dest} (about 100 MB).\n`)
89
+ const checksums = new TextDecoder().decode(
90
+ await download(releaseUrl(version, "checksums.txt", base)),
91
+ )
92
+ const bytes = await download(releaseUrl(version, name, base))
93
+ verifyChecksum(checksums, name, bytes)
94
+ install(bytes, dest)
95
+ }
96
+ run(dest, args)
97
+ }
98
+
99
+ await main(process.argv.slice(2)).catch((error) => {
100
+ fail(error instanceof Error ? error.message : String(error))
101
+ })
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
- "name": "@trajectonomics/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@trajectonomics/cli",
3
+ "version": "0.0.1-bootstrap.0",
4
+ "description": "trajectonomics command line",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "packageManager": "bun@1.4.4",
8
+ "bin": {
9
+ "traj": "./bin/traj.js"
10
+ },
11
+ "files": [
12
+ "bin/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/trajectonomics/cli.git"
23
+ },
24
+ "homepage": "https://github.com/trajectonomics/cli#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/trajectonomics/cli/issues"
27
+ },
28
+ "scripts": {
29
+ "dev": "bun run src/index.ts",
30
+ "test": "bun test",
31
+ "typecheck": "tsc --noEmit",
32
+ "lint": "biome ci --error-on-warnings .",
33
+ "format": "biome check --write .",
34
+ "build": "bun build src/index.ts --compile --outfile dist/traj",
35
+ "check": "bun run lint && bun run typecheck && bun test && bun run build",
36
+ "update:skills": "bunx skills@latest update -p -y"
37
+ },
38
+ "devDependencies": {
39
+ "@biomejs/biome": "2.6.3",
40
+ "@trajectonomics/ledger": "0.1.0-alpha.0",
41
+ "@trajectonomics/sim": "0.1.0",
42
+ "@trajectonomics/tooling": "1.1.0",
43
+ "@types/bun": "^1.4.4",
44
+ "typescript": "^7.0.2",
45
+ "zod": "^4.6.7"
46
+ }
47
+ }