oatty 0.1.0-placeholder.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/README.md +340 -0
- package/npm/bin/oatty.js +34 -0
- package/npm/scripts/platform.js +27 -0
- package/npm/scripts/postinstall.js +182 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://github.com/oattyio/oatty/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
# Oatty - Schema-driven CLI + TUI + MCP
|
|
9
|
+
|
|
10
|
+
A vendor-agnostic operations surface for teams that are tired of juggling near-identical CLIs, partial API coverage, and
|
|
11
|
+
separate MCP servers with different constraints.
|
|
12
|
+
|
|
13
|
+
Oatty turns OpenAPI documents into runnable commands, lets you execute them from one consistent CLI/TUI, and extends
|
|
14
|
+
that surface with MCP tools and reusable workflows.
|
|
15
|
+
|
|
16
|
+
## Why Oatty Exists
|
|
17
|
+
|
|
18
|
+
Modern developer tooling has a strange paradox:
|
|
19
|
+
the APIs are powerful and well-documented, but the tools built on top of them are fragmented, incomplete, and
|
|
20
|
+
inconsistent.
|
|
21
|
+
|
|
22
|
+
Most vendor CLIs are thin wrappers around an API with a small set of hand-crafted workflows.
|
|
23
|
+
When you work across vendors, the experience converges into the same problems:
|
|
24
|
+
|
|
25
|
+
- Nearly identical commands with different naming conventions
|
|
26
|
+
- Partial API coverage that forces you back to curl or custom scripts
|
|
27
|
+
- Separate MCP servers that expose even *less* functionality than the CLI
|
|
28
|
+
- Automation that lives in brittle scripts instead of reusable, inspectable workflows
|
|
29
|
+
|
|
30
|
+
Oatty was built to collapse this complexity into **one coherent operational surface**.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Core UX Principles
|
|
35
|
+
|
|
36
|
+
Oatty is not just a CLI replacement — it is a **carefully designed terminal experience** built around four core
|
|
37
|
+
principles.
|
|
38
|
+
|
|
39
|
+
### 1. Discoverability
|
|
40
|
+
|
|
41
|
+
You should never need to memorize commands.
|
|
42
|
+
|
|
43
|
+
- Every command, workflow, and option is searchable and browsable.
|
|
44
|
+
- The TUI acts as a living catalog derived directly from schemas and plugins.
|
|
45
|
+
- If the API supports it, you can *find it* — even if you’ve never used it before.
|
|
46
|
+
|
|
47
|
+
This makes Oatty approachable for new users and powerful for experts.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### 2. Simplicity
|
|
52
|
+
|
|
53
|
+
Each screen does **one thing**, clearly.
|
|
54
|
+
|
|
55
|
+
- Search commands
|
|
56
|
+
- Inspect details
|
|
57
|
+
- Run an action
|
|
58
|
+
- Monitor progress or results
|
|
59
|
+
|
|
60
|
+
There is no clutter, no overloaded views, and no hidden modes.
|
|
61
|
+
Familiar interaction patterns (lists, tables, forms, logs) keep cognitive load low, even when the underlying task is
|
|
62
|
+
complex.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 3. Speed
|
|
67
|
+
|
|
68
|
+
Oatty is designed for real work, not demos.
|
|
69
|
+
|
|
70
|
+
- Fast startup and low-latency interactions
|
|
71
|
+
- Keyboard-first navigation with the mouse interactions you'd expect from a beautiful terminal UI
|
|
72
|
+
- Reliable execution paths for long-running workflows and streaming output
|
|
73
|
+
|
|
74
|
+
Power users can move as fast as they can type — without sacrificing safety or clarity.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 4. Consistency
|
|
79
|
+
|
|
80
|
+
Once you learn Oatty, you’ve learned every vendor's CLI, workflow, and MCP server.
|
|
81
|
+
|
|
82
|
+
- The same command model powers the CLI, the TUI, workflows, and MCP tools
|
|
83
|
+
- Flags, arguments, outputs, and behaviors follow consistent patterns
|
|
84
|
+
- Vendor-specific quirks are normalized behind a shared execution model
|
|
85
|
+
|
|
86
|
+
This consistency dramatically reduces context switching and relearning.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## The Big Idea
|
|
91
|
+
|
|
92
|
+
Treat vendor APIs and MCP servers as **inputs**, not product boundaries.
|
|
93
|
+
|
|
94
|
+
Oatty ingests OpenAPI schemas to build a runtime command catalog, exposes that catalog through a unified CLI and TUI,
|
|
95
|
+
and allows vendors or teams to extend it via MCP — all while enabling first-class, filesystem-backed workflows.
|
|
96
|
+
|
|
97
|
+
Instead of maintaining:
|
|
98
|
+
|
|
99
|
+
- N vendor CLIs
|
|
100
|
+
- N MCP servers
|
|
101
|
+
- N incompatible automation approaches
|
|
102
|
+
|
|
103
|
+
You get **one interface**, one mental model, and one place to operate.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## What This Unlocks
|
|
108
|
+
|
|
109
|
+
- Full API coverage without waiting for vendors to implement it their CLI
|
|
110
|
+
- Workflows that span multiple vendors and MCP servers
|
|
111
|
+
- A single MCP surface instead of fragmented plugin ecosystems
|
|
112
|
+
- Shareable, reviewable workflows instead of opaque scripts
|
|
113
|
+
- A terminal UX that scales from quick commands to complex operations
|
|
114
|
+
|
|
115
|
+
## Quick Example
|
|
116
|
+
|
|
117
|
+
Use one interface for API-derived commands and workflow execution:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Start interactive mode
|
|
121
|
+
cargo run -p oatty
|
|
122
|
+
|
|
123
|
+
# Execute a command from a loaded catalog
|
|
124
|
+
cargo run -p oatty -- apps list
|
|
125
|
+
|
|
126
|
+
# Run a workflow from runtime storage
|
|
127
|
+
cargo run -p oatty -- workflow list
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Usage
|
|
131
|
+
|
|
132
|
+
- Build: `cargo build --workspace`
|
|
133
|
+
- Run TUI (no args): `cargo run -p oatty` (or the installed binary `oatty`)
|
|
134
|
+
- CLI examples:
|
|
135
|
+
- `cargo run -p oatty -- apps list`
|
|
136
|
+
- `cargo run -p oatty -- apps info my-app`
|
|
137
|
+
- `cargo run -p oatty -- workflow list`
|
|
138
|
+
- `cargo run -p oatty -- workflow preview --file workflows/create_app_and_db.yaml`
|
|
139
|
+
|
|
140
|
+
Note: available commands depend on which registry catalogs and MCP plugins are configured/enabled.
|
|
141
|
+
|
|
142
|
+
## NPM Distribution
|
|
143
|
+
|
|
144
|
+
Oatty can be installed via npm and will download the matching prebuilt binary for your platform during `postinstall`.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm i -g oatty
|
|
148
|
+
oatty --help
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
How it works:
|
|
152
|
+
|
|
153
|
+
- npm installs a small Node launcher package (`oatty`).
|
|
154
|
+
- The installer detects your OS/arch and fetches the matching GitHub release asset for the npm package version.
|
|
155
|
+
- The download is verified against the release `SHA256SUMS` before extraction.
|
|
156
|
+
|
|
157
|
+
Currently mapped platforms:
|
|
158
|
+
|
|
159
|
+
- macOS `x64` -> `x86_64-apple-darwin`
|
|
160
|
+
- macOS `arm64` -> `aarch64-apple-darwin`
|
|
161
|
+
- Linux `x64` -> `x86_64-unknown-linux-gnu`
|
|
162
|
+
- Windows `x64` -> `x86_64-pc-windows-msvc`
|
|
163
|
+
|
|
164
|
+
If your platform is not currently mapped, install from source (`cargo build --release`) or use a directly downloaded binary.
|
|
165
|
+
|
|
166
|
+
Maintainers:
|
|
167
|
+
|
|
168
|
+
- GitHub release assets are built/published via `.github/workflows/release.yml`.
|
|
169
|
+
- npm publication is handled by `.github/workflows/npm-publish.yml` when a non-prerelease GitHub Release is published and npm Trusted Publishing is configured.
|
|
170
|
+
|
|
171
|
+
### First-time: import a registry catalog
|
|
172
|
+
|
|
173
|
+
If you have no catalogs configured yet, the fastest way to get started is via the TUI:
|
|
174
|
+
|
|
175
|
+
1. Run `cargo run -p oatty`.
|
|
176
|
+
2. In the Library view, import a local OpenAPI document (e.g., `schemas/samples/render-public-api.json`) or a URL.
|
|
177
|
+
3. Accept the default command prefix (or enter your own).
|
|
178
|
+
4. The registry configuration is saved to `~/.config/oatty/registry.json` and manifests are stored under
|
|
179
|
+
`~/.config/oatty/catalogs/`.
|
|
180
|
+
|
|
181
|
+
## Architecture (high level)
|
|
182
|
+
|
|
183
|
+
- **Registry** (`crates/registry`): loads registry catalogs from `~/.config/oatty/registry.json` and reads per-catalog
|
|
184
|
+
manifest files (typically `~/.config/oatty/catalogs/*.bin`).
|
|
185
|
+
- **MCP** (`crates/mcp`): loads `~/.config/oatty/mcp.json`, manages plugin lifecycles, and can inject MCP tool commands
|
|
186
|
+
into the registry at runtime.
|
|
187
|
+
- **CLI** (`crates/cli`): builds a Clap tree from the registry, routes `workflow` commands, and executes HTTP/MCP
|
|
188
|
+
commands.
|
|
189
|
+
- **TUI** (`crates/tui`): interactive UI built on Ratatui/Crossterm; includes in-app help and log panes.
|
|
190
|
+
- **Engine** (`crates/engine`): workflow parsing and execution utilities used by `oatty workflow ...`.
|
|
191
|
+
|
|
192
|
+
## Environment & Config
|
|
193
|
+
|
|
194
|
+
- `OATTY_LOG`: tracing level for stderr logs in CLI mode (`error`, `warn`, `info` [default], `debug`, `trace`).
|
|
195
|
+
- `TUI_THEME`: theme override for the TUI (`dracula`, `dracula_hc`, `nord`, `nord_hc`, `cyberpunk`, `cyberpunk_hc`,
|
|
196
|
+
`ansi256`, `ansi256_hc`).
|
|
197
|
+
- `MCP_CONFIG_PATH`: overrides MCP config path (default: `~/.config/oatty/mcp.json`).
|
|
198
|
+
- `REGISTRY_CONFIG_PATH`: overrides registry config path (default: `~/.config/oatty/registry.json`).
|
|
199
|
+
- `REGISTRY_CATALOGS_PATH`: overrides the directory where catalog manifest files are stored (default:
|
|
200
|
+
`~/.config/oatty/catalogs`).
|
|
201
|
+
- `REGISTRY_WORKFLOWS_PATH`: overrides workflow manifest storage (default: `~/.config/oatty/workflows`).
|
|
202
|
+
|
|
203
|
+
Authentication headers/tokens are configured per catalog in the Library view (or persisted catalog configuration), not
|
|
204
|
+
via a global API token environment variable.
|
|
205
|
+
|
|
206
|
+
### Logging behavior
|
|
207
|
+
|
|
208
|
+
- TUI mode silences tracing output to stderr while the UI is active to prevent overlaying the terminal UI; logs are
|
|
209
|
+
routed into in-app panes where applicable.
|
|
210
|
+
- CLI mode writes logs to stderr as usual.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
- Toolchain: `rust-toolchain.toml` pins the project to Rust `stable`.
|
|
215
|
+
- Workspace crates: `cli`, `tui`, `registry`, `registry-gen`, `engine`, `api`, `util`, `types`, `mcp`.
|
|
216
|
+
- Common commands:
|
|
217
|
+
- Build: `cargo build --workspace`
|
|
218
|
+
- Test: `cargo test --workspace`
|
|
219
|
+
- Lint: `cargo clippy --workspace -- -D warnings`
|
|
220
|
+
- Format: `cargo fmt --all`
|
|
221
|
+
|
|
222
|
+
### Registry catalogs (manifests)
|
|
223
|
+
|
|
224
|
+
- Generator crate: `crates/registry-gen` (`oatty-registry-gen`) can derive a manifest from OpenAPI documents.
|
|
225
|
+
- At runtime, the registry reads catalog manifests from paths referenced in `~/.config/oatty/registry.json`.
|
|
226
|
+
|
|
227
|
+
## Registry Generator (Library)
|
|
228
|
+
|
|
229
|
+
The registry is derived from OpenAPI documents using the `registry-gen` crate.
|
|
230
|
+
|
|
231
|
+
### Library Usage
|
|
232
|
+
|
|
233
|
+
Add a dependency on the generator crate from within the workspace:
|
|
234
|
+
|
|
235
|
+
```toml
|
|
236
|
+
[dependencies]
|
|
237
|
+
oatty-registry-gen = { path = "crates/registry-gen" }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Generate a manifest file (postcard):
|
|
241
|
+
|
|
242
|
+
```rust
|
|
243
|
+
use std::path::PathBuf;
|
|
244
|
+
use oatty_registry_gen::{io::ManifestInput, write_manifest};
|
|
245
|
+
|
|
246
|
+
fn main() -> anyhow::Result<()> {
|
|
247
|
+
let schema = PathBuf::from("schemas/samples/render-public-api.json");
|
|
248
|
+
let output = PathBuf::from("target/manifest.bin");
|
|
249
|
+
write_manifest(ManifestInput::new(Some(schema), None, None), output)?;
|
|
250
|
+
Ok(())
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Generate a manifest file (JSON):
|
|
255
|
+
|
|
256
|
+
```rust
|
|
257
|
+
use std::path::PathBuf;
|
|
258
|
+
use oatty_registry_gen::{io::ManifestInput, write_manifest_json};
|
|
259
|
+
|
|
260
|
+
fn main() -> anyhow::Result<()> {
|
|
261
|
+
let schema = PathBuf::from("schemas/samples/render-public-api.json");
|
|
262
|
+
let output = PathBuf::from("target/manifest.json");
|
|
263
|
+
write_manifest_json(ManifestInput::new(Some(schema), None, None), output)?;
|
|
264
|
+
Ok(())
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Derive commands in-memory from an OpenAPI document:
|
|
269
|
+
|
|
270
|
+
```rust
|
|
271
|
+
use oatty_registry_gen::openapi::{derive_commands_from_openapi, derive_vendor_from_document};
|
|
272
|
+
|
|
273
|
+
fn load_commands(openapi_json: &str) -> anyhow::Result<Vec<oatty_types::CommandSpec>> {
|
|
274
|
+
let document: serde_json::Value = serde_json::from_str(openapi_json)?;
|
|
275
|
+
let vendor = derive_vendor_from_document(&document);
|
|
276
|
+
let cmds = derive_commands_from_openapi(&document, &vendor)?;
|
|
277
|
+
Ok(cmds)
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Flow
|
|
282
|
+
|
|
283
|
+
```mermaid
|
|
284
|
+
flowchart LR
|
|
285
|
+
subgraph Runtime
|
|
286
|
+
A[oatty no args] -->|Launch| TUI[TUI]
|
|
287
|
+
A2[oatty group cmd] -->|Parse| CLI[CLI Router]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
subgraph Registry
|
|
291
|
+
S[OpenAPI Document] --> D["Derive Manifest (registry-gen)"]
|
|
292
|
+
D --> M["Catalog Manifest (.bin)"]
|
|
293
|
+
M --> RC[Registry Catalogs]
|
|
294
|
+
RC --> C[Clap Tree]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
TUI -->|Search/Select| C
|
|
298
|
+
TUI -->|Compose| RUN[Execute]
|
|
299
|
+
CLI -->|Match| C
|
|
300
|
+
CLI -->|Execute| RUN
|
|
301
|
+
|
|
302
|
+
RUN -->|HTTP| HTTP[Oatty API]
|
|
303
|
+
RUN -->|MCP| MCP[MCP Tool]
|
|
304
|
+
HTTP --> OUT2[Status + Body]
|
|
305
|
+
MCP --> OUT2
|
|
306
|
+
|
|
307
|
+
subgraph Policy
|
|
308
|
+
REDACT[Redact secrets in output]
|
|
309
|
+
end
|
|
310
|
+
OUT2 --> REDACT
|
|
311
|
+
RUN --> LOGS[Logs Panel / stdout]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Security
|
|
315
|
+
|
|
316
|
+
- Secrets are redacted from output by default (headers/payload in output and logs).
|
|
317
|
+
- Network calls go through reqwest with default timeouts.
|
|
318
|
+
- Release artifact verification guide: `VERIFY.md`.
|
|
319
|
+
- Release publish secrets setup: `RELEASE_SECRETS.md`.
|
|
320
|
+
|
|
321
|
+
## Status
|
|
322
|
+
|
|
323
|
+
- Schema-driven registry, CLI router, TUI, MCP plugin engine, and workflow support are implemented. Some HTTP client
|
|
324
|
+
behaviors (retries/backoff) are minimal.
|
|
325
|
+
|
|
326
|
+
## Theme Architecture
|
|
327
|
+
|
|
328
|
+
- Location: `crates/tui/src/ui/theme` (roles, helpers, Dracula/Nord themes)
|
|
329
|
+
- Docs: `specs/THEME.md` (theme mapping, usage, and guidelines)
|
|
330
|
+
- Select theme via env var `TUI_THEME`:
|
|
331
|
+
- `dracula` (default), `dracula_hc`
|
|
332
|
+
- `nord`, `nord_hc`
|
|
333
|
+
- `cyberpunk`, `cyberpunk_hc`
|
|
334
|
+
- `ansi256`, `ansi256_hc`
|
|
335
|
+
- Example: `TUI_THEME=dracula cargo run -p oatty`
|
|
336
|
+
|
|
337
|
+
## Release Trust
|
|
338
|
+
|
|
339
|
+
- Artifact/user verification steps: `VERIFY.md`
|
|
340
|
+
- CI publish setup (npm Trusted Publishing): `RELEASE_SECRETS.md`
|
package/npm/bin/oatty.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
const binaryPath = path.join(__dirname, process.platform === "win32" ? "oatty.exe" : "oatty");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(binaryPath)) {
|
|
11
|
+
process.stderr.write(
|
|
12
|
+
"[oatty] Binary not found. Reinstall with `npm rebuild oatty` or verify release assets exist for your platform.\n"
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
env: process.env,
|
|
20
|
+
cwd: process.cwd()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
child.on("exit", (code, signal) => {
|
|
24
|
+
if (signal) {
|
|
25
|
+
process.kill(process.pid, signal);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
process.exit(code ?? 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on("error", (error) => {
|
|
32
|
+
process.stderr.write(`[oatty] Failed to launch binary at ${binaryPath}: ${error.message}\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const PLATFORM_TARGETS = {
|
|
4
|
+
darwin: {
|
|
5
|
+
x64: { target: "x86_64-apple-darwin", archiveExtension: ".zip", binaryName: "oatty" },
|
|
6
|
+
arm64: { target: "aarch64-apple-darwin", archiveExtension: ".zip", binaryName: "oatty" }
|
|
7
|
+
},
|
|
8
|
+
linux: {
|
|
9
|
+
x64: { target: "x86_64-unknown-linux-gnu", archiveExtension: ".tar.gz", binaryName: "oatty" }
|
|
10
|
+
},
|
|
11
|
+
win32: {
|
|
12
|
+
x64: { target: "x86_64-pc-windows-msvc", archiveExtension: ".zip", binaryName: "oatty.exe" }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function resolvePlatformTarget(platform, architecture) {
|
|
17
|
+
const platformEntry = PLATFORM_TARGETS[platform];
|
|
18
|
+
if (!platformEntry) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return platformEntry[architecture] || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
resolvePlatformTarget
|
|
27
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const fsp = require("node:fs/promises");
|
|
5
|
+
const https = require("node:https");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const crypto = require("node:crypto");
|
|
8
|
+
const os = require("node:os");
|
|
9
|
+
const tar = require("tar");
|
|
10
|
+
const AdmZip = require("adm-zip");
|
|
11
|
+
const { resolvePlatformTarget } = require("./platform");
|
|
12
|
+
|
|
13
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
14
|
+
const BIN_DIRECTORY = path.join(PACKAGE_ROOT, "npm", "bin");
|
|
15
|
+
const OUTPUT_EXECUTABLE_PATH = path.join(BIN_DIRECTORY, process.platform === "win32" ? "oatty.exe" : "oatty");
|
|
16
|
+
const OUTPUT_MARKER_PATH = path.join(BIN_DIRECTORY, ".oatty-installed-version");
|
|
17
|
+
const RELEASE_REPOSITORY = process.env.OATTY_NPM_RELEASE_REPOSITORY || "oattyio/oatty";
|
|
18
|
+
const RELEASE_TAG_PREFIX = process.env.OATTY_NPM_RELEASE_TAG_PREFIX || "v";
|
|
19
|
+
const RELEASE_BASE_URL = process.env.OATTY_NPM_RELEASE_BASE_URL || `https://github.com/${RELEASE_REPOSITORY}/releases/download`;
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
|
|
23
|
+
const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
|
|
24
|
+
const packageVersion = packageJson.version;
|
|
25
|
+
const releaseTag = `${RELEASE_TAG_PREFIX}${packageVersion}`;
|
|
26
|
+
|
|
27
|
+
const platformTarget = resolvePlatformTarget(process.platform, process.arch);
|
|
28
|
+
if (!platformTarget) {
|
|
29
|
+
emitWarning(
|
|
30
|
+
`No prebuilt oatty binary is published for platform=${process.platform} arch=${process.arch}.` +
|
|
31
|
+
" Skipping binary download."
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const assetFileName = `oatty-${releaseTag}-${platformTarget.target}${platformTarget.archiveExtension}`;
|
|
37
|
+
const checksumFileName = "SHA256SUMS";
|
|
38
|
+
const assetUrl = `${RELEASE_BASE_URL}/${releaseTag}/${assetFileName}`;
|
|
39
|
+
const checksumUrl = `${RELEASE_BASE_URL}/${releaseTag}/${checksumFileName}`;
|
|
40
|
+
|
|
41
|
+
await fsp.mkdir(BIN_DIRECTORY, { recursive: true });
|
|
42
|
+
if (await isAlreadyInstalled(packageVersion)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const temporaryDirectory = await fsp.mkdtemp(path.join(os.tmpdir(), "oatty-npm-"));
|
|
47
|
+
const archivePath = path.join(temporaryDirectory, assetFileName);
|
|
48
|
+
const checksumPath = path.join(temporaryDirectory, checksumFileName);
|
|
49
|
+
try {
|
|
50
|
+
await downloadToPath(assetUrl, archivePath);
|
|
51
|
+
await downloadToPath(checksumUrl, checksumPath);
|
|
52
|
+
await verifyArchiveChecksum(archivePath, checksumPath, assetFileName);
|
|
53
|
+
|
|
54
|
+
if (platformTarget.archiveExtension === ".tar.gz") {
|
|
55
|
+
await extractTarArchive(archivePath, BIN_DIRECTORY);
|
|
56
|
+
} else {
|
|
57
|
+
extractZipArchive(archivePath, BIN_DIRECTORY);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await ensureExecutablePermissions(OUTPUT_EXECUTABLE_PATH);
|
|
61
|
+
await fsp.writeFile(OUTPUT_MARKER_PATH, packageVersion, "utf8");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
emitWarning(`Failed to install oatty binary from ${assetUrl}. ${error.message}`);
|
|
64
|
+
emitWarning("You can retry later with: npm rebuild oatty");
|
|
65
|
+
} finally {
|
|
66
|
+
await fsp.rm(temporaryDirectory, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function isAlreadyInstalled(packageVersion) {
|
|
71
|
+
try {
|
|
72
|
+
const version = (await fsp.readFile(OUTPUT_MARKER_PATH, "utf8")).trim();
|
|
73
|
+
const binaryExists = await fileExists(OUTPUT_EXECUTABLE_PATH);
|
|
74
|
+
return version === packageVersion && binaryExists;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function verifyArchiveChecksum(archivePath, checksumPath, assetFileName) {
|
|
81
|
+
const expectedChecksum = await readExpectedChecksum(checksumPath, assetFileName);
|
|
82
|
+
const actualChecksum = await hashFileSha256(archivePath);
|
|
83
|
+
if (actualChecksum !== expectedChecksum) {
|
|
84
|
+
throw new Error(`Checksum mismatch for ${assetFileName}. expected=${expectedChecksum} actual=${actualChecksum}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readExpectedChecksum(checksumPath, assetFileName) {
|
|
89
|
+
const checksumContent = await fsp.readFile(checksumPath, "utf8");
|
|
90
|
+
const lines = checksumContent.split(/\r?\n/);
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (!line.trim()) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
96
|
+
if (!match) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const [, checksum, fileName] = match;
|
|
100
|
+
if (fileName.trim() === assetFileName) {
|
|
101
|
+
return checksum.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Checksum for ${assetFileName} not found in SHA256SUMS`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function hashFileSha256(filePath) {
|
|
108
|
+
const hash = crypto.createHash("sha256");
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
const stream = fs.createReadStream(filePath);
|
|
111
|
+
stream.on("error", reject);
|
|
112
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
113
|
+
stream.on("end", resolve);
|
|
114
|
+
});
|
|
115
|
+
return hash.digest("hex");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function downloadToPath(url, destinationPath) {
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
const request = https.get(url, (response) => {
|
|
121
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
122
|
+
response.resume();
|
|
123
|
+
downloadToPath(response.headers.location, destinationPath).then(resolve).catch(reject);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (response.statusCode !== 200) {
|
|
128
|
+
response.resume();
|
|
129
|
+
reject(new Error(`Request failed for ${url}. status=${response.statusCode}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const file = fs.createWriteStream(destinationPath);
|
|
134
|
+
response.pipe(file);
|
|
135
|
+
file.on("finish", () => file.close(resolve));
|
|
136
|
+
file.on("error", reject);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
request.on("error", reject);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function extractTarArchive(archivePath, destinationPath) {
|
|
144
|
+
await tar.x({
|
|
145
|
+
cwd: destinationPath,
|
|
146
|
+
file: archivePath
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractZipArchive(archivePath, destinationPath) {
|
|
151
|
+
const zipArchive = new AdmZip(archivePath);
|
|
152
|
+
zipArchive.extractAllTo(destinationPath, true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function ensureExecutablePermissions(binaryPath) {
|
|
156
|
+
if (process.platform === "win32") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!(await fileExists(binaryPath))) {
|
|
161
|
+
throw new Error(`Expected binary not found after extraction: ${binaryPath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await fsp.chmod(binaryPath, 0o755);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function fileExists(filePath) {
|
|
168
|
+
try {
|
|
169
|
+
await fsp.access(filePath);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function emitWarning(message) {
|
|
177
|
+
process.stderr.write(`[oatty npm installer] ${message}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((error) => {
|
|
181
|
+
emitWarning(error.message);
|
|
182
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oatty",
|
|
3
|
+
"version": "0.1.0-placeholder.0",
|
|
4
|
+
"description": "Schema-driven CLI + TUI + MCP runtime",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/oattyio/oatty.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/oattyio/oatty",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/oattyio/oatty/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"oatty": "npm/bin/oatty.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"npm/bin",
|
|
19
|
+
"npm/scripts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE*"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "node npm/scripts/postinstall.js"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cli",
|
|
28
|
+
"tui",
|
|
29
|
+
"mcp",
|
|
30
|
+
"openapi",
|
|
31
|
+
"workflow"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"adm-zip": "^0.5.16",
|
|
38
|
+
"tar": "^7.4.3"
|
|
39
|
+
}
|
|
40
|
+
}
|