@struktoai/mirage-cli 0.0.1-alpha.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 +230 -0
- package/dist/bin/mirage.d.ts +1 -0
- package/dist/bin/mirage.js +449 -0
- package/dist/bin/mirage.js.map +1 -0
- package/dist/chunk-42V4PGGP.js +252 -0
- package/dist/chunk-42V4PGGP.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-og-light@2x.png" alt="Mirage: A Unified Virtual File System for AI Agents" width="900">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
Mirage is **a Unified Virtual File System for AI Agents**: a single tree that mounts services and data sources like S3, Google Drive, Slack, Gmail, and Redis side-by-side as one filesystem.
|
|
6
|
+
|
|
7
|
+
AI agents reach every backend with the same handful of Unix-like tools, and pipelines compose across services as naturally as on a local disk. It's a simulated environment, agents see one filesystem underneath. Any LLM that already knows bash can use Mirage out of the box, with zero new vocabulary.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const ws = new Workspace({
|
|
11
|
+
'/data': new RAMResource(),
|
|
12
|
+
'/s3': new S3Resource({ bucket: 'logs' }),
|
|
13
|
+
'/slack': new SlackResource({}),
|
|
14
|
+
'/github': new GitHubResource({}),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await ws.execute('grep alert /slack/general/*.json | wc -l')
|
|
18
|
+
await ws.execute('cat /github/mirage/README.md')
|
|
19
|
+
await ws.execute('cp /s3/report.csv /data/local.csv')
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## About
|
|
23
|
+
|
|
24
|
+
- **One filesystem, every backend.** Every service speaks the same filesystem semantics, so agents reason about one abstraction instead of N SDKs and M MCPs, leaning on the filesystem and bash vocabulary LLMs are most fluent in.
|
|
25
|
+
- **Multiple resources, one filesystem:** RAM, Disk, Redis, S3 / R2 / OCI / Supabase / GCS, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Telegram / Email, MongoDB, SSH, and more, mounted side-by-side under a single root.
|
|
26
|
+
- **Familiar bash tools across every mount.** Agents reuse the same handful of Unix-like tools instead of learning a new API per service, and pipelines compose across services as naturally as on a local disk, the exact corpus modern LLMs are most heavily trained on.
|
|
27
|
+
- **Portable workspaces:** clone, snapshot, and version your environment. Move agent runs between machines without restarting or reconfiguring the system.
|
|
28
|
+
- **Embed in your apps and services:** Python and TypeScript SDKs let you give your AI agents a virtual filesystem directly inside FastAPI, Express, browser apps, or any async runtime, no separate process required. Clone, snapshot, and version the workspace from inside your code.
|
|
29
|
+
- **Works with major agent application frameworks:** OpenAI Agents SDK, Vercel AI SDK (TypeScript), LangChain, Pydantic AI, CAMEL, and OpenHands.
|
|
30
|
+
- **Lightweight CLI + daemon:** plugs into coding agents like Claude Code and Codex so they reach every mounted resource through familiar bash, getting more useful work done per turn.
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<picture>
|
|
36
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-arch-dark.svg">
|
|
37
|
+
<img src="https://raw.githubusercontent.com/strukto-ai/mirage/main/assets/mirage-arch-light.svg" alt="Mirage architecture: AI Agent and Application → Mirage Bash and VFS → Dispatcher & Cache → Infrastructure and Remote" width="900">
|
|
38
|
+
</picture>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### Prerequisites
|
|
44
|
+
|
|
45
|
+
- **Python** ≥ 3.12 for the `mirage-ai` package and the `mirage` CLI
|
|
46
|
+
- **Node.js** ≥ 20 for the TypeScript SDK
|
|
47
|
+
- **macOS** or **Linux** (FUSE-based mounts require platform support)
|
|
48
|
+
|
|
49
|
+
### Python
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add mirage-ai
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This installs both the `mirage` library and the `mirage` CLI binary.
|
|
56
|
+
|
|
57
|
+
### TypeScript
|
|
58
|
+
|
|
59
|
+
Pick the package that matches your runtime:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @struktoai/mirage-node # Node.js servers and CLIs
|
|
63
|
+
npm install @struktoai/mirage-browser # browser / edge runtimes
|
|
64
|
+
npm install @struktoai/mirage-core # runtime-agnostic primitives
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`@struktoai/mirage-node` and `@struktoai/mirage-browser` both pull in `@struktoai/mirage-core` automatically.
|
|
68
|
+
|
|
69
|
+
### CLI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl -fsSL https://strukto.ai/install.sh | sh
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or via your package manager of choice:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
brew install mirage
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uvx mirage-ai
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @struktoai/mirage-cli
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quickstart (Python)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from mirage import Workspace
|
|
93
|
+
from mirage.resource.gdocs import GDocsConfig, GDocsResource
|
|
94
|
+
from mirage.resource.ram import RAMResource
|
|
95
|
+
from mirage.resource.s3 import S3Config, S3Resource
|
|
96
|
+
from mirage.resource.slack import SlackConfig, SlackResource
|
|
97
|
+
|
|
98
|
+
ws = Workspace({
|
|
99
|
+
"/data": RAMResource(),
|
|
100
|
+
"/s3": S3Resource(S3Config(bucket="my-bucket")),
|
|
101
|
+
"/slack": SlackResource(SlackConfig()),
|
|
102
|
+
"/docs": GDocsResource(GDocsConfig()),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await ws.execute("cp /s3/report.csv /data/report.csv")
|
|
106
|
+
await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
|
|
107
|
+
|
|
108
|
+
ws.snapshot("demo.tar")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quickstart (TypeScript)
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
Workspace,
|
|
116
|
+
RAMResource,
|
|
117
|
+
S3Resource,
|
|
118
|
+
SlackResource,
|
|
119
|
+
GDocsResource,
|
|
120
|
+
} from '@struktoai/mirage-browser'
|
|
121
|
+
|
|
122
|
+
const ws = new Workspace({
|
|
123
|
+
'/data': new RAMResource(),
|
|
124
|
+
'/s3': new S3Resource({ bucket: 'my-bucket' }),
|
|
125
|
+
'/slack': new SlackResource({}),
|
|
126
|
+
'/docs': new GDocsResource({}),
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
await ws.execute('cp /s3/report.csv /data/report.csv')
|
|
130
|
+
await ws.execute('grep alert /s3/data/log.jsonl | wc -l')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Quickstart (CLI)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mirage workspace create ws.yaml --id demo
|
|
137
|
+
mirage execute --workspace_id demo --command "cp /s3/report.csv /data/report.csv"
|
|
138
|
+
mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
|
|
139
|
+
mirage workspace snapshot demo demo.tar
|
|
140
|
+
mirage workspace load demo.tar --id demo-restored
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Agent Frameworks
|
|
144
|
+
|
|
145
|
+
Mirage drops into the major agent application frameworks as a sandbox or tool layer. Your agent runs against the same mount tree it would in bash, so swapping the model or runtime never changes the surface.
|
|
146
|
+
|
|
147
|
+
### OpenAI Agents SDK (Python)
|
|
148
|
+
|
|
149
|
+
The `MirageSandboxClient` plugs a `Workspace` into the OpenAI Agents SDK as a sandbox: bash commands the agent runs execute against your mounts.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from agents import Runner
|
|
153
|
+
from agents.run import RunConfig
|
|
154
|
+
from agents.sandbox import SandboxAgent, SandboxRunConfig
|
|
155
|
+
|
|
156
|
+
from mirage.agents.openai_agents import MirageSandboxClient
|
|
157
|
+
|
|
158
|
+
client = MirageSandboxClient(ws)
|
|
159
|
+
agent = SandboxAgent(
|
|
160
|
+
name="Mirage Sandbox Agent",
|
|
161
|
+
model="gpt-5.4-nano",
|
|
162
|
+
instructions=ws.file_prompt,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
result = await Runner.run(
|
|
166
|
+
agent,
|
|
167
|
+
"Summarize /s3/data/report.parquet into /report.txt.",
|
|
168
|
+
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Vercel AI SDK (TypeScript)
|
|
173
|
+
|
|
174
|
+
`mirageTools(ws)` exposes the workspace as a typed AI SDK tool set, so any model wired into the AI SDK can read and write across mounts, in Node or the browser.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { generateText } from 'ai'
|
|
178
|
+
import { openai } from '@ai-sdk/openai'
|
|
179
|
+
import { mirageTools } from '@struktoai/mirage-agents/vercel'
|
|
180
|
+
import { buildSystemPrompt } from '@struktoai/mirage-agents/openai'
|
|
181
|
+
|
|
182
|
+
const { text } = await generateText({
|
|
183
|
+
model: openai('gpt-5.4-nano'),
|
|
184
|
+
system: buildSystemPrompt({ mountInfo: { '/': 'In-memory filesystem' } }),
|
|
185
|
+
prompt: "Use readFile to read /docs/paper.pdf, then describe what's in it.",
|
|
186
|
+
tools: mirageTools(ws),
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
LangChain, Pydantic AI, CAMEL, OpenHands, and Mastra adapters live alongside these.
|
|
191
|
+
|
|
192
|
+
## Cache
|
|
193
|
+
|
|
194
|
+
Every `Workspace` ships with a **two-layer cache** so repeated work against remote backends (S3, GDrive, Slack, …) hits local state instead of the network:
|
|
195
|
+
|
|
196
|
+
- **Index cache.** Listings and metadata. The first directory walk hits the API; subsequent ones serve from the index until TTL expires.
|
|
197
|
+
- **File cache.** Object bytes. The first read streams from origin; later pipelines read from cache.
|
|
198
|
+
- **Pluggable backends.** Each layer is a store with two built-ins:
|
|
199
|
+
- **RAM** (default): in-process, zero setup, 512 MB file cache and 10-minute index TTL. Best for single-process apps and notebooks.
|
|
200
|
+
- **Redis**: shared across workers, processes, and machines. Best for serverless, multi-replica services, or when you want cache state to survive restarts.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { RedisFileCacheStore, RedisIndexCacheStore, Workspace } from 'mirage/node'
|
|
204
|
+
|
|
205
|
+
const ws = new Workspace(
|
|
206
|
+
{ '/s3': new S3Resource({ bucket: 'my-bucket' }) },
|
|
207
|
+
{
|
|
208
|
+
cache: new RedisFileCacheStore({ url: 'redis://localhost:6379/0', limit: '8GB' }),
|
|
209
|
+
index: new RedisIndexCacheStore({ url: 'redis://localhost:6379/0', ttl: 600 }),
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { S3Resource, Workspace } from 'mirage/node'
|
|
216
|
+
|
|
217
|
+
const ws = new Workspace({ '/s3': new S3Resource({ bucket: 'my-bucket' }) })
|
|
218
|
+
|
|
219
|
+
// 1. Index miss → S3 LIST. Listing stored in index cache.
|
|
220
|
+
await ws.execute('ls /s3/data/')
|
|
221
|
+
|
|
222
|
+
// 2. Index hit → 0 network calls.
|
|
223
|
+
await ws.execute('find /s3/data/ -name "*.jsonl"')
|
|
224
|
+
|
|
225
|
+
// 3. File miss → S3 GET. Bytes stored in file cache.
|
|
226
|
+
await ws.execute('cat /s3/data/log.jsonl | wc -l')
|
|
227
|
+
|
|
228
|
+
// 4. File hit → 0 network calls.
|
|
229
|
+
await ws.execute('grep alert /s3/data/log.jsonl')
|
|
230
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
emit,
|
|
4
|
+
fail,
|
|
5
|
+
formatAge,
|
|
6
|
+
formatTable,
|
|
7
|
+
handleResponse,
|
|
8
|
+
loadDaemonSettings,
|
|
9
|
+
makeClient
|
|
10
|
+
} from "../chunk-42V4PGGP.js";
|
|
11
|
+
|
|
12
|
+
// src/main.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/daemon.ts
|
|
16
|
+
import { existsSync, readFileSync } from "fs";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
function formatStatus(d) {
|
|
20
|
+
if (!d.running) return `Daemon not running. URL: ${d.url}`;
|
|
21
|
+
const parts = [`Running. PID ${d.pid !== null ? String(d.pid) : "?"}`];
|
|
22
|
+
if (d.uptimeS !== void 0) {
|
|
23
|
+
parts.push(`uptime ${formatAge(Date.now() / 1e3 - d.uptimeS)}`);
|
|
24
|
+
}
|
|
25
|
+
if (d.workspaces !== void 0) {
|
|
26
|
+
parts.push(`${String(d.workspaces)} workspace${d.workspaces === 1 ? "" : "s"}`);
|
|
27
|
+
}
|
|
28
|
+
return parts.join(", ") + `. URL: ${d.url}`;
|
|
29
|
+
}
|
|
30
|
+
function formatStop(d) {
|
|
31
|
+
const via = d.via ?? "?";
|
|
32
|
+
return `Stopped (via ${via}${d.pid !== void 0 ? `, PID ${String(d.pid)}` : ""}).`;
|
|
33
|
+
}
|
|
34
|
+
function formatRestart(d) {
|
|
35
|
+
if (d.spawnedFresh === true) return "Restarted (eager spawn).";
|
|
36
|
+
return "Restarted; next CLI command will auto-spawn.";
|
|
37
|
+
}
|
|
38
|
+
function formatKill(d) {
|
|
39
|
+
if (d.killed) return `Killed PID ${String(d.pid ?? "?")}.`;
|
|
40
|
+
if (d.pid === null || d.pid === void 0) return "Daemon not running.";
|
|
41
|
+
return `Already gone (PID ${String(d.pid)}).`;
|
|
42
|
+
}
|
|
43
|
+
function pidFilePath() {
|
|
44
|
+
return join(homedir(), ".mirage", "daemon.pid");
|
|
45
|
+
}
|
|
46
|
+
function readPid() {
|
|
47
|
+
const p = pidFilePath();
|
|
48
|
+
if (!existsSync(p)) return null;
|
|
49
|
+
const text = readFileSync(p, "utf-8").trim();
|
|
50
|
+
const pid = Number(text);
|
|
51
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
52
|
+
}
|
|
53
|
+
function processAlive(pid) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const code = err.code;
|
|
59
|
+
if (code === "ESRCH") return false;
|
|
60
|
+
if (code === "EPERM") return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildClient() {
|
|
65
|
+
return makeClient(loadDaemonSettings());
|
|
66
|
+
}
|
|
67
|
+
async function waitForUnreachable(timeoutMs) {
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
const c = buildClient();
|
|
71
|
+
if (!await c.isReachable(300)) return true;
|
|
72
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function registerDaemonCommands(program) {
|
|
77
|
+
const d = program.command("daemon").description("Manage the daemon process lifecycle.");
|
|
78
|
+
d.command("status").description("Show daemon health, PID, uptime, workspace count.").action(async () => {
|
|
79
|
+
const pid = readPid();
|
|
80
|
+
const c = buildClient();
|
|
81
|
+
const url = c.settings.url;
|
|
82
|
+
try {
|
|
83
|
+
const r = await c.request("GET", "/v1/health");
|
|
84
|
+
if (r.status !== 200) {
|
|
85
|
+
emit({ running: false, pid, url }, formatStatus);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const body = await r.json();
|
|
89
|
+
emit({ running: true, pid, url, ...body }, formatStatus);
|
|
90
|
+
} catch {
|
|
91
|
+
emit({ running: false, pid, url }, formatStatus);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
d.command("stop").description("Gracefully stop the daemon.").option("--timeout <s>", "Seconds to wait for graceful exit", "5").action(async (opts) => {
|
|
96
|
+
const timeoutMs = Number(opts.timeout) * 1e3;
|
|
97
|
+
const c = buildClient();
|
|
98
|
+
let r;
|
|
99
|
+
try {
|
|
100
|
+
r = await c.request("POST", "/v1/shutdown");
|
|
101
|
+
} catch (err) {
|
|
102
|
+
fail(`daemon not reachable: ${String(err)}`);
|
|
103
|
+
}
|
|
104
|
+
if (r.status >= 400) fail(`shutdown failed: ${await r.text()}`, 2);
|
|
105
|
+
if (await waitForUnreachable(timeoutMs)) {
|
|
106
|
+
emit({ stopped: true, via: "graceful" }, formatStop);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const pid = readPid();
|
|
110
|
+
if (pid !== null && processAlive(pid)) {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, "SIGTERM");
|
|
113
|
+
emit({ stopped: true, via: "sigterm", pid }, formatStop);
|
|
114
|
+
return;
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
fail(`daemon did not exit within ${opts.timeout}s and no live PID found`, 2);
|
|
119
|
+
});
|
|
120
|
+
d.command("restart").description("Stop the daemon. Next CLI command auto-spawns a fresh one.").option("--timeout <s>", "Seconds to wait for graceful exit", "5").option("--eager", "Spawn a fresh daemon immediately").action(async (opts) => {
|
|
121
|
+
const timeoutMs = Number(opts.timeout) * 1e3;
|
|
122
|
+
try {
|
|
123
|
+
await buildClient().request("POST", "/v1/shutdown");
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
if (!await waitForUnreachable(timeoutMs)) {
|
|
127
|
+
const pid = readPid();
|
|
128
|
+
if (pid !== null && processAlive(pid)) {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(pid, "SIGTERM");
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (opts.eager === true) {
|
|
136
|
+
await buildClient().ensureRunning({ allowSpawn: true });
|
|
137
|
+
emit({ restarted: true, spawnedFresh: true }, formatRestart);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
emit(
|
|
141
|
+
{
|
|
142
|
+
restarted: true,
|
|
143
|
+
spawnedFresh: false,
|
|
144
|
+
note: "next workspace create will auto-spawn"
|
|
145
|
+
},
|
|
146
|
+
formatRestart
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
d.command("kill").description("SIGKILL the daemon. Last resort -- skips graceful shutdown.").action(() => {
|
|
150
|
+
const pid = readPid();
|
|
151
|
+
if (pid === null) {
|
|
152
|
+
emit({ killed: false, reason: "no daemon running", pid: null }, formatKill);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!processAlive(pid)) {
|
|
156
|
+
emit({ killed: false, reason: "process already gone", pid }, formatKill);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, "SIGKILL");
|
|
161
|
+
} catch {
|
|
162
|
+
emit({ killed: false, reason: "process gone", pid }, formatKill);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
emit({ killed: true, pid }, formatKill);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/execute.ts
|
|
170
|
+
function registerExecuteCommand(program) {
|
|
171
|
+
program.command("execute").description("Execute a command.").requiredOption("-w, --workspace <id>", "Workspace id").requiredOption("-c, --command <command>", "Shell command to execute").option("-s, --session <id>", "Session id").option("--bg", "Background; return job_id immediately").action(
|
|
172
|
+
async (opts) => {
|
|
173
|
+
const body = { command: opts.command, provision: false };
|
|
174
|
+
if (opts.session !== void 0) body.sessionId = opts.session;
|
|
175
|
+
const path = `/v1/workspaces/${opts.workspace}/execute` + (opts.bg === true ? "?background=true" : "");
|
|
176
|
+
const c = makeClient(loadDaemonSettings());
|
|
177
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
178
|
+
emit(await handleResponse(await c.request("POST", path, { body: JSON.stringify(body) })));
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/job.ts
|
|
184
|
+
function buildClient2() {
|
|
185
|
+
return makeClient(loadDaemonSettings());
|
|
186
|
+
}
|
|
187
|
+
function registerJobCommands(program) {
|
|
188
|
+
const job = program.command("job").description("Manage daemon jobs.");
|
|
189
|
+
job.command("list").option("-w, --workspace <id>").action(async (opts) => {
|
|
190
|
+
const c = buildClient2();
|
|
191
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
192
|
+
const path = "/v1/jobs" + (opts.workspace !== void 0 ? `?workspaceId=${opts.workspace}` : "");
|
|
193
|
+
emit(await handleResponse(await c.request("GET", path)));
|
|
194
|
+
});
|
|
195
|
+
job.command("get").argument("<id>").action(async (id) => {
|
|
196
|
+
const c = buildClient2();
|
|
197
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
198
|
+
emit(await handleResponse(await c.request("GET", `/v1/jobs/${id}`)));
|
|
199
|
+
});
|
|
200
|
+
job.command("wait").argument("<id>").option("--timeout <s>").action(async (id, opts) => {
|
|
201
|
+
const c = buildClient2();
|
|
202
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
203
|
+
const body = {};
|
|
204
|
+
if (opts.timeout !== void 0) body.timeoutS = Number(opts.timeout);
|
|
205
|
+
emit(
|
|
206
|
+
await handleResponse(
|
|
207
|
+
await c.request("POST", `/v1/jobs/${id}/wait`, { body: JSON.stringify(body) })
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
job.command("cancel").argument("<id>").action(async (id) => {
|
|
212
|
+
const c = buildClient2();
|
|
213
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
214
|
+
emit(await handleResponse(await c.request("DELETE", `/v1/jobs/${id}`)));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/provision.ts
|
|
219
|
+
function registerProvisionCommand(program) {
|
|
220
|
+
program.command("provision").description("Estimate cost / preview a command without running it.").requiredOption("-w, --workspace <id>", "Workspace id").requiredOption("-c, --command <command>", "Shell command to estimate").option("-s, --session <id>", "Session id").action(async (opts) => {
|
|
221
|
+
const body = { command: opts.command, provision: true };
|
|
222
|
+
if (opts.session !== void 0) body.sessionId = opts.session;
|
|
223
|
+
const c = makeClient(loadDaemonSettings());
|
|
224
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
225
|
+
emit(
|
|
226
|
+
await handleResponse(
|
|
227
|
+
await c.request("POST", `/v1/workspaces/${opts.workspace}/execute`, {
|
|
228
|
+
body: JSON.stringify(body)
|
|
229
|
+
})
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/session.ts
|
|
236
|
+
function buildClient3() {
|
|
237
|
+
return makeClient(loadDaemonSettings());
|
|
238
|
+
}
|
|
239
|
+
function registerSessionCommands(program) {
|
|
240
|
+
const sess = program.command("session").description("Manage workspace sessions.");
|
|
241
|
+
sess.command("create").argument("<wsId>").option("--id <sessionId>").action(async (wsId, opts) => {
|
|
242
|
+
const c = buildClient3();
|
|
243
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
244
|
+
const body = {};
|
|
245
|
+
if (opts.id !== void 0) body.sessionId = opts.id;
|
|
246
|
+
emit(
|
|
247
|
+
await handleResponse(
|
|
248
|
+
await c.request("POST", `/v1/workspaces/${wsId}/sessions`, {
|
|
249
|
+
body: JSON.stringify(body)
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
sess.command("list").argument("<wsId>").action(async (wsId) => {
|
|
255
|
+
const c = buildClient3();
|
|
256
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
257
|
+
emit(await handleResponse(await c.request("GET", `/v1/workspaces/${wsId}/sessions`)));
|
|
258
|
+
});
|
|
259
|
+
sess.command("delete").argument("<wsId>").argument("<sessionId>").action(async (wsId, sessionId) => {
|
|
260
|
+
const c = buildClient3();
|
|
261
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
262
|
+
emit(
|
|
263
|
+
await handleResponse(
|
|
264
|
+
await c.request("DELETE", `/v1/workspaces/${wsId}/sessions/${sessionId}`)
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/workspace.ts
|
|
271
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
272
|
+
import { basename } from "path";
|
|
273
|
+
import { interpolateEnv, loadWorkspaceConfig } from "@struktoai/mirage-server";
|
|
274
|
+
import { parse as yamlParse } from "yaml";
|
|
275
|
+
function buildClient4() {
|
|
276
|
+
return makeClient(loadDaemonSettings());
|
|
277
|
+
}
|
|
278
|
+
function envRecord() {
|
|
279
|
+
const out = {};
|
|
280
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
281
|
+
if (typeof v === "string") out[k] = v;
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function formatWorkspaceList(items) {
|
|
286
|
+
if (items.length === 0) return "No active workspaces.";
|
|
287
|
+
const rows = items.map((w) => [
|
|
288
|
+
w.id,
|
|
289
|
+
w.mode,
|
|
290
|
+
String(w.mountCount),
|
|
291
|
+
String(w.sessionCount),
|
|
292
|
+
formatAge(w.createdAt)
|
|
293
|
+
]);
|
|
294
|
+
return formatTable(["ID", "MODE", "MOUNTS", "SESSIONS", "AGE"], rows);
|
|
295
|
+
}
|
|
296
|
+
function formatWorkspaceDetail(d) {
|
|
297
|
+
const lines = [
|
|
298
|
+
`ID: ${d.id}`,
|
|
299
|
+
`Mode: ${d.mode}`,
|
|
300
|
+
`Created: ${formatAge(d.createdAt)} ago`
|
|
301
|
+
];
|
|
302
|
+
if (d.mounts !== void 0 && d.mounts.length > 0) {
|
|
303
|
+
const rows = d.mounts.map((m) => [m.prefix, m.resource, m.mode]);
|
|
304
|
+
lines.push("", "Mounts:");
|
|
305
|
+
for (const ln of formatTable(["PREFIX", "RESOURCE", "MODE"], rows).split("\n")) {
|
|
306
|
+
lines.push(" " + ln);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (d.sessions !== void 0 && d.sessions.length > 0) {
|
|
310
|
+
const rows = d.sessions.map((s) => [s.sessionId, s.cwd]);
|
|
311
|
+
lines.push("", "Sessions:");
|
|
312
|
+
for (const ln of formatTable(["SESSION", "CWD"], rows).split("\n")) {
|
|
313
|
+
lines.push(" " + ln);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (d.internals != null) {
|
|
317
|
+
lines.push("", "Internals:");
|
|
318
|
+
for (const k of ["cacheBytes", "cacheEntries", "historyLength", "inFlightJobs"]) {
|
|
319
|
+
lines.push(` ${k.padEnd(16)} ${String(d.internals[k])}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
function registerWorkspaceCommands(program) {
|
|
325
|
+
const ws = program.command("workspace").description("Manage workspaces.");
|
|
326
|
+
ws.command("create").description("Create a workspace; daemon auto-spawns if not running.").argument("<config>", "YAML/JSON workspace config").option("--id <id>", "Explicit workspace id").action(async (configPath, opts) => {
|
|
327
|
+
const cfg = loadWorkspaceConfig(configPath);
|
|
328
|
+
const body = { config: cfg };
|
|
329
|
+
if (opts.id !== void 0) body.id = opts.id;
|
|
330
|
+
const c = buildClient4();
|
|
331
|
+
await c.ensureRunning({ allowSpawn: true });
|
|
332
|
+
const r = await c.request("POST", "/v1/workspaces", { body: JSON.stringify(body) });
|
|
333
|
+
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
334
|
+
});
|
|
335
|
+
ws.command("list").description("List active workspaces.").action(async () => {
|
|
336
|
+
const c = buildClient4();
|
|
337
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
338
|
+
emit(
|
|
339
|
+
await handleResponse(await c.request("GET", "/v1/workspaces")),
|
|
340
|
+
formatWorkspaceList
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
ws.command("get").description("Show full details for one workspace.").argument("<id>").option("--verbose", "Include cache/dirty/history internals").action(async (id, opts) => {
|
|
344
|
+
const c = buildClient4();
|
|
345
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
346
|
+
const path = `/v1/workspaces/${id}` + (opts.verbose === true ? "?verbose=true" : "");
|
|
347
|
+
emit(
|
|
348
|
+
await handleResponse(await c.request("GET", path)),
|
|
349
|
+
formatWorkspaceDetail
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
ws.command("delete").description("Stop and remove a workspace.").argument("<id>").action(async (id) => {
|
|
353
|
+
const c = buildClient4();
|
|
354
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
355
|
+
emit(
|
|
356
|
+
await handleResponse(await c.request("DELETE", `/v1/workspaces/${id}`)),
|
|
357
|
+
(d) => `Deleted workspace ${d.id}.`
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
ws.command("clone").description("Clone a workspace; defaults to fresh local backings + shared remotes.").argument("<srcId>").option("--id <id>").option("--override <path>", "Partial config JSON/YAML for per-mount overrides").action(async (srcId, opts) => {
|
|
361
|
+
const body = {};
|
|
362
|
+
if (opts.id !== void 0) body.id = opts.id;
|
|
363
|
+
if (opts.override !== void 0) {
|
|
364
|
+
const text = readFileSync2(opts.override, "utf-8");
|
|
365
|
+
let parsed;
|
|
366
|
+
try {
|
|
367
|
+
parsed = yamlParse(text);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
fail(`invalid override YAML/JSON at ${opts.override}: ${String(err)}`, 2);
|
|
370
|
+
}
|
|
371
|
+
body.override = interpolateEnv(parsed, envRecord());
|
|
372
|
+
}
|
|
373
|
+
const c = buildClient4();
|
|
374
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
375
|
+
const r = await c.request("POST", `/v1/workspaces/${srcId}/clone`, {
|
|
376
|
+
body: JSON.stringify(body)
|
|
377
|
+
});
|
|
378
|
+
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
379
|
+
});
|
|
380
|
+
ws.command("snapshot").description("Snapshot a workspace to a tar file.").argument("<id>").argument("<output>", "Path to write the .tar to").action(async (id, output) => {
|
|
381
|
+
const c = buildClient4();
|
|
382
|
+
await c.ensureRunning({ allowSpawn: false });
|
|
383
|
+
const r = await c.request("GET", `/v1/workspaces/${id}/snapshot`);
|
|
384
|
+
if (r.status >= 400) {
|
|
385
|
+
fail(`daemon error ${String(r.status)}: ${await r.text()}`, 2);
|
|
386
|
+
}
|
|
387
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
388
|
+
writeFileSync(output, buf);
|
|
389
|
+
emit(
|
|
390
|
+
{ workspaceId: id, path: output, bytes: buf.length },
|
|
391
|
+
(d) => `Snapshot ${d.workspaceId} -> ${d.path} (${d.bytes.toLocaleString()} bytes).`
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
ws.command("load").description("Load a workspace from a tar file.").argument("<tar>", "Path to a .tar produced by `mirage workspace snapshot`").option("--id <id>", "Explicit workspace id").option("--override <path>", "Partial config YAML/JSON for per-mount overrides").action(async (tarPath, opts) => {
|
|
395
|
+
if (!existsSync2(tarPath)) fail(`tar file not found: ${tarPath}`, 2);
|
|
396
|
+
const tarBuf = readFileSync2(tarPath);
|
|
397
|
+
const form = new FormData();
|
|
398
|
+
form.append("tar", new Blob([tarBuf], { type: "application/x-tar" }), basename(tarPath));
|
|
399
|
+
if (opts.id !== void 0) form.append("id", opts.id);
|
|
400
|
+
if (opts.override !== void 0) {
|
|
401
|
+
const overrideText = readFileSync2(opts.override, "utf-8");
|
|
402
|
+
let parsed;
|
|
403
|
+
try {
|
|
404
|
+
parsed = yamlParse(overrideText);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
fail(`invalid override YAML/JSON at ${opts.override}: ${String(err)}`, 2);
|
|
407
|
+
}
|
|
408
|
+
form.append("override", JSON.stringify(interpolateEnv(parsed, envRecord())));
|
|
409
|
+
}
|
|
410
|
+
const c = buildClient4();
|
|
411
|
+
await c.ensureRunning({ allowSpawn: true });
|
|
412
|
+
const r = await c.requestMultipart("POST", "/v1/workspaces/load", form);
|
|
413
|
+
if (r.status >= 400) fail(`load failed: ${await r.text()}`, 2);
|
|
414
|
+
emit(await handleResponse(r), formatWorkspaceDetail);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/main.ts
|
|
419
|
+
function buildProgram() {
|
|
420
|
+
const program = new Command();
|
|
421
|
+
program.name("mirage").description("Mirage daemon CLI: manage workspaces and execute commands.").version("0.0.0");
|
|
422
|
+
registerWorkspaceCommands(program);
|
|
423
|
+
registerSessionCommands(program);
|
|
424
|
+
registerJobCommands(program);
|
|
425
|
+
registerExecuteCommand(program);
|
|
426
|
+
registerProvisionCommand(program);
|
|
427
|
+
registerDaemonCommands(program);
|
|
428
|
+
return program;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/bin/mirage.ts
|
|
432
|
+
var ALIASES = {
|
|
433
|
+
"--workspace_id": "--workspace",
|
|
434
|
+
"--session_id": "--session"
|
|
435
|
+
};
|
|
436
|
+
function rewriteArgv(argv) {
|
|
437
|
+
return argv.map((arg) => {
|
|
438
|
+
for (const [from, to] of Object.entries(ALIASES)) {
|
|
439
|
+
if (arg === from) return to;
|
|
440
|
+
if (arg.startsWith(from + "=")) return to + arg.slice(from.length);
|
|
441
|
+
}
|
|
442
|
+
return arg;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
buildProgram().parseAsync(rewriteArgv(process.argv)).catch((err) => {
|
|
446
|
+
console.error(err);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
});
|
|
449
|
+
//# sourceMappingURL=mirage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/main.ts","../../src/daemon.ts","../../src/execute.ts","../../src/job.ts","../../src/provision.ts","../../src/session.ts","../../src/workspace.ts","../../src/bin/mirage.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { Command } from 'commander'\nimport { registerDaemonCommands } from './daemon.ts'\nimport { registerExecuteCommand } from './execute.ts'\nimport { registerJobCommands } from './job.ts'\nimport { registerProvisionCommand } from './provision.ts'\nimport { registerSessionCommands } from './session.ts'\nimport { registerWorkspaceCommands } from './workspace.ts'\n\nexport function buildProgram(): Command {\n const program = new Command()\n program\n .name('mirage')\n .description('Mirage daemon CLI: manage workspaces and execute commands.')\n .version('0.0.0')\n registerWorkspaceCommands(program)\n registerSessionCommands(program)\n registerJobCommands(program)\n registerExecuteCommand(program)\n registerProvisionCommand(program)\n registerDaemonCommands(program)\n return program\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Command } from 'commander'\nimport { makeClient } from './client.ts'\nimport { emit, fail, formatAge } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\ninterface DaemonStatus {\n running: boolean\n pid: number | null\n url: string\n uptimeS?: number\n workspaces?: number\n}\n\nfunction formatStatus(d: DaemonStatus): string {\n if (!d.running) return `Daemon not running. URL: ${d.url}`\n const parts: string[] = [`Running. PID ${d.pid !== null ? String(d.pid) : '?'}`]\n if (d.uptimeS !== undefined) {\n parts.push(`uptime ${formatAge(Date.now() / 1000 - d.uptimeS)}`)\n }\n if (d.workspaces !== undefined) {\n parts.push(`${String(d.workspaces)} workspace${d.workspaces === 1 ? '' : 's'}`)\n }\n return parts.join(', ') + `. URL: ${d.url}`\n}\n\nfunction formatStop(d: { via?: string; pid?: number }): string {\n const via = d.via ?? '?'\n return `Stopped (via ${via}${d.pid !== undefined ? `, PID ${String(d.pid)}` : ''}).`\n}\n\nfunction formatRestart(d: { spawnedFresh?: boolean }): string {\n if (d.spawnedFresh === true) return 'Restarted (eager spawn).'\n return 'Restarted; next CLI command will auto-spawn.'\n}\n\nfunction formatKill(d: { killed: boolean; pid?: number | null }): string {\n if (d.killed) return `Killed PID ${String(d.pid ?? '?')}.`\n if (d.pid === null || d.pid === undefined) return 'Daemon not running.'\n return `Already gone (PID ${String(d.pid)}).`\n}\n\nfunction pidFilePath(): string {\n return join(homedir(), '.mirage', 'daemon.pid')\n}\n\nfunction readPid(): number | null {\n const p = pidFilePath()\n if (!existsSync(p)) return null\n const text = readFileSync(p, 'utf-8').trim()\n const pid = Number(text)\n return Number.isFinite(pid) && pid > 0 ? pid : null\n}\n\nfunction processAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === 'ESRCH') return false\n if (code === 'EPERM') return true\n return false\n }\n}\n\nfunction buildClient() {\n return makeClient(loadDaemonSettings())\n}\n\nasync function waitForUnreachable(timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n const c = buildClient()\n if (!(await c.isReachable(300))) return true\n await new Promise((r) => setTimeout(r, 200))\n }\n return false\n}\n\nexport function registerDaemonCommands(program: Command): void {\n const d = program.command('daemon').description('Manage the daemon process lifecycle.')\n\n d.command('status')\n .description('Show daemon health, PID, uptime, workspace count.')\n .action(async () => {\n const pid = readPid()\n const c = buildClient()\n const url = c.settings.url\n try {\n const r = await c.request('GET', '/v1/health')\n if (r.status !== 200) {\n emit({ running: false, pid, url } as DaemonStatus, formatStatus)\n process.exit(1)\n }\n const body = (await r.json()) as Record<string, unknown>\n emit({ running: true, pid, url, ...body } as DaemonStatus, formatStatus)\n } catch {\n emit({ running: false, pid, url } as DaemonStatus, formatStatus)\n process.exit(1)\n }\n })\n\n d.command('stop')\n .description('Gracefully stop the daemon.')\n .option('--timeout <s>', 'Seconds to wait for graceful exit', '5')\n .action(async (opts: { timeout: string }) => {\n const timeoutMs = Number(opts.timeout) * 1000\n const c = buildClient()\n let r: Response\n try {\n r = await c.request('POST', '/v1/shutdown')\n } catch (err: unknown) {\n fail(`daemon not reachable: ${String(err)}`)\n }\n if (r.status >= 400) fail(`shutdown failed: ${await r.text()}`, 2)\n if (await waitForUnreachable(timeoutMs)) {\n emit({ stopped: true, via: 'graceful' }, formatStop)\n return\n }\n const pid = readPid()\n if (pid !== null && processAlive(pid)) {\n try {\n process.kill(pid, 'SIGTERM')\n emit({ stopped: true, via: 'sigterm', pid }, formatStop)\n return\n } catch {\n // fall through to fail\n }\n }\n fail(`daemon did not exit within ${opts.timeout}s and no live PID found`, 2)\n })\n\n d.command('restart')\n .description('Stop the daemon. Next CLI command auto-spawns a fresh one.')\n .option('--timeout <s>', 'Seconds to wait for graceful exit', '5')\n .option('--eager', 'Spawn a fresh daemon immediately')\n .action(async (opts: { timeout: string; eager?: boolean }) => {\n const timeoutMs = Number(opts.timeout) * 1000\n try {\n await buildClient().request('POST', '/v1/shutdown')\n } catch {\n // already unreachable; fall through to pid cleanup\n }\n if (!(await waitForUnreachable(timeoutMs))) {\n const pid = readPid()\n if (pid !== null && processAlive(pid)) {\n try {\n process.kill(pid, 'SIGTERM')\n } catch {\n // process may have exited between checks\n }\n }\n }\n if (opts.eager === true) {\n await buildClient().ensureRunning({ allowSpawn: true })\n emit({ restarted: true, spawnedFresh: true }, formatRestart)\n return\n }\n emit(\n {\n restarted: true,\n spawnedFresh: false,\n note: 'next workspace create will auto-spawn',\n },\n formatRestart,\n )\n })\n\n d.command('kill')\n .description('SIGKILL the daemon. Last resort -- skips graceful shutdown.')\n .action(() => {\n const pid = readPid()\n if (pid === null) {\n emit({ killed: false, reason: 'no daemon running', pid: null }, formatKill)\n return\n }\n if (!processAlive(pid)) {\n emit({ killed: false, reason: 'process already gone', pid }, formatKill)\n return\n }\n try {\n process.kill(pid, 'SIGKILL')\n } catch {\n emit({ killed: false, reason: 'process gone', pid }, formatKill)\n return\n }\n emit({ killed: true, pid }, formatKill)\n })\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport type { Command } from 'commander'\nimport { makeClient } from './client.ts'\nimport { emit, handleResponse } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\nexport function registerExecuteCommand(program: Command): void {\n program\n .command('execute')\n .description('Execute a command.')\n .requiredOption('-w, --workspace <id>', 'Workspace id')\n .requiredOption('-c, --command <command>', 'Shell command to execute')\n .option('-s, --session <id>', 'Session id')\n .option('--bg', 'Background; return job_id immediately')\n .action(\n async (opts: { workspace: string; command: string; session?: string; bg?: boolean }) => {\n const body: Record<string, unknown> = { command: opts.command, provision: false }\n if (opts.session !== undefined) body.sessionId = opts.session\n const path =\n `/v1/workspaces/${opts.workspace}/execute` + (opts.bg === true ? '?background=true' : '')\n const c = makeClient(loadDaemonSettings())\n await c.ensureRunning({ allowSpawn: false })\n emit(await handleResponse(await c.request('POST', path, { body: JSON.stringify(body) })))\n },\n )\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport type { Command } from 'commander'\nimport { makeClient } from './client.ts'\nimport { emit, handleResponse } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\nfunction buildClient() {\n return makeClient(loadDaemonSettings())\n}\n\nexport function registerJobCommands(program: Command): void {\n const job = program.command('job').description('Manage daemon jobs.')\n\n job\n .command('list')\n .option('-w, --workspace <id>')\n .action(async (opts: { workspace?: string }) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const path =\n '/v1/jobs' + (opts.workspace !== undefined ? `?workspaceId=${opts.workspace}` : '')\n emit(await handleResponse(await c.request('GET', path)))\n })\n\n job\n .command('get')\n .argument('<id>')\n .action(async (id: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(await handleResponse(await c.request('GET', `/v1/jobs/${id}`)))\n })\n\n job\n .command('wait')\n .argument('<id>')\n .option('--timeout <s>')\n .action(async (id: string, opts: { timeout?: string }) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const body: Record<string, unknown> = {}\n if (opts.timeout !== undefined) body.timeoutS = Number(opts.timeout)\n emit(\n await handleResponse(\n await c.request('POST', `/v1/jobs/${id}/wait`, { body: JSON.stringify(body) }),\n ),\n )\n })\n\n job\n .command('cancel')\n .argument('<id>')\n .action(async (id: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(await handleResponse(await c.request('DELETE', `/v1/jobs/${id}`)))\n })\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport type { Command } from 'commander'\nimport { makeClient } from './client.ts'\nimport { emit, handleResponse } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\nexport function registerProvisionCommand(program: Command): void {\n program\n .command('provision')\n .description('Estimate cost / preview a command without running it.')\n .requiredOption('-w, --workspace <id>', 'Workspace id')\n .requiredOption('-c, --command <command>', 'Shell command to estimate')\n .option('-s, --session <id>', 'Session id')\n .action(async (opts: { workspace: string; command: string; session?: string }) => {\n const body: Record<string, unknown> = { command: opts.command, provision: true }\n if (opts.session !== undefined) body.sessionId = opts.session\n const c = makeClient(loadDaemonSettings())\n await c.ensureRunning({ allowSpawn: false })\n emit(\n await handleResponse(\n await c.request('POST', `/v1/workspaces/${opts.workspace}/execute`, {\n body: JSON.stringify(body),\n }),\n ),\n )\n })\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport type { Command } from 'commander'\nimport { makeClient } from './client.ts'\nimport { emit, handleResponse } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\nfunction buildClient() {\n return makeClient(loadDaemonSettings())\n}\n\nexport function registerSessionCommands(program: Command): void {\n const sess = program.command('session').description('Manage workspace sessions.')\n\n sess\n .command('create')\n .argument('<wsId>')\n .option('--id <sessionId>')\n .action(async (wsId: string, opts: { id?: string }) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const body: Record<string, unknown> = {}\n if (opts.id !== undefined) body.sessionId = opts.id\n emit(\n await handleResponse(\n await c.request('POST', `/v1/workspaces/${wsId}/sessions`, {\n body: JSON.stringify(body),\n }),\n ),\n )\n })\n\n sess\n .command('list')\n .argument('<wsId>')\n .action(async (wsId: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(await handleResponse(await c.request('GET', `/v1/workspaces/${wsId}/sessions`)))\n })\n\n sess\n .command('delete')\n .argument('<wsId>')\n .argument('<sessionId>')\n .action(async (wsId: string, sessionId: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(\n await handleResponse(\n await c.request('DELETE', `/v1/workspaces/${wsId}/sessions/${sessionId}`),\n ),\n )\n })\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { basename } from 'node:path'\nimport type { Command } from 'commander'\nimport { interpolateEnv, loadWorkspaceConfig } from '@struktoai/mirage-server'\nimport { parse as yamlParse } from 'yaml'\nimport { makeClient } from './client.ts'\nimport { emit, fail, formatAge, formatTable, handleResponse } from './output.ts'\nimport { loadDaemonSettings } from './settings.ts'\n\nfunction buildClient() {\n return makeClient(loadDaemonSettings())\n}\n\nfunction envRecord(): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(process.env)) {\n if (typeof v === 'string') out[k] = v\n }\n return out\n}\n\ninterface WorkspaceBrief {\n id: string\n mode: string\n mountCount: number\n sessionCount: number\n createdAt: number\n}\n\ninterface MountSummary {\n prefix: string\n resource: string\n mode: string\n}\n\ninterface SessionSummary {\n sessionId: string\n cwd: string\n}\n\ninterface Internals {\n cacheBytes: number\n cacheEntries: number\n historyLength: number\n inFlightJobs: number\n}\n\ninterface WorkspaceDetail {\n id: string\n mode: string\n createdAt: number\n mounts?: MountSummary[]\n sessions?: SessionSummary[]\n internals?: Internals | null\n}\n\nfunction formatWorkspaceList(items: WorkspaceBrief[]): string {\n if (items.length === 0) return 'No active workspaces.'\n const rows = items.map((w) => [\n w.id,\n w.mode,\n String(w.mountCount),\n String(w.sessionCount),\n formatAge(w.createdAt),\n ])\n return formatTable(['ID', 'MODE', 'MOUNTS', 'SESSIONS', 'AGE'], rows)\n}\n\nfunction formatWorkspaceDetail(d: WorkspaceDetail): string {\n const lines: string[] = [\n `ID: ${d.id}`,\n `Mode: ${d.mode}`,\n `Created: ${formatAge(d.createdAt)} ago`,\n ]\n if (d.mounts !== undefined && d.mounts.length > 0) {\n const rows = d.mounts.map((m) => [m.prefix, m.resource, m.mode])\n lines.push('', 'Mounts:')\n for (const ln of formatTable(['PREFIX', 'RESOURCE', 'MODE'], rows).split('\\n')) {\n lines.push(' ' + ln)\n }\n }\n if (d.sessions !== undefined && d.sessions.length > 0) {\n const rows = d.sessions.map((s) => [s.sessionId, s.cwd])\n lines.push('', 'Sessions:')\n for (const ln of formatTable(['SESSION', 'CWD'], rows).split('\\n')) {\n lines.push(' ' + ln)\n }\n }\n if (d.internals != null) {\n lines.push('', 'Internals:')\n for (const k of ['cacheBytes', 'cacheEntries', 'historyLength', 'inFlightJobs'] as const) {\n lines.push(` ${k.padEnd(16)} ${String(d.internals[k])}`)\n }\n }\n return lines.join('\\n')\n}\n\nexport function registerWorkspaceCommands(program: Command): void {\n const ws = program.command('workspace').description('Manage workspaces.')\n\n ws.command('create')\n .description('Create a workspace; daemon auto-spawns if not running.')\n .argument('<config>', 'YAML/JSON workspace config')\n .option('--id <id>', 'Explicit workspace id')\n .action(async (configPath: string, opts: { id?: string }) => {\n const cfg = loadWorkspaceConfig(configPath)\n const body: { config: unknown; id?: string } = { config: cfg }\n if (opts.id !== undefined) body.id = opts.id\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: true })\n const r = await c.request('POST', '/v1/workspaces', { body: JSON.stringify(body) })\n emit((await handleResponse(r)) as WorkspaceDetail, formatWorkspaceDetail)\n })\n\n ws.command('list')\n .description('List active workspaces.')\n .action(async () => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(\n (await handleResponse(await c.request('GET', '/v1/workspaces'))) as WorkspaceBrief[],\n formatWorkspaceList,\n )\n })\n\n ws.command('get')\n .description('Show full details for one workspace.')\n .argument('<id>')\n .option('--verbose', 'Include cache/dirty/history internals')\n .action(async (id: string, opts: { verbose?: boolean }) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const path = `/v1/workspaces/${id}` + (opts.verbose === true ? '?verbose=true' : '')\n emit(\n (await handleResponse(await c.request('GET', path))) as WorkspaceDetail,\n formatWorkspaceDetail,\n )\n })\n\n ws.command('delete')\n .description('Stop and remove a workspace.')\n .argument('<id>')\n .action(async (id: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n emit(\n (await handleResponse(await c.request('DELETE', `/v1/workspaces/${id}`))) as {\n id: string\n },\n (d) => `Deleted workspace ${d.id}.`,\n )\n })\n\n ws.command('clone')\n .description('Clone a workspace; defaults to fresh local backings + shared remotes.')\n .argument('<srcId>')\n .option('--id <id>')\n .option('--override <path>', 'Partial config JSON/YAML for per-mount overrides')\n .action(async (srcId: string, opts: { id?: string; override?: string }) => {\n const body: Record<string, unknown> = {}\n if (opts.id !== undefined) body.id = opts.id\n if (opts.override !== undefined) {\n const text = readFileSync(opts.override, 'utf-8')\n let parsed: unknown\n try {\n parsed = yamlParse(text)\n } catch (err: unknown) {\n fail(`invalid override YAML/JSON at ${opts.override}: ${String(err)}`, 2)\n }\n body.override = interpolateEnv(parsed, envRecord())\n }\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const r = await c.request('POST', `/v1/workspaces/${srcId}/clone`, {\n body: JSON.stringify(body),\n })\n emit((await handleResponse(r)) as WorkspaceDetail, formatWorkspaceDetail)\n })\n\n ws.command('snapshot')\n .description('Snapshot a workspace to a tar file.')\n .argument('<id>')\n .argument('<output>', 'Path to write the .tar to')\n .action(async (id: string, output: string) => {\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: false })\n const r = await c.request('GET', `/v1/workspaces/${id}/snapshot`)\n if (r.status >= 400) {\n fail(`daemon error ${String(r.status)}: ${await r.text()}`, 2)\n }\n const buf = Buffer.from(await r.arrayBuffer())\n writeFileSync(output, buf)\n emit(\n { workspaceId: id, path: output, bytes: buf.length },\n (d) => `Snapshot ${d.workspaceId} -> ${d.path} (${d.bytes.toLocaleString()} bytes).`,\n )\n })\n\n ws.command('load')\n .description('Load a workspace from a tar file.')\n .argument('<tar>', 'Path to a .tar produced by `mirage workspace snapshot`')\n .option('--id <id>', 'Explicit workspace id')\n .option('--override <path>', 'Partial config YAML/JSON for per-mount overrides')\n .action(async (tarPath: string, opts: { id?: string; override?: string }) => {\n if (!existsSync(tarPath)) fail(`tar file not found: ${tarPath}`, 2)\n const tarBuf = readFileSync(tarPath)\n const form = new FormData()\n form.append('tar', new Blob([tarBuf], { type: 'application/x-tar' }), basename(tarPath))\n if (opts.id !== undefined) form.append('id', opts.id)\n if (opts.override !== undefined) {\n const overrideText = readFileSync(opts.override, 'utf-8')\n let parsed: unknown\n try {\n parsed = yamlParse(overrideText)\n } catch (err: unknown) {\n fail(`invalid override YAML/JSON at ${opts.override}: ${String(err)}`, 2)\n }\n form.append('override', JSON.stringify(interpolateEnv(parsed, envRecord())))\n }\n const c = buildClient()\n await c.ensureRunning({ allowSpawn: true })\n const r = await c.requestMultipart('POST', '/v1/workspaces/load', form)\n if (r.status >= 400) fail(`load failed: ${await r.text()}`, 2)\n emit((await handleResponse(r)) as WorkspaceDetail, formatWorkspaceDetail)\n })\n}\n","#!/usr/bin/env node\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { buildProgram } from '../main.ts'\n\nconst ALIASES: Record<string, string> = {\n '--workspace_id': '--workspace',\n '--session_id': '--session',\n}\n\nfunction rewriteArgv(argv: string[]): string[] {\n return argv.map((arg) => {\n for (const [from, to] of Object.entries(ALIASES)) {\n if (arg === from) return to\n if (arg.startsWith(from + '=')) return to + arg.slice(from.length)\n }\n return arg\n })\n}\n\nbuildProgram()\n .parseAsync(rewriteArgv(process.argv))\n .catch((err: unknown) => {\n console.error(err)\n process.exit(1)\n })\n"],"mappings":";;;;;;;;;;;;AAcA,SAAS,eAAe;;;ACAxB,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY;AAcrB,SAAS,aAAa,GAAyB;AAC7C,MAAI,CAAC,EAAE,QAAS,QAAO,4BAA4B,EAAE,GAAG;AACxD,QAAM,QAAkB,CAAC,gBAAgB,EAAE,QAAQ,OAAO,OAAO,EAAE,GAAG,IAAI,GAAG,EAAE;AAC/E,MAAI,EAAE,YAAY,QAAW;AAC3B,UAAM,KAAK,UAAU,UAAU,KAAK,IAAI,IAAI,MAAO,EAAE,OAAO,CAAC,EAAE;AAAA,EACjE;AACA,MAAI,EAAE,eAAe,QAAW;AAC9B,UAAM,KAAK,GAAG,OAAO,EAAE,UAAU,CAAC,aAAa,EAAE,eAAe,IAAI,KAAK,GAAG,EAAE;AAAA,EAChF;AACA,SAAO,MAAM,KAAK,IAAI,IAAI,UAAU,EAAE,GAAG;AAC3C;AAEA,SAAS,WAAW,GAA2C;AAC7D,QAAM,MAAM,EAAE,OAAO;AACrB,SAAO,gBAAgB,GAAG,GAAG,EAAE,QAAQ,SAAY,SAAS,OAAO,EAAE,GAAG,CAAC,KAAK,EAAE;AAClF;AAEA,SAAS,cAAc,GAAuC;AAC5D,MAAI,EAAE,iBAAiB,KAAM,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,WAAW,GAAqD;AACvE,MAAI,EAAE,OAAQ,QAAO,cAAc,OAAO,EAAE,OAAO,GAAG,CAAC;AACvD,MAAI,EAAE,QAAQ,QAAQ,EAAE,QAAQ,OAAW,QAAO;AAClD,SAAO,qBAAqB,OAAO,EAAE,GAAG,CAAC;AAC3C;AAEA,SAAS,cAAsB;AAC7B,SAAO,KAAK,QAAQ,GAAG,WAAW,YAAY;AAChD;AAEA,SAAS,UAAyB;AAChC,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,WAAW,CAAC,EAAG,QAAO;AAC3B,QAAM,OAAO,aAAa,GAAG,OAAO,EAAE,KAAK;AAC3C,QAAM,MAAM,OAAO,IAAI;AACvB,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACjD;AAEA,SAAS,aAAa,KAAsB;AAC1C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,KAAc;AACrB,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,QAAS,QAAO;AAC7B,QAAI,SAAS,QAAS,QAAO;AAC7B,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc;AACrB,SAAO,WAAW,mBAAmB,CAAC;AACxC;AAEA,eAAe,mBAAmB,WAAqC;AACrE,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,IAAI,YAAY;AACtB,QAAI,CAAE,MAAM,EAAE,YAAY,GAAG,EAAI,QAAO;AACxC,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAAA,EAC7C;AACA,SAAO;AACT;AAEO,SAAS,uBAAuB,SAAwB;AAC7D,QAAM,IAAI,QAAQ,QAAQ,QAAQ,EAAE,YAAY,sCAAsC;AAEtF,IAAE,QAAQ,QAAQ,EACf,YAAY,mDAAmD,EAC/D,OAAO,YAAY;AAClB,UAAM,MAAM,QAAQ;AACpB,UAAM,IAAI,YAAY;AACtB,UAAM,MAAM,EAAE,SAAS;AACvB,QAAI;AACF,YAAM,IAAI,MAAM,EAAE,QAAQ,OAAO,YAAY;AAC7C,UAAI,EAAE,WAAW,KAAK;AACpB,aAAK,EAAE,SAAS,OAAO,KAAK,IAAI,GAAmB,YAAY;AAC/D,gBAAQ,KAAK,CAAC;AAAA,MAChB;AACA,YAAM,OAAQ,MAAM,EAAE,KAAK;AAC3B,WAAK,EAAE,SAAS,MAAM,KAAK,KAAK,GAAG,KAAK,GAAmB,YAAY;AAAA,IACzE,QAAQ;AACN,WAAK,EAAE,SAAS,OAAO,KAAK,IAAI,GAAmB,YAAY;AAC/D,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF,CAAC;AAEH,IAAE,QAAQ,MAAM,EACb,YAAY,6BAA6B,EACzC,OAAO,iBAAiB,qCAAqC,GAAG,EAChE,OAAO,OAAO,SAA8B;AAC3C,UAAM,YAAY,OAAO,KAAK,OAAO,IAAI;AACzC,UAAM,IAAI,YAAY;AACtB,QAAI;AACJ,QAAI;AACF,UAAI,MAAM,EAAE,QAAQ,QAAQ,cAAc;AAAA,IAC5C,SAAS,KAAc;AACrB,WAAK,yBAAyB,OAAO,GAAG,CAAC,EAAE;AAAA,IAC7C;AACA,QAAI,EAAE,UAAU,IAAK,MAAK,oBAAoB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;AACjE,QAAI,MAAM,mBAAmB,SAAS,GAAG;AACvC,WAAK,EAAE,SAAS,MAAM,KAAK,WAAW,GAAG,UAAU;AACnD;AAAA,IACF;AACA,UAAM,MAAM,QAAQ;AACpB,QAAI,QAAQ,QAAQ,aAAa,GAAG,GAAG;AACrC,UAAI;AACF,gBAAQ,KAAK,KAAK,SAAS;AAC3B,aAAK,EAAE,SAAS,MAAM,KAAK,WAAW,IAAI,GAAG,UAAU;AACvD;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AACA,SAAK,8BAA8B,KAAK,OAAO,2BAA2B,CAAC;AAAA,EAC7E,CAAC;AAEH,IAAE,QAAQ,SAAS,EAChB,YAAY,4DAA4D,EACxE,OAAO,iBAAiB,qCAAqC,GAAG,EAChE,OAAO,WAAW,kCAAkC,EACpD,OAAO,OAAO,SAA+C;AAC5D,UAAM,YAAY,OAAO,KAAK,OAAO,IAAI;AACzC,QAAI;AACF,YAAM,YAAY,EAAE,QAAQ,QAAQ,cAAc;AAAA,IACpD,QAAQ;AAAA,IAER;AACA,QAAI,CAAE,MAAM,mBAAmB,SAAS,GAAI;AAC1C,YAAM,MAAM,QAAQ;AACpB,UAAI,QAAQ,QAAQ,aAAa,GAAG,GAAG;AACrC,YAAI;AACF,kBAAQ,KAAK,KAAK,SAAS;AAAA,QAC7B,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,QAAI,KAAK,UAAU,MAAM;AACvB,YAAM,YAAY,EAAE,cAAc,EAAE,YAAY,KAAK,CAAC;AACtD,WAAK,EAAE,WAAW,MAAM,cAAc,KAAK,GAAG,aAAa;AAC3D;AAAA,IACF;AACA;AAAA,MACE;AAAA,QACE,WAAW;AAAA,QACX,cAAc;AAAA,QACd,MAAM;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AAEH,IAAE,QAAQ,MAAM,EACb,YAAY,6DAA6D,EACzE,OAAO,MAAM;AACZ,UAAM,MAAM,QAAQ;AACpB,QAAI,QAAQ,MAAM;AAChB,WAAK,EAAE,QAAQ,OAAO,QAAQ,qBAAqB,KAAK,KAAK,GAAG,UAAU;AAC1E;AAAA,IACF;AACA,QAAI,CAAC,aAAa,GAAG,GAAG;AACtB,WAAK,EAAE,QAAQ,OAAO,QAAQ,wBAAwB,IAAI,GAAG,UAAU;AACvE;AAAA,IACF;AACA,QAAI;AACF,cAAQ,KAAK,KAAK,SAAS;AAAA,IAC7B,QAAQ;AACN,WAAK,EAAE,QAAQ,OAAO,QAAQ,gBAAgB,IAAI,GAAG,UAAU;AAC/D;AAAA,IACF;AACA,SAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,UAAU;AAAA,EACxC,CAAC;AACL;;;AC1LO,SAAS,uBAAuB,SAAwB;AAC7D,UACG,QAAQ,SAAS,EACjB,YAAY,oBAAoB,EAChC,eAAe,wBAAwB,cAAc,EACrD,eAAe,2BAA2B,0BAA0B,EACpE,OAAO,sBAAsB,YAAY,EACzC,OAAO,QAAQ,uCAAuC,EACtD;AAAA,IACC,OAAO,SAAiF;AACtF,YAAM,OAAgC,EAAE,SAAS,KAAK,SAAS,WAAW,MAAM;AAChF,UAAI,KAAK,YAAY,OAAW,MAAK,YAAY,KAAK;AACtD,YAAM,OACJ,kBAAkB,KAAK,SAAS,cAAc,KAAK,OAAO,OAAO,qBAAqB;AACxF,YAAM,IAAI,WAAW,mBAAmB,CAAC;AACzC,YAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,WAAK,MAAM,eAAe,MAAM,EAAE,QAAQ,QAAQ,MAAM,EAAE,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC;AAAA,IAC1F;AAAA,EACF;AACJ;;;ACnBA,SAASA,eAAc;AACrB,SAAO,WAAW,mBAAmB,CAAC;AACxC;AAEO,SAAS,oBAAoB,SAAwB;AAC1D,QAAM,MAAM,QAAQ,QAAQ,KAAK,EAAE,YAAY,qBAAqB;AAEpE,MACG,QAAQ,MAAM,EACd,OAAO,sBAAsB,EAC7B,OAAO,OAAO,SAAiC;AAC9C,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,OACJ,cAAc,KAAK,cAAc,SAAY,gBAAgB,KAAK,SAAS,KAAK;AAClF,SAAK,MAAM,eAAe,MAAM,EAAE,QAAQ,OAAO,IAAI,CAAC,CAAC;AAAA,EACzD,CAAC;AAEH,MACG,QAAQ,KAAK,EACb,SAAS,MAAM,EACf,OAAO,OAAO,OAAe;AAC5B,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,SAAK,MAAM,eAAe,MAAM,EAAE,QAAQ,OAAO,YAAY,EAAE,EAAE,CAAC,CAAC;AAAA,EACrE,CAAC;AAEH,MACG,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,OAAO,eAAe,EACtB,OAAO,OAAO,IAAY,SAA+B;AACxD,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,OAAgC,CAAC;AACvC,QAAI,KAAK,YAAY,OAAW,MAAK,WAAW,OAAO,KAAK,OAAO;AACnE;AAAA,MACE,MAAM;AAAA,QACJ,MAAM,EAAE,QAAQ,QAAQ,YAAY,EAAE,SAAS,EAAE,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AAAA,MAC/E;AAAA,IACF;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,QAAQ,EAChB,SAAS,MAAM,EACf,OAAO,OAAO,OAAe;AAC5B,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,SAAK,MAAM,eAAe,MAAM,EAAE,QAAQ,UAAU,YAAY,EAAE,EAAE,CAAC,CAAC;AAAA,EACxE,CAAC;AACL;;;ACnDO,SAAS,yBAAyB,SAAwB;AAC/D,UACG,QAAQ,WAAW,EACnB,YAAY,uDAAuD,EACnE,eAAe,wBAAwB,cAAc,EACrD,eAAe,2BAA2B,2BAA2B,EACrE,OAAO,sBAAsB,YAAY,EACzC,OAAO,OAAO,SAAmE;AAChF,UAAM,OAAgC,EAAE,SAAS,KAAK,SAAS,WAAW,KAAK;AAC/E,QAAI,KAAK,YAAY,OAAW,MAAK,YAAY,KAAK;AACtD,UAAM,IAAI,WAAW,mBAAmB,CAAC;AACzC,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C;AAAA,MACE,MAAM;AAAA,QACJ,MAAM,EAAE,QAAQ,QAAQ,kBAAkB,KAAK,SAAS,YAAY;AAAA,UAClE,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AACL;;;ACpBA,SAASC,eAAc;AACrB,SAAO,WAAW,mBAAmB,CAAC;AACxC;AAEO,SAAS,wBAAwB,SAAwB;AAC9D,QAAM,OAAO,QAAQ,QAAQ,SAAS,EAAE,YAAY,4BAA4B;AAEhF,OACG,QAAQ,QAAQ,EAChB,SAAS,QAAQ,EACjB,OAAO,kBAAkB,EACzB,OAAO,OAAO,MAAc,SAA0B;AACrD,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,OAAgC,CAAC;AACvC,QAAI,KAAK,OAAO,OAAW,MAAK,YAAY,KAAK;AACjD;AAAA,MACE,MAAM;AAAA,QACJ,MAAM,EAAE,QAAQ,QAAQ,kBAAkB,IAAI,aAAa;AAAA,UACzD,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AAEH,OACG,QAAQ,MAAM,EACd,SAAS,QAAQ,EACjB,OAAO,OAAO,SAAiB;AAC9B,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,SAAK,MAAM,eAAe,MAAM,EAAE,QAAQ,OAAO,kBAAkB,IAAI,WAAW,CAAC,CAAC;AAAA,EACtF,CAAC;AAEH,OACG,QAAQ,QAAQ,EAChB,SAAS,QAAQ,EACjB,SAAS,aAAa,EACtB,OAAO,OAAO,MAAc,cAAsB;AACjD,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C;AAAA,MACE,MAAM;AAAA,QACJ,MAAM,EAAE,QAAQ,UAAU,kBAAkB,IAAI,aAAa,SAAS,EAAE;AAAA,MAC1E;AAAA,IACF;AAAA,EACF,CAAC;AACL;;;ACpDA,SAAS,cAAAC,aAAY,gBAAAC,eAAc,qBAAqB;AACxD,SAAS,gBAAgB;AAEzB,SAAS,gBAAgB,2BAA2B;AACpD,SAAS,SAAS,iBAAiB;AAKnC,SAASC,eAAc;AACrB,SAAO,WAAW,mBAAmB,CAAC;AACxC;AAEA,SAAS,YAAoC;AAC3C,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AAChD,QAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI;AAAA,EACtC;AACA,SAAO;AACT;AAqCA,SAAS,oBAAoB,OAAiC;AAC5D,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,OAAO,MAAM,IAAI,CAAC,MAAM;AAAA,IAC5B,EAAE;AAAA,IACF,EAAE;AAAA,IACF,OAAO,EAAE,UAAU;AAAA,IACnB,OAAO,EAAE,YAAY;AAAA,IACrB,UAAU,EAAE,SAAS;AAAA,EACvB,CAAC;AACD,SAAO,YAAY,CAAC,MAAM,QAAQ,UAAU,YAAY,KAAK,GAAG,IAAI;AACtE;AAEA,SAAS,sBAAsB,GAA4B;AACzD,QAAM,QAAkB;AAAA,IACtB,cAAc,EAAE,EAAE;AAAA,IAClB,cAAc,EAAE,IAAI;AAAA,IACpB,cAAc,UAAU,EAAE,SAAS,CAAC;AAAA,EACtC;AACA,MAAI,EAAE,WAAW,UAAa,EAAE,OAAO,SAAS,GAAG;AACjD,UAAM,OAAO,EAAE,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC;AAC/D,UAAM,KAAK,IAAI,SAAS;AACxB,eAAW,MAAM,YAAY,CAAC,UAAU,YAAY,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,GAAG;AAC9E,YAAM,KAAK,OAAO,EAAE;AAAA,IACtB;AAAA,EACF;AACA,MAAI,EAAE,aAAa,UAAa,EAAE,SAAS,SAAS,GAAG;AACrD,UAAM,OAAO,EAAE,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC;AACvD,UAAM,KAAK,IAAI,WAAW;AAC1B,eAAW,MAAM,YAAY,CAAC,WAAW,KAAK,GAAG,IAAI,EAAE,MAAM,IAAI,GAAG;AAClE,YAAM,KAAK,OAAO,EAAE;AAAA,IACtB;AAAA,EACF;AACA,MAAI,EAAE,aAAa,MAAM;AACvB,UAAM,KAAK,IAAI,YAAY;AAC3B,eAAW,KAAK,CAAC,cAAc,gBAAgB,iBAAiB,cAAc,GAAY;AACxF,YAAM,KAAK,KAAK,EAAE,OAAO,EAAE,CAAC,IAAI,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE;AAAA,IAC1D;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,0BAA0B,SAAwB;AAChE,QAAM,KAAK,QAAQ,QAAQ,WAAW,EAAE,YAAY,oBAAoB;AAExE,KAAG,QAAQ,QAAQ,EAChB,YAAY,wDAAwD,EACpE,SAAS,YAAY,4BAA4B,EACjD,OAAO,aAAa,uBAAuB,EAC3C,OAAO,OAAO,YAAoB,SAA0B;AAC3D,UAAM,MAAM,oBAAoB,UAAU;AAC1C,UAAM,OAAyC,EAAE,QAAQ,IAAI;AAC7D,QAAI,KAAK,OAAO,OAAW,MAAK,KAAK,KAAK;AAC1C,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,KAAK,CAAC;AAC1C,UAAM,IAAI,MAAM,EAAE,QAAQ,QAAQ,kBAAkB,EAAE,MAAM,KAAK,UAAU,IAAI,EAAE,CAAC;AAClF,SAAM,MAAM,eAAe,CAAC,GAAuB,qBAAqB;AAAA,EAC1E,CAAC;AAEH,KAAG,QAAQ,MAAM,EACd,YAAY,yBAAyB,EACrC,OAAO,YAAY;AAClB,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C;AAAA,MACG,MAAM,eAAe,MAAM,EAAE,QAAQ,OAAO,gBAAgB,CAAC;AAAA,MAC9D;AAAA,IACF;AAAA,EACF,CAAC;AAEH,KAAG,QAAQ,KAAK,EACb,YAAY,sCAAsC,EAClD,SAAS,MAAM,EACf,OAAO,aAAa,uCAAuC,EAC3D,OAAO,OAAO,IAAY,SAAgC;AACzD,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,OAAO,kBAAkB,EAAE,MAAM,KAAK,YAAY,OAAO,kBAAkB;AACjF;AAAA,MACG,MAAM,eAAe,MAAM,EAAE,QAAQ,OAAO,IAAI,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,EACF,CAAC;AAEH,KAAG,QAAQ,QAAQ,EAChB,YAAY,8BAA8B,EAC1C,SAAS,MAAM,EACf,OAAO,OAAO,OAAe;AAC5B,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C;AAAA,MACG,MAAM,eAAe,MAAM,EAAE,QAAQ,UAAU,kBAAkB,EAAE,EAAE,CAAC;AAAA,MAGvE,CAAC,MAAM,qBAAqB,EAAE,EAAE;AAAA,IAClC;AAAA,EACF,CAAC;AAEH,KAAG,QAAQ,OAAO,EACf,YAAY,uEAAuE,EACnF,SAAS,SAAS,EAClB,OAAO,WAAW,EAClB,OAAO,qBAAqB,kDAAkD,EAC9E,OAAO,OAAO,OAAe,SAA6C;AACzE,UAAM,OAAgC,CAAC;AACvC,QAAI,KAAK,OAAO,OAAW,MAAK,KAAK,KAAK;AAC1C,QAAI,KAAK,aAAa,QAAW;AAC/B,YAAM,OAAOC,cAAa,KAAK,UAAU,OAAO;AAChD,UAAI;AACJ,UAAI;AACF,iBAAS,UAAU,IAAI;AAAA,MACzB,SAAS,KAAc;AACrB,aAAK,iCAAiC,KAAK,QAAQ,KAAK,OAAO,GAAG,CAAC,IAAI,CAAC;AAAA,MAC1E;AACA,WAAK,WAAW,eAAe,QAAQ,UAAU,CAAC;AAAA,IACpD;AACA,UAAM,IAAID,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,IAAI,MAAM,EAAE,QAAQ,QAAQ,kBAAkB,KAAK,UAAU;AAAA,MACjE,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AACD,SAAM,MAAM,eAAe,CAAC,GAAuB,qBAAqB;AAAA,EAC1E,CAAC;AAEH,KAAG,QAAQ,UAAU,EAClB,YAAY,qCAAqC,EACjD,SAAS,MAAM,EACf,SAAS,YAAY,2BAA2B,EAChD,OAAO,OAAO,IAAY,WAAmB;AAC5C,UAAM,IAAIA,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,MAAM,CAAC;AAC3C,UAAM,IAAI,MAAM,EAAE,QAAQ,OAAO,kBAAkB,EAAE,WAAW;AAChE,QAAI,EAAE,UAAU,KAAK;AACnB,WAAK,gBAAgB,OAAO,EAAE,MAAM,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;AAAA,IAC/D;AACA,UAAM,MAAM,OAAO,KAAK,MAAM,EAAE,YAAY,CAAC;AAC7C,kBAAc,QAAQ,GAAG;AACzB;AAAA,MACE,EAAE,aAAa,IAAI,MAAM,QAAQ,OAAO,IAAI,OAAO;AAAA,MACnD,CAAC,MAAM,YAAY,EAAE,WAAW,OAAO,EAAE,IAAI,KAAK,EAAE,MAAM,eAAe,CAAC;AAAA,IAC5E;AAAA,EACF,CAAC;AAEH,KAAG,QAAQ,MAAM,EACd,YAAY,mCAAmC,EAC/C,SAAS,SAAS,wDAAwD,EAC1E,OAAO,aAAa,uBAAuB,EAC3C,OAAO,qBAAqB,kDAAkD,EAC9E,OAAO,OAAO,SAAiB,SAA6C;AAC3E,QAAI,CAACE,YAAW,OAAO,EAAG,MAAK,uBAAuB,OAAO,IAAI,CAAC;AAClE,UAAM,SAASD,cAAa,OAAO;AACnC,UAAM,OAAO,IAAI,SAAS;AAC1B,SAAK,OAAO,OAAO,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,MAAM,oBAAoB,CAAC,GAAG,SAAS,OAAO,CAAC;AACvF,QAAI,KAAK,OAAO,OAAW,MAAK,OAAO,MAAM,KAAK,EAAE;AACpD,QAAI,KAAK,aAAa,QAAW;AAC/B,YAAM,eAAeA,cAAa,KAAK,UAAU,OAAO;AACxD,UAAI;AACJ,UAAI;AACF,iBAAS,UAAU,YAAY;AAAA,MACjC,SAAS,KAAc;AACrB,aAAK,iCAAiC,KAAK,QAAQ,KAAK,OAAO,GAAG,CAAC,IAAI,CAAC;AAAA,MAC1E;AACA,WAAK,OAAO,YAAY,KAAK,UAAU,eAAe,QAAQ,UAAU,CAAC,CAAC,CAAC;AAAA,IAC7E;AACA,UAAM,IAAID,aAAY;AACtB,UAAM,EAAE,cAAc,EAAE,YAAY,KAAK,CAAC;AAC1C,UAAM,IAAI,MAAM,EAAE,iBAAiB,QAAQ,uBAAuB,IAAI;AACtE,QAAI,EAAE,UAAU,IAAK,MAAK,gBAAgB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;AAC7D,SAAM,MAAM,eAAe,CAAC,GAAuB,qBAAqB;AAAA,EAC1E,CAAC;AACL;;;ANzNO,SAAS,eAAwB;AACtC,QAAM,UAAU,IAAI,QAAQ;AAC5B,UACG,KAAK,QAAQ,EACb,YAAY,4DAA4D,EACxE,QAAQ,OAAO;AAClB,4BAA0B,OAAO;AACjC,0BAAwB,OAAO;AAC/B,sBAAoB,OAAO;AAC3B,yBAAuB,OAAO;AAC9B,2BAAyB,OAAO;AAChC,yBAAuB,OAAO;AAC9B,SAAO;AACT;;;AOlBA,IAAM,UAAkC;AAAA,EACtC,kBAAkB;AAAA,EAClB,gBAAgB;AAClB;AAEA,SAAS,YAAY,MAA0B;AAC7C,SAAO,KAAK,IAAI,CAAC,QAAQ;AACvB,eAAW,CAAC,MAAM,EAAE,KAAK,OAAO,QAAQ,OAAO,GAAG;AAChD,UAAI,QAAQ,KAAM,QAAO;AACzB,UAAI,IAAI,WAAW,OAAO,GAAG,EAAG,QAAO,KAAK,IAAI,MAAM,KAAK,MAAM;AAAA,IACnE;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAEA,aAAa,EACV,WAAW,YAAY,QAAQ,IAAI,CAAC,EACpC,MAAM,CAAC,QAAiB;AACvB,UAAQ,MAAM,GAAG;AACjB,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["buildClient","buildClient","existsSync","readFileSync","buildClient","readFileSync","existsSync"]}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync, mkdirSync, openSync } from "fs";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
8
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
9
|
+
var DaemonClient = class {
|
|
10
|
+
settings;
|
|
11
|
+
constructor(settings) {
|
|
12
|
+
this.settings = settings;
|
|
13
|
+
}
|
|
14
|
+
headers(extra) {
|
|
15
|
+
const h = { ...extra };
|
|
16
|
+
if (this.settings.authToken !== "") h.Authorization = `Bearer ${this.settings.authToken}`;
|
|
17
|
+
return h;
|
|
18
|
+
}
|
|
19
|
+
async request(method, path, init = {}) {
|
|
20
|
+
const { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ...rest } = init;
|
|
21
|
+
const headers = {
|
|
22
|
+
...this.headers(),
|
|
23
|
+
...rest.headers ?? {}
|
|
24
|
+
};
|
|
25
|
+
if (rest.body !== void 0 && headers["Content-Type"] === void 0) {
|
|
26
|
+
headers["Content-Type"] = "application/json";
|
|
27
|
+
}
|
|
28
|
+
const ctrl = new AbortController();
|
|
29
|
+
const t = setTimeout(() => {
|
|
30
|
+
ctrl.abort();
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
return await fetch(this.settings.url + path, {
|
|
34
|
+
...rest,
|
|
35
|
+
method,
|
|
36
|
+
headers,
|
|
37
|
+
signal: ctrl.signal
|
|
38
|
+
});
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(t);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async requestMultipart(method, path, form) {
|
|
44
|
+
const ctrl = new AbortController();
|
|
45
|
+
const t = setTimeout(() => {
|
|
46
|
+
ctrl.abort();
|
|
47
|
+
}, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
48
|
+
try {
|
|
49
|
+
return await fetch(this.settings.url + path, {
|
|
50
|
+
method,
|
|
51
|
+
headers: this.headers(),
|
|
52
|
+
body: form,
|
|
53
|
+
signal: ctrl.signal
|
|
54
|
+
});
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(t);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async isReachable(timeoutMs = 500) {
|
|
60
|
+
const ctrl = new AbortController();
|
|
61
|
+
const t = setTimeout(() => {
|
|
62
|
+
ctrl.abort();
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
try {
|
|
65
|
+
const r = await fetch(this.settings.url + "/v1/health", {
|
|
66
|
+
headers: this.headers(),
|
|
67
|
+
signal: ctrl.signal
|
|
68
|
+
});
|
|
69
|
+
return r.status === 200;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(t);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async ensureRunning(opts = {}) {
|
|
77
|
+
const allowSpawn = opts.allowSpawn ?? true;
|
|
78
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
79
|
+
if (await this.isReachable()) return;
|
|
80
|
+
if (!allowSpawn) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`daemon not reachable at ${this.settings.url}; run \`mirage workspace create CONFIG.yaml\``
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
this.spawnDaemon();
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
if (await this.isReachable(300)) return;
|
|
89
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`daemon spawned but did not answer /v1/health within ${String(timeoutMs)}ms`);
|
|
92
|
+
}
|
|
93
|
+
spawnDaemon() {
|
|
94
|
+
const env = {};
|
|
95
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
96
|
+
if (typeof v === "string") env[k] = v;
|
|
97
|
+
}
|
|
98
|
+
env.MIRAGE_DAEMON_PORT = String(this.portFromUrl());
|
|
99
|
+
env.MIRAGE_IDLE_GRACE_SECONDS = String(this.settings.idleGraceSeconds);
|
|
100
|
+
if (this.settings.authToken !== "") env.MIRAGE_AUTH_TOKEN = this.settings.authToken;
|
|
101
|
+
if (this.settings.persistDir !== "") env.MIRAGE_PERSIST_DIR = this.settings.persistDir;
|
|
102
|
+
const logDir = join(homedir(), ".mirage");
|
|
103
|
+
mkdirSync(logDir, { recursive: true });
|
|
104
|
+
const out = openSync(join(logDir, "daemon.log"), "a");
|
|
105
|
+
const daemonEntry = requireFromHere.resolve("@struktoai/mirage-server/bin/daemon");
|
|
106
|
+
if (!existsSync(daemonEntry)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`daemon binary not found at ${daemonEntry}; reinstall @struktoai/mirage-server`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const child = spawn(process.execPath, [daemonEntry], {
|
|
112
|
+
env,
|
|
113
|
+
detached: true,
|
|
114
|
+
stdio: ["ignore", out, out]
|
|
115
|
+
});
|
|
116
|
+
child.on("error", (err) => {
|
|
117
|
+
console.error("failed to spawn daemon:", err);
|
|
118
|
+
});
|
|
119
|
+
child.unref();
|
|
120
|
+
}
|
|
121
|
+
portFromUrl() {
|
|
122
|
+
const u = new URL(this.settings.url);
|
|
123
|
+
return Number(u.port) || 8765;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function makeClient(settings) {
|
|
127
|
+
return new DaemonClient(settings);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/settings.ts
|
|
131
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
132
|
+
import { homedir as homedir2 } from "os";
|
|
133
|
+
import { join as join2 } from "path";
|
|
134
|
+
var DEFAULT_DAEMON_URL = "http://127.0.0.1:8765";
|
|
135
|
+
function defaultConfigPath() {
|
|
136
|
+
return join2(homedir2(), ".mirage", "config.toml");
|
|
137
|
+
}
|
|
138
|
+
function parseValue(raw) {
|
|
139
|
+
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
140
|
+
return raw;
|
|
141
|
+
}
|
|
142
|
+
function readDaemonTable(path) {
|
|
143
|
+
if (!existsSync2(path)) return {};
|
|
144
|
+
const text = readFileSync(path, "utf-8");
|
|
145
|
+
const out = {};
|
|
146
|
+
let inDaemon = false;
|
|
147
|
+
for (const line of text.split("\n")) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
150
|
+
if (trimmed === "[daemon]") {
|
|
151
|
+
inDaemon = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (trimmed.startsWith("[")) {
|
|
155
|
+
inDaemon = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!inDaemon) continue;
|
|
159
|
+
const eq = trimmed.indexOf("=");
|
|
160
|
+
if (eq < 0) continue;
|
|
161
|
+
const key = trimmed.slice(0, eq).trim();
|
|
162
|
+
out[key] = parseValue(trimmed.slice(eq + 1).trim());
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function loadDaemonSettings(options = {}) {
|
|
167
|
+
const env = options.env ?? process.env;
|
|
168
|
+
const path = options.configPath ?? defaultConfigPath();
|
|
169
|
+
const table = readDaemonTable(path);
|
|
170
|
+
const settings = {
|
|
171
|
+
url: table.url ?? DEFAULT_DAEMON_URL,
|
|
172
|
+
persistDir: table.persist_dir ?? "",
|
|
173
|
+
authToken: table.auth_token ?? "",
|
|
174
|
+
idleGraceSeconds: Number(table.idle_grace_seconds ?? "30")
|
|
175
|
+
};
|
|
176
|
+
if (env.MIRAGE_DAEMON_URL !== void 0 && env.MIRAGE_DAEMON_URL !== "") {
|
|
177
|
+
settings.url = env.MIRAGE_DAEMON_URL;
|
|
178
|
+
}
|
|
179
|
+
if (env.MIRAGE_TOKEN !== void 0 && env.MIRAGE_TOKEN !== "") {
|
|
180
|
+
settings.authToken = env.MIRAGE_TOKEN;
|
|
181
|
+
}
|
|
182
|
+
return settings;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/output.ts
|
|
186
|
+
function emit(obj, human) {
|
|
187
|
+
if (human !== void 0 && process.stdout.isTTY) {
|
|
188
|
+
process.stdout.write(human(obj) + "\n");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
192
|
+
}
|
|
193
|
+
function fail(message, exitCode = 1) {
|
|
194
|
+
process.stderr.write(message + "\n");
|
|
195
|
+
process.exit(exitCode);
|
|
196
|
+
}
|
|
197
|
+
async function handleResponse(r) {
|
|
198
|
+
if (r.status >= 400) {
|
|
199
|
+
let detail = await r.text();
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(detail);
|
|
202
|
+
if (parsed.detail !== void 0) detail = parsed.detail;
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
fail(`daemon error ${String(r.status)}: ${detail}`, 2);
|
|
206
|
+
}
|
|
207
|
+
if (r.status === 204) return {};
|
|
208
|
+
const text = await r.text();
|
|
209
|
+
if (text === "") return {};
|
|
210
|
+
return JSON.parse(text);
|
|
211
|
+
}
|
|
212
|
+
function formatAge(epoch, now = Date.now() / 1e3) {
|
|
213
|
+
const delta = Math.max(0, now - epoch);
|
|
214
|
+
if (delta < 60) return `${String(Math.floor(delta))}s`;
|
|
215
|
+
if (delta < 3600) return `${String(Math.floor(delta / 60))}m`;
|
|
216
|
+
if (delta < 86400) return `${String(Math.floor(delta / 3600))}h`;
|
|
217
|
+
return `${String(Math.floor(delta / 86400))}d`;
|
|
218
|
+
}
|
|
219
|
+
function formatTable(headers, rows) {
|
|
220
|
+
if (rows.length === 0) return headers.join(" ");
|
|
221
|
+
const norm = rows.map((row) => row.map((c) => c ?? ""));
|
|
222
|
+
const widths = headers.map((h) => h.length);
|
|
223
|
+
for (const row of norm) {
|
|
224
|
+
for (let i = 0; i < row.length; i++) {
|
|
225
|
+
widths[i] = Math.max(widths[i] ?? 0, row[i]?.length ?? 0);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
229
|
+
const out = [];
|
|
230
|
+
out.push(
|
|
231
|
+
headers.map((h, i) => pad(h, widths[i] ?? h.length)).join(" ").replace(/\s+$/, "")
|
|
232
|
+
);
|
|
233
|
+
for (const row of norm) {
|
|
234
|
+
out.push(
|
|
235
|
+
row.map((c, i) => pad(c, widths[i] ?? c.length)).join(" ").replace(/\s+$/, "")
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return out.join("\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export {
|
|
242
|
+
DaemonClient,
|
|
243
|
+
makeClient,
|
|
244
|
+
DEFAULT_DAEMON_URL,
|
|
245
|
+
loadDaemonSettings,
|
|
246
|
+
emit,
|
|
247
|
+
fail,
|
|
248
|
+
handleResponse,
|
|
249
|
+
formatAge,
|
|
250
|
+
formatTable
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=chunk-42V4PGGP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/settings.ts","../src/output.ts"],"sourcesContent":["// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { spawn } from 'node:child_process'\nimport { existsSync, mkdirSync, openSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { DaemonSettings } from './settings.ts'\n\nconst requireFromHere = createRequire(import.meta.url)\n\nconst DEFAULT_REQUEST_TIMEOUT_MS = 60_000\n\nexport class DaemonClient {\n readonly settings: DaemonSettings\n\n constructor(settings: DaemonSettings) {\n this.settings = settings\n }\n\n private headers(extra?: Record<string, string>): Record<string, string> {\n const h: Record<string, string> = { ...extra }\n if (this.settings.authToken !== '') h.Authorization = `Bearer ${this.settings.authToken}`\n return h\n }\n\n async request(\n method: string,\n path: string,\n init: RequestInit & { timeoutMs?: number } = {},\n ): Promise<Response> {\n const { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ...rest } = init\n const headers: Record<string, string> = {\n ...this.headers(),\n ...((rest.headers ?? {}) as Record<string, string>),\n }\n if (rest.body !== undefined && headers['Content-Type'] === undefined) {\n headers['Content-Type'] = 'application/json'\n }\n const ctrl = new AbortController()\n const t = setTimeout(() => {\n ctrl.abort()\n }, timeoutMs)\n try {\n return await fetch(this.settings.url + path, {\n ...rest,\n method,\n headers,\n signal: ctrl.signal,\n })\n } finally {\n clearTimeout(t)\n }\n }\n\n async requestMultipart(method: string, path: string, form: FormData): Promise<Response> {\n const ctrl = new AbortController()\n const t = setTimeout(() => {\n ctrl.abort()\n }, DEFAULT_REQUEST_TIMEOUT_MS)\n try {\n return await fetch(this.settings.url + path, {\n method,\n headers: this.headers(),\n body: form,\n signal: ctrl.signal,\n })\n } finally {\n clearTimeout(t)\n }\n }\n\n async isReachable(timeoutMs = 500): Promise<boolean> {\n const ctrl = new AbortController()\n const t = setTimeout(() => {\n ctrl.abort()\n }, timeoutMs)\n try {\n const r = await fetch(this.settings.url + '/v1/health', {\n headers: this.headers(),\n signal: ctrl.signal,\n })\n return r.status === 200\n } catch {\n return false\n } finally {\n clearTimeout(t)\n }\n }\n\n async ensureRunning(opts: { allowSpawn?: boolean; timeoutMs?: number } = {}): Promise<void> {\n const allowSpawn = opts.allowSpawn ?? true\n const timeoutMs = opts.timeoutMs ?? 5000\n if (await this.isReachable()) return\n if (!allowSpawn) {\n throw new Error(\n `daemon not reachable at ${this.settings.url}; run \\`mirage workspace create CONFIG.yaml\\``,\n )\n }\n this.spawnDaemon()\n const deadline = Date.now() + timeoutMs\n while (Date.now() < deadline) {\n if (await this.isReachable(300)) return\n await new Promise((r) => setTimeout(r, 100))\n }\n throw new Error(`daemon spawned but did not answer /v1/health within ${String(timeoutMs)}ms`)\n }\n\n private spawnDaemon(): void {\n const env: Record<string, string> = {}\n for (const [k, v] of Object.entries(process.env)) {\n if (typeof v === 'string') env[k] = v\n }\n env.MIRAGE_DAEMON_PORT = String(this.portFromUrl())\n env.MIRAGE_IDLE_GRACE_SECONDS = String(this.settings.idleGraceSeconds)\n if (this.settings.authToken !== '') env.MIRAGE_AUTH_TOKEN = this.settings.authToken\n if (this.settings.persistDir !== '') env.MIRAGE_PERSIST_DIR = this.settings.persistDir\n const logDir = join(homedir(), '.mirage')\n mkdirSync(logDir, { recursive: true })\n const out = openSync(join(logDir, 'daemon.log'), 'a')\n const daemonEntry = requireFromHere.resolve('@struktoai/mirage-server/bin/daemon')\n if (!existsSync(daemonEntry)) {\n throw new Error(\n `daemon binary not found at ${daemonEntry}; reinstall @struktoai/mirage-server`,\n )\n }\n const child = spawn(process.execPath, [daemonEntry], {\n env,\n detached: true,\n stdio: ['ignore', out, out],\n })\n child.on('error', (err) => {\n console.error('failed to spawn daemon:', err)\n })\n child.unref()\n }\n\n private portFromUrl(): number {\n const u = new URL(this.settings.url)\n return Number(u.port) || 8765\n }\n}\n\nexport function makeClient(settings: DaemonSettings): DaemonClient {\n return new DaemonClient(settings)\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport const DEFAULT_DAEMON_URL = 'http://127.0.0.1:8765'\n\nexport interface DaemonSettings {\n url: string\n persistDir: string\n authToken: string\n idleGraceSeconds: number\n}\n\nexport interface LoadOptions {\n env?: Record<string, string | undefined>\n configPath?: string\n}\n\nfunction defaultConfigPath(): string {\n return join(homedir(), '.mirage', 'config.toml')\n}\n\nfunction parseValue(raw: string): string {\n if (raw.startsWith('\"') && raw.endsWith('\"')) return raw.slice(1, -1)\n return raw\n}\n\nfunction readDaemonTable(path: string): Record<string, string> {\n if (!existsSync(path)) return {}\n const text = readFileSync(path, 'utf-8')\n const out: Record<string, string> = {}\n let inDaemon = false\n for (const line of text.split('\\n')) {\n const trimmed = line.trim()\n if (trimmed === '' || trimmed.startsWith('#')) continue\n if (trimmed === '[daemon]') {\n inDaemon = true\n continue\n }\n if (trimmed.startsWith('[')) {\n inDaemon = false\n continue\n }\n if (!inDaemon) continue\n const eq = trimmed.indexOf('=')\n if (eq < 0) continue\n const key = trimmed.slice(0, eq).trim()\n out[key] = parseValue(trimmed.slice(eq + 1).trim())\n }\n return out\n}\n\nexport function loadDaemonSettings(options: LoadOptions = {}): DaemonSettings {\n const env = options.env ?? (process.env as Record<string, string | undefined>)\n const path = options.configPath ?? defaultConfigPath()\n const table = readDaemonTable(path)\n const settings: DaemonSettings = {\n url: table.url ?? DEFAULT_DAEMON_URL,\n persistDir: table.persist_dir ?? '',\n authToken: table.auth_token ?? '',\n idleGraceSeconds: Number(table.idle_grace_seconds ?? '30'),\n }\n if (env.MIRAGE_DAEMON_URL !== undefined && env.MIRAGE_DAEMON_URL !== '') {\n settings.url = env.MIRAGE_DAEMON_URL\n }\n if (env.MIRAGE_TOKEN !== undefined && env.MIRAGE_TOKEN !== '') {\n settings.authToken = env.MIRAGE_TOKEN\n }\n return settings\n}\n","// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========\n\nexport type Formatter<T = unknown> = (obj: T) => string\n\nexport function emit<T = unknown>(obj: T, human?: Formatter<T>): void {\n if (human !== undefined && process.stdout.isTTY) {\n process.stdout.write(human(obj) + '\\n')\n return\n }\n process.stdout.write(JSON.stringify(obj, null, 2) + '\\n')\n}\n\nexport function fail(message: string, exitCode = 1): never {\n process.stderr.write(message + '\\n')\n process.exit(exitCode)\n}\n\nexport async function handleResponse(r: Response): Promise<unknown> {\n if (r.status >= 400) {\n let detail = await r.text()\n try {\n const parsed = JSON.parse(detail) as { detail?: string }\n if (parsed.detail !== undefined) detail = parsed.detail\n } catch {\n // body wasn't JSON; fall through with the raw text as detail\n }\n fail(`daemon error ${String(r.status)}: ${detail}`, 2)\n }\n if (r.status === 204) return {}\n const text = await r.text()\n if (text === '') return {}\n return JSON.parse(text) as unknown\n}\n\nexport function formatAge(epoch: number, now: number = Date.now() / 1000): string {\n const delta = Math.max(0, now - epoch)\n if (delta < 60) return `${String(Math.floor(delta))}s`\n if (delta < 3600) return `${String(Math.floor(delta / 60))}m`\n if (delta < 86400) return `${String(Math.floor(delta / 3600))}h`\n return `${String(Math.floor(delta / 86400))}d`\n}\n\nexport function formatTable(headers: string[], rows: (string | undefined)[][]): string {\n if (rows.length === 0) return headers.join(' ')\n const norm: string[][] = rows.map((row) => row.map((c) => c ?? ''))\n const widths = headers.map((h) => h.length)\n for (const row of norm) {\n for (let i = 0; i < row.length; i++) {\n widths[i] = Math.max(widths[i] ?? 0, row[i]?.length ?? 0)\n }\n }\n const pad = (s: string, w: number) => s + ' '.repeat(Math.max(0, w - s.length))\n const out: string[] = []\n out.push(\n headers\n .map((h, i) => pad(h, widths[i] ?? h.length))\n .join(' ')\n .replace(/\\s+$/, ''),\n )\n for (const row of norm) {\n out.push(\n row\n .map((c, i) => pad(c, widths[i] ?? c.length))\n .join(' ')\n .replace(/\\s+$/, ''),\n )\n }\n return out.join('\\n')\n}\n"],"mappings":";AAcA,SAAS,aAAa;AACtB,SAAS,YAAY,WAAW,gBAAgB;AAChD,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,YAAY;AAGrB,IAAM,kBAAkB,cAAc,YAAY,GAAG;AAErD,IAAM,6BAA6B;AAE5B,IAAM,eAAN,MAAmB;AAAA,EACf;AAAA,EAET,YAAY,UAA0B;AACpC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEQ,QAAQ,OAAwD;AACtE,UAAM,IAA4B,EAAE,GAAG,MAAM;AAC7C,QAAI,KAAK,SAAS,cAAc,GAAI,GAAE,gBAAgB,UAAU,KAAK,SAAS,SAAS;AACvF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QACJ,QACA,MACA,OAA6C,CAAC,GAC3B;AACnB,UAAM,EAAE,YAAY,4BAA4B,GAAG,KAAK,IAAI;AAC5D,UAAM,UAAkC;AAAA,MACtC,GAAG,KAAK,QAAQ;AAAA,MAChB,GAAK,KAAK,WAAW,CAAC;AAAA,IACxB;AACA,QAAI,KAAK,SAAS,UAAa,QAAQ,cAAc,MAAM,QAAW;AACpE,cAAQ,cAAc,IAAI;AAAA,IAC5B;AACA,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,IAAI,WAAW,MAAM;AACzB,WAAK,MAAM;AAAA,IACb,GAAG,SAAS;AACZ,QAAI;AACF,aAAO,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM;AAAA,QAC3C,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,QAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACH,UAAE;AACA,mBAAa,CAAC;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,QAAgB,MAAc,MAAmC;AACtF,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,IAAI,WAAW,MAAM;AACzB,WAAK,MAAM;AAAA,IACb,GAAG,0BAA0B;AAC7B,QAAI;AACF,aAAO,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM;AAAA,QAC3C;AAAA,QACA,SAAS,KAAK,QAAQ;AAAA,QACtB,MAAM;AAAA,QACN,QAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACH,UAAE;AACA,mBAAa,CAAC;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,YAAY,KAAuB;AACnD,UAAM,OAAO,IAAI,gBAAgB;AACjC,UAAM,IAAI,WAAW,MAAM;AACzB,WAAK,MAAM;AAAA,IACb,GAAG,SAAS;AACZ,QAAI;AACF,YAAM,IAAI,MAAM,MAAM,KAAK,SAAS,MAAM,cAAc;AAAA,QACtD,SAAS,KAAK,QAAQ;AAAA,QACtB,QAAQ,KAAK;AAAA,MACf,CAAC;AACD,aAAO,EAAE,WAAW;AAAA,IACtB,QAAQ;AACN,aAAO;AAAA,IACT,UAAE;AACA,mBAAa,CAAC;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,OAAqD,CAAC,GAAkB;AAC1F,UAAM,aAAa,KAAK,cAAc;AACtC,UAAM,YAAY,KAAK,aAAa;AACpC,QAAI,MAAM,KAAK,YAAY,EAAG;AAC9B,QAAI,CAAC,YAAY;AACf,YAAM,IAAI;AAAA,QACR,2BAA2B,KAAK,SAAS,GAAG;AAAA,MAC9C;AAAA,IACF;AACA,SAAK,YAAY;AACjB,UAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,WAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAI,MAAM,KAAK,YAAY,GAAG,EAAG;AACjC,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAAA,IAC7C;AACA,UAAM,IAAI,MAAM,uDAAuD,OAAO,SAAS,CAAC,IAAI;AAAA,EAC9F;AAAA,EAEQ,cAAoB;AAC1B,UAAM,MAA8B,CAAC;AACrC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AAChD,UAAI,OAAO,MAAM,SAAU,KAAI,CAAC,IAAI;AAAA,IACtC;AACA,QAAI,qBAAqB,OAAO,KAAK,YAAY,CAAC;AAClD,QAAI,4BAA4B,OAAO,KAAK,SAAS,gBAAgB;AACrE,QAAI,KAAK,SAAS,cAAc,GAAI,KAAI,oBAAoB,KAAK,SAAS;AAC1E,QAAI,KAAK,SAAS,eAAe,GAAI,KAAI,qBAAqB,KAAK,SAAS;AAC5E,UAAM,SAAS,KAAK,QAAQ,GAAG,SAAS;AACxC,cAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AACrC,UAAM,MAAM,SAAS,KAAK,QAAQ,YAAY,GAAG,GAAG;AACpD,UAAM,cAAc,gBAAgB,QAAQ,qCAAqC;AACjF,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,YAAM,IAAI;AAAA,QACR,8BAA8B,WAAW;AAAA,MAC3C;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,WAAW,GAAG;AAAA,MACnD;AAAA,MACA,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,KAAK,GAAG;AAAA,IAC5B,CAAC;AACD,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,cAAQ,MAAM,2BAA2B,GAAG;AAAA,IAC9C,CAAC;AACD,UAAM,MAAM;AAAA,EACd;AAAA,EAEQ,cAAsB;AAC5B,UAAM,IAAI,IAAI,IAAI,KAAK,SAAS,GAAG;AACnC,WAAO,OAAO,EAAE,IAAI,KAAK;AAAA,EAC3B;AACF;AAEO,SAAS,WAAW,UAAwC;AACjE,SAAO,IAAI,aAAa,QAAQ;AAClC;;;AC/IA,SAAS,cAAAA,aAAY,oBAAoB;AACzC,SAAS,WAAAC,gBAAe;AACxB,SAAS,QAAAC,aAAY;AAEd,IAAM,qBAAqB;AAclC,SAAS,oBAA4B;AACnC,SAAOA,MAAKD,SAAQ,GAAG,WAAW,aAAa;AACjD;AAEA,SAAS,WAAW,KAAqB;AACvC,MAAI,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,EAAG,QAAO,IAAI,MAAM,GAAG,EAAE;AACpE,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAsC;AAC7D,MAAI,CAACD,YAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,QAAM,OAAO,aAAa,MAAM,OAAO;AACvC,QAAM,MAA8B,CAAC;AACrC,MAAI,WAAW;AACf,aAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,EAAG;AAC/C,QAAI,YAAY,YAAY;AAC1B,iBAAW;AACX;AAAA,IACF;AACA,QAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,iBAAW;AACX;AAAA,IACF;AACA,QAAI,CAAC,SAAU;AACf,UAAM,KAAK,QAAQ,QAAQ,GAAG;AAC9B,QAAI,KAAK,EAAG;AACZ,UAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AACtC,QAAI,GAAG,IAAI,WAAW,QAAQ,MAAM,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,EACpD;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,UAAuB,CAAC,GAAmB;AAC5E,QAAM,MAAM,QAAQ,OAAQ,QAAQ;AACpC,QAAM,OAAO,QAAQ,cAAc,kBAAkB;AACrD,QAAM,QAAQ,gBAAgB,IAAI;AAClC,QAAM,WAA2B;AAAA,IAC/B,KAAK,MAAM,OAAO;AAAA,IAClB,YAAY,MAAM,eAAe;AAAA,IACjC,WAAW,MAAM,cAAc;AAAA,IAC/B,kBAAkB,OAAO,MAAM,sBAAsB,IAAI;AAAA,EAC3D;AACA,MAAI,IAAI,sBAAsB,UAAa,IAAI,sBAAsB,IAAI;AACvE,aAAS,MAAM,IAAI;AAAA,EACrB;AACA,MAAI,IAAI,iBAAiB,UAAa,IAAI,iBAAiB,IAAI;AAC7D,aAAS,YAAY,IAAI;AAAA,EAC3B;AACA,SAAO;AACT;;;ACnEO,SAAS,KAAkB,KAAQ,OAA4B;AACpE,MAAI,UAAU,UAAa,QAAQ,OAAO,OAAO;AAC/C,YAAQ,OAAO,MAAM,MAAM,GAAG,IAAI,IAAI;AACtC;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,KAAK,UAAU,KAAK,MAAM,CAAC,IAAI,IAAI;AAC1D;AAEO,SAAS,KAAK,SAAiB,WAAW,GAAU;AACzD,UAAQ,OAAO,MAAM,UAAU,IAAI;AACnC,UAAQ,KAAK,QAAQ;AACvB;AAEA,eAAsB,eAAe,GAA+B;AAClE,MAAI,EAAE,UAAU,KAAK;AACnB,QAAI,SAAS,MAAM,EAAE,KAAK;AAC1B,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,MAAM;AAChC,UAAI,OAAO,WAAW,OAAW,UAAS,OAAO;AAAA,IACnD,QAAQ;AAAA,IAER;AACA,SAAK,gBAAgB,OAAO,EAAE,MAAM,CAAC,KAAK,MAAM,IAAI,CAAC;AAAA,EACvD;AACA,MAAI,EAAE,WAAW,IAAK,QAAO,CAAC;AAC9B,QAAM,OAAO,MAAM,EAAE,KAAK;AAC1B,MAAI,SAAS,GAAI,QAAO,CAAC;AACzB,SAAO,KAAK,MAAM,IAAI;AACxB;AAEO,SAAS,UAAU,OAAe,MAAc,KAAK,IAAI,IAAI,KAAc;AAChF,QAAM,QAAQ,KAAK,IAAI,GAAG,MAAM,KAAK;AACrC,MAAI,QAAQ,GAAI,QAAO,GAAG,OAAO,KAAK,MAAM,KAAK,CAAC,CAAC;AACnD,MAAI,QAAQ,KAAM,QAAO,GAAG,OAAO,KAAK,MAAM,QAAQ,EAAE,CAAC,CAAC;AAC1D,MAAI,QAAQ,MAAO,QAAO,GAAG,OAAO,KAAK,MAAM,QAAQ,IAAI,CAAC,CAAC;AAC7D,SAAO,GAAG,OAAO,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAC7C;AAEO,SAAS,YAAY,SAAmB,MAAwC;AACrF,MAAI,KAAK,WAAW,EAAG,QAAO,QAAQ,KAAK,IAAI;AAC/C,QAAM,OAAmB,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC;AAClE,QAAM,SAAS,QAAQ,IAAI,CAAC,MAAM,EAAE,MAAM;AAC1C,aAAW,OAAO,MAAM;AACtB,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,aAAO,CAAC,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC;AAAA,IAC1D;AAAA,EACF;AACA,QAAM,MAAM,CAAC,GAAW,MAAc,IAAI,IAAI,OAAO,KAAK,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC;AAC9E,QAAM,MAAgB,CAAC;AACvB,MAAI;AAAA,IACF,QACG,IAAI,CAAC,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,EAC3C,KAAK,IAAI,EACT,QAAQ,QAAQ,EAAE;AAAA,EACvB;AACA,aAAW,OAAO,MAAM;AACtB,QAAI;AAAA,MACF,IACG,IAAI,CAAC,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,EAC3C,KAAK,IAAI,EACT,QAAQ,QAAQ,EAAE;AAAA,IACvB;AAAA,EACF;AACA,SAAO,IAAI,KAAK,IAAI;AACtB;","names":["existsSync","homedir","join"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
declare const DEFAULT_DAEMON_URL = "http://127.0.0.1:8765";
|
|
2
|
+
interface DaemonSettings {
|
|
3
|
+
url: string;
|
|
4
|
+
persistDir: string;
|
|
5
|
+
authToken: string;
|
|
6
|
+
idleGraceSeconds: number;
|
|
7
|
+
}
|
|
8
|
+
interface LoadOptions {
|
|
9
|
+
env?: Record<string, string | undefined>;
|
|
10
|
+
configPath?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function loadDaemonSettings(options?: LoadOptions): DaemonSettings;
|
|
13
|
+
|
|
14
|
+
declare class DaemonClient {
|
|
15
|
+
readonly settings: DaemonSettings;
|
|
16
|
+
constructor(settings: DaemonSettings);
|
|
17
|
+
private headers;
|
|
18
|
+
request(method: string, path: string, init?: RequestInit & {
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}): Promise<Response>;
|
|
21
|
+
requestMultipart(method: string, path: string, form: FormData): Promise<Response>;
|
|
22
|
+
isReachable(timeoutMs?: number): Promise<boolean>;
|
|
23
|
+
ensureRunning(opts?: {
|
|
24
|
+
allowSpawn?: boolean;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
private spawnDaemon;
|
|
28
|
+
private portFromUrl;
|
|
29
|
+
}
|
|
30
|
+
declare function makeClient(settings: DaemonSettings): DaemonClient;
|
|
31
|
+
|
|
32
|
+
type Formatter<T = unknown> = (obj: T) => string;
|
|
33
|
+
declare function emit<T = unknown>(obj: T, human?: Formatter<T>): void;
|
|
34
|
+
declare function fail(message: string, exitCode?: number): never;
|
|
35
|
+
declare function handleResponse(r: Response): Promise<unknown>;
|
|
36
|
+
|
|
37
|
+
export { DEFAULT_DAEMON_URL, DaemonClient, type DaemonSettings, emit, fail, handleResponse, loadDaemonSettings, makeClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DAEMON_URL,
|
|
3
|
+
DaemonClient,
|
|
4
|
+
emit,
|
|
5
|
+
fail,
|
|
6
|
+
handleResponse,
|
|
7
|
+
loadDaemonSettings,
|
|
8
|
+
makeClient
|
|
9
|
+
} from "./chunk-42V4PGGP.js";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_DAEMON_URL,
|
|
12
|
+
DaemonClient,
|
|
13
|
+
emit,
|
|
14
|
+
fail,
|
|
15
|
+
handleResponse,
|
|
16
|
+
loadDaemonSettings,
|
|
17
|
+
makeClient
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@struktoai/mirage-cli",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "A Unified Virtual Filesystem For AI Agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bash",
|
|
7
|
+
"fuse",
|
|
8
|
+
"typescript",
|
|
9
|
+
"vfs",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"agent-sandbox",
|
|
12
|
+
"langchain",
|
|
13
|
+
"virtual-filesystem",
|
|
14
|
+
"llm-agents",
|
|
15
|
+
"agent-tools",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"openai-agents"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/strukto-ai/mirage#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/strukto-ai/mirage/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/strukto-ai/mirage.git",
|
|
26
|
+
"directory": "typescript/packages/cli"
|
|
27
|
+
},
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"author": "Zecheng Zhang <zecheng@strukto.ai>",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"bin": {
|
|
40
|
+
"mirage": "./dist/bin/mirage.js"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^14.0.0",
|
|
44
|
+
"yaml": "^2.6.0",
|
|
45
|
+
"@struktoai/mirage-server": "0.0.1-alpha.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.0.0",
|
|
49
|
+
"tsup": "^8.5.0",
|
|
50
|
+
"typescript": "^6.0.0",
|
|
51
|
+
"vitest": "^3.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|