@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 +21 -0
- package/README.md +70 -2
- package/bin/launcher.d.ts +9 -0
- package/bin/launcher.js +64 -0
- package/bin/traj.js +101 -0
- package/package.json +46 -5
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
|
-
#
|
|
1
|
+
# trajectonomics
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/bin/launcher.js
ADDED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|