omkit 0.2.2 → 0.4.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 +66 -43
- package/dist/actions/browser.d.ts +28 -0
- package/dist/actions/browser.js +36 -0
- package/dist/actions/command.d.ts +11 -7
- package/dist/actions/command.js +16 -10
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/prompt.js +43 -17
- package/dist/cli/client/channel.d.ts +14 -0
- package/dist/cli/client/channel.js +48 -0
- package/dist/cli/client/check.d.ts +3 -0
- package/dist/cli/client/check.js +32 -0
- package/dist/cli/client/discovery.d.ts +8 -0
- package/dist/cli/client/discovery.js +163 -0
- package/dist/cli/client/index.d.ts +9 -0
- package/dist/cli/client/index.js +11 -0
- package/dist/cli/client/registry.d.ts +29 -0
- package/dist/cli/client/registry.js +1 -0
- package/dist/cli/client/runner.d.ts +15 -0
- package/dist/cli/client/runner.js +61 -0
- package/dist/cli/client/types.d.ts +53 -0
- package/dist/cli/client/types.js +1 -0
- package/dist/cli/commands/check.d.ts +6 -0
- package/dist/cli/commands/check.js +11 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +54 -0
- package/dist/cli/commands/ls.d.ts +5 -0
- package/dist/cli/commands/ls.js +25 -0
- package/dist/cli/commands/run.d.ts +10 -0
- package/dist/cli/commands/run.js +12 -0
- package/dist/cli/commands/ui.d.ts +11 -0
- package/dist/cli/commands/ui.js +37 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +110 -0
- package/dist/cli/ui/Spinner.d.ts +10 -0
- package/dist/cli/ui/Spinner.js +20 -0
- package/dist/cli/ui/app.d.ts +12 -0
- package/dist/cli/ui/app.js +95 -0
- package/dist/cli/ui/views/OmList.d.ts +9 -0
- package/dist/cli/ui/views/OmList.js +36 -0
- package/dist/cli/ui/views/RunView.d.ts +14 -0
- package/dist/cli/ui/views/RunView.js +47 -0
- package/dist/core/ActionRun.d.ts +2 -4
- package/dist/core/ActionRun.js +32 -36
- package/dist/core/ExecutionTree.d.ts +1 -0
- package/dist/core/ExecutionTree.js +25 -4
- package/dist/core/interaction.d.ts +63 -0
- package/dist/core/interaction.js +82 -0
- package/dist/core/step.js +2 -7
- package/dist/core/types.d.ts +10 -20
- package/dist/index.d.ts +1 -1
- package/dist/output/LiveRenderer.d.ts +7 -17
- package/dist/output/LiveRenderer.js +8 -82
- package/dist/output/milestones.d.ts +8 -0
- package/dist/output/milestones.js +39 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# omkit
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/omkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
3
6
|
**The operational-model kit.** _Humans orchestrate. Runs narrate. AI assistants follow along._
|
|
4
7
|
|
|
5
8
|
A tiny runtime for the workflows _around_ your code — start servers, wait for health checks, build, watch, drive a browser, read state back, and tear it all down together. You write the orchestration as ordinary TypeScript; the run **narrates itself** into a structured, on-disk record — what launched, what came up, what attached, what failed, what the world actually looked like — that an AI assistant can read instead of guessing from terminal scrollback. Where [tskb](https://www.npmjs.com/package/tskb) is the _knowledge_ layer (what your system **is**), `omkit` is the _operational_ one (what it's **doing right now**).
|
|
@@ -14,22 +17,23 @@ Now hand that same script to an AI assistant. It has it even worse: interleaved
|
|
|
14
17
|
|
|
15
18
|
```ts
|
|
16
19
|
import { om } from "omkit";
|
|
17
|
-
import { command, healthcheck, chromePage } from "omkit/actions";
|
|
18
|
-
|
|
19
|
-
// Define once. `command` names the action after the command; calling it launches.
|
|
20
|
-
const api = command("npm run dev", { cwd: "api" });
|
|
21
|
-
const web = command("npm run dev", { cwd: "web" });
|
|
20
|
+
import { command, healthcheck, browser, chromePage } from "omkit/actions";
|
|
22
21
|
|
|
23
22
|
om("dev", async () => {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// `command` names the action after the command and launches it — a normal action call.
|
|
24
|
+
command("npm run dev", { cwd: "api" }).tag("api"); // start both dev servers as daemons…
|
|
25
|
+
command("npm run dev", { cwd: "web" }).tag("web");
|
|
26
26
|
|
|
27
27
|
// …wait until the app actually serves (not just "process started")…
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
try {
|
|
29
|
+
await healthcheck({ url: "http://localhost:3000" }).result;
|
|
30
|
+
} catch {
|
|
31
|
+
return; // never came up — the servers stay up so you can debug them
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// …then launch a real browser and hand its live page downstream — typed, not scraped.
|
|
35
|
+
const chrome = browser({ headless: false }); // a visible Chromium, closed on teardown
|
|
36
|
+
const page = chromePage("app", chrome.ref, { url: "http://localhost:3000" });
|
|
33
37
|
console.log(`opened ${(await page.ref).url()}`);
|
|
34
38
|
|
|
35
39
|
console.log("stack is up — press Ctrl+C to stop");
|
|
@@ -41,12 +45,13 @@ om("dev", async () => {
|
|
|
41
45
|
```
|
|
42
46
|
om()
|
|
43
47
|
│
|
|
44
|
-
|
|
45
|
-
▼
|
|
46
|
-
command
|
|
47
|
-
api · web
|
|
48
|
-
(daemons)
|
|
49
|
-
|
|
48
|
+
┌───────────┬────┴──────┬──────────────────┐
|
|
49
|
+
▼ ▼ ▼ ▼
|
|
50
|
+
command healthcheck browser ──ref──▶ chromePage
|
|
51
|
+
api · web (gate) (launch) .ref → live Page
|
|
52
|
+
(daemons) │
|
|
53
|
+
▼
|
|
54
|
+
your smoke test
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
## Who it's for
|
|
@@ -69,11 +74,11 @@ const build = action("build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
|
69
74
|
const activity = build(); // calling launches; returns the live Activity
|
|
70
75
|
```
|
|
71
76
|
|
|
72
|
-
`om(name, async (ctx) => …)` hosts the orchestration as the root of a run. The name plus the file it's defined in identify the run — logs land in `logs/<name>-<hash8>/…`, so same-named oms in different files never share a folder. You write ordinary `await` / `if` / loops / variables; the Activities you launch keep running in parallel, and because the body stays in-flight while you `await`, the run never idles shut between steps. Config chained on an Activity
|
|
77
|
+
`om(name, async (ctx) => …)` hosts the orchestration as the root of a run. The name plus the file it's defined in identify the run — logs land in `logs/<name>-<hash8>/…`, so same-named oms in different files never share a folder. You write ordinary `await` / `if` / loops / variables; the Activities you launch keep running in parallel, and because the body stays in-flight while you `await`, the run never idles shut between steps. Config chained synchronously on an Activity right after launching it (like `withCache`) applies before its body runs — the body commits one microtask later, so chain it in the same tick, before you `await`. For a one-off inline step, `step(name, fn)` runs `fn` as its own node without a reusable definition.
|
|
73
78
|
|
|
74
79
|
### Typed capabilities
|
|
75
80
|
|
|
76
|
-
An action can `attach` a value — a port, a client, a page — that downstream actions receive by awaiting `activity.ref`. Not a file descriptor scraped from a log: a typed runtime handoff, so steps chain without globals or string-parsing.
|
|
81
|
+
An action can `attach` a value — a port, a client, a page — that downstream actions receive by awaiting `activity.ref`. Not a file descriptor scraped from a log: a typed runtime handoff, so steps chain without globals or string-parsing. The first `attach` wins; later ones are ignored. Unlike `.result`, `.ref` **rejects** on failure or cancellation — a consumer waiting on a capability that never arrives fails instead of hanging — and an action that publishes nothing still resolves its `.ref` with `undefined` on success, so awaiting it never wedges.
|
|
77
82
|
|
|
78
83
|
```ts
|
|
79
84
|
const server = action("server")
|
|
@@ -92,32 +97,34 @@ om("migrate", async () => {
|
|
|
92
97
|
});
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
###
|
|
100
|
+
### Results — one promise that rejects on failure
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
An Activity's **`.result`** resolves the value on success and **rejects** on failure — the same reject-on-failure shape as `.ref` and `.once`. A failure you want to branch on is a `try/catch`; one you want to note without stopping is a `.catch`. Reading `.result` also _observes_ the Activity (see teardown), so a handled failure doesn't tear the run down:
|
|
98
103
|
|
|
99
104
|
```ts
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// red tests are an expected outcome here — decide what to do, don't crash
|
|
105
|
+
try {
|
|
106
|
+
await command("npm test").result; // resolves on green, throws on red
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// red tests are an expected outcome here — decide what to do
|
|
105
109
|
}
|
|
110
|
+
|
|
111
|
+
// …or handle it inline, without stopping:
|
|
112
|
+
await command("npm test").result.catch((err) => report(err));
|
|
106
113
|
```
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
A cancelled Activity rejects with `CancelledError` — `isCancelled(err)` tells an intentional stop apart from a real fault.
|
|
109
116
|
|
|
110
117
|
### Failure & teardown
|
|
111
118
|
|
|
112
|
-
omkit uses **structured supervision**: a failure that **nobody is watching** tears the whole run down and marks it failed. An Activity is "watched" if — before it fails — you awaited its `.result
|
|
119
|
+
omkit uses **structured supervision**: a failure that **nobody is watching** tears the whole run down and marks it failed. An Activity is "watched" if — before it fails — you awaited its `.result`/`.ref`/`.once`, attached a `.result.catch`, or added an `on("error")` listener.
|
|
113
120
|
|
|
114
|
-
- **Await it** (`await task().result`) →
|
|
121
|
+
- **Await it** (`await task().result`) → a failure throws at the `await`, in your body; wrap it in `try/catch` to branch. The run stays green because you observed it.
|
|
115
122
|
- **Fire-and-forget it** (`task()`, never awaited) → an unobserved crash tears the run down. This is the guardrail for daemons: a dev server that dies fails the run instead of leaving it wedged.
|
|
116
|
-
- **`.
|
|
123
|
+
- **`.result.catch(fn)`** → own a fire-and-forget Activity's failure: reading `.result` observes it, and `fn(error)` runs instead of tearing the run down (it also fires on cancel — guard with `isCancelled` if that matters).
|
|
117
124
|
|
|
118
125
|
```ts
|
|
119
|
-
web
|
|
120
|
-
flakyBackgroundJob().
|
|
126
|
+
command("npm run dev", { cwd: "web" }).tag("web"); // if it crashes, the run tears down — you'll know
|
|
127
|
+
flakyBackgroundJob().result.catch((e) => console.warn("job failed, carrying on", e));
|
|
121
128
|
```
|
|
122
129
|
|
|
123
130
|
**Ctrl+C** and **`ctx.cancel()`** tear everything down gracefully — procs are killed, waits unblock, the log is always written. And **success is keep-alive**: when your `om` body returns, the daemons it started keep running until Ctrl+C or `cancel()`, so wiring things up doesn't kill the servers you just started.
|
|
@@ -148,7 +155,7 @@ const build = action("build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
|
148
155
|
|
|
149
156
|
om("build", async () => {
|
|
150
157
|
const out = await build().withCache(`${process.cwd()}/src`).result;
|
|
151
|
-
if (out
|
|
158
|
+
if (out === undefined) console.log("no changes — skipped the build");
|
|
152
159
|
});
|
|
153
160
|
```
|
|
154
161
|
|
|
@@ -164,13 +171,16 @@ om("watch", async () => {
|
|
|
164
171
|
});
|
|
165
172
|
```
|
|
166
173
|
|
|
167
|
-
**Gate a deploy on green tests.**
|
|
174
|
+
**Gate a deploy on green tests.** A red test rejects at the `await`; catch it to branch:
|
|
168
175
|
|
|
169
176
|
```ts
|
|
170
177
|
om("ship", async () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
178
|
+
try {
|
|
179
|
+
await command("npm test").result; // resolves on green, throws on red
|
|
180
|
+
} catch {
|
|
181
|
+
return; // red → stop; the run stays green because you observed the failure
|
|
182
|
+
}
|
|
183
|
+
await command("./deploy.sh").result;
|
|
174
184
|
});
|
|
175
185
|
```
|
|
176
186
|
|
|
@@ -187,7 +197,7 @@ om("deploy", async () => {
|
|
|
187
197
|
default: "no",
|
|
188
198
|
timeoutMs: 10_000, // no answer in 10s → "no"
|
|
189
199
|
}).result;
|
|
190
|
-
if (answer
|
|
200
|
+
if (answer === "yes") await command("./deploy.sh").result;
|
|
191
201
|
});
|
|
192
202
|
```
|
|
193
203
|
|
|
@@ -196,8 +206,8 @@ om("deploy", async () => {
|
|
|
196
206
|
An Activity exposes its declared events plus the lifecycle ones (`done`, `error`, `attached`):
|
|
197
207
|
|
|
198
208
|
- **`activity.on(event, handler)`** — subscribe to every emit.
|
|
199
|
-
- **`activity.once(event)`** — a **promise** for the next emit
|
|
200
|
-
- **`activity.once("done")`** is the
|
|
209
|
+
- **`activity.once(event)`** — a **promise** for the next emit. Resolves the payload when it fires; if the Activity settles first it resolves `undefined` on success and **rejects** on failure/cancel — so a gate on an event surfaces a failure instead of hanging. A retained snapshot (like `healthy`) is replayed immediately even if you subscribe late.
|
|
210
|
+
- **`activity.once("done")`** is the outcome itself — identical to `.result` (resolve the value, reject on failure). `await step().once("done")` and `await step().result` are the same hard gate.
|
|
201
211
|
|
|
202
212
|
```ts
|
|
203
213
|
const probe = healthcheck({ url: "http://localhost:3000" });
|
|
@@ -208,13 +218,26 @@ probe.on("healthy", (r) => console.log(`up after ${r.attempts} tries`));
|
|
|
208
218
|
|
|
209
219
|
Reusable actions built on the core engine. For anything beyond these, drop down to `action(...).run(...)`.
|
|
210
220
|
|
|
211
|
-
- **`command`** —
|
|
221
|
+
- **`command`** — run a shell command (or, with `args`, an executable) as an action named after it; output streams to the log, killed on teardown.
|
|
212
222
|
- **`watch`** / **`watchDir`** — watch a file or directory for changes (survives wipe + recreate).
|
|
213
223
|
- **`untilLog`** — gate on the first log entry matching a predicate.
|
|
214
224
|
- **`tailLog`** — tail a file another process writes, folding its lines into the combined log.
|
|
215
225
|
- **`healthcheck`** — poll a URL/port until the status (and optionally body) matches.
|
|
216
226
|
- **`prompt`** — ask the terminal for input or a choice, with a timeout that falls back to a default; the answer is its handle.
|
|
217
|
-
- **`
|
|
227
|
+
- **`browser`** — launch a Chromium browser with Playwright and expose the live `Browser` as a handle (defaults to the installed Chrome); closed on teardown. Feed its `.ref` to `chromePage`.
|
|
228
|
+
- **`chromePage`** — attach to Chrome over CDP, or to an existing Playwright `Page`/`Browser`/`Context` (including a `browser` handle or an Electron window), and expose the live `Page` as a handle.
|
|
229
|
+
|
|
230
|
+
## CLI
|
|
231
|
+
|
|
232
|
+
Point the `omkit` bin at a project — a `tsconfig.omkit.json` that lists your `oms/` and `actions/` — and it discovers, typechecks, and runs them. (Override the config path with `--tsconfig`.)
|
|
233
|
+
|
|
234
|
+
- **`omkit init`** — scaffold a starter project: `tsconfig.omkit.json`, a sample `oms/dev.ts`, and `actions/hello.ts`.
|
|
235
|
+
- **`omkit run`** — with no argument, open the interactive picker: browse and search your oms, run one, and watch its milestones stream live — answering any `prompt` in-console. A bare `omkit` does the same (`run` is the default command).
|
|
236
|
+
- **`omkit run <om>`** — run one om directly by name or file path, inheriting the terminal.
|
|
237
|
+
- **`omkit ls`** — list the discovered oms and actions.
|
|
238
|
+
- **`omkit check`** — typecheck the project (`tsc --noEmit`) and report diagnostics.
|
|
239
|
+
|
|
240
|
+
Each run is supervised in its own child process and narrated to a run folder (see above), whether you launch it from the picker or with `run <om>`.
|
|
218
241
|
|
|
219
242
|
## Install
|
|
220
243
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Browser } from "playwright-core";
|
|
2
|
+
export type { Browser } from "playwright-core";
|
|
3
|
+
export interface BrowserOptions {
|
|
4
|
+
/** Run without a visible window. Default false — the browser is visible. */
|
|
5
|
+
headless?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Which Chromium to launch. Defaults to the installed Google Chrome (`"chrome"`):
|
|
8
|
+
* playwright-core ships no bundled browser, so a channel (or {@link executablePath})
|
|
9
|
+
* is what lets `launch` succeed without a separate `playwright install`. Pass an
|
|
10
|
+
* empty string to use a bundled Chromium instead.
|
|
11
|
+
*/
|
|
12
|
+
channel?: string;
|
|
13
|
+
/** Absolute path to a browser executable, overriding {@link channel}. */
|
|
14
|
+
executablePath?: string;
|
|
15
|
+
/** Extra flags for the browser process (e.g. `"--start-maximized"`). */
|
|
16
|
+
args?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Launches a Chromium browser with Playwright and publishes the live {@link Browser}
|
|
20
|
+
* as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
|
|
21
|
+
* { url })` — can await `instance.ref` and open pages in it. This owns the browser it
|
|
22
|
+
* launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
|
|
23
|
+
* so teardown closes it. Replaces the external chromedriver/WebDriver dance.
|
|
24
|
+
*
|
|
25
|
+
* Daemon-shaped: launch + attach, stay alive until the run's `signal` aborts, then close
|
|
26
|
+
* the browser (bounded by a short race, so a wedged `close()` can't stall teardown).
|
|
27
|
+
*/
|
|
28
|
+
export declare const browser: import("../index.ts").Action<[opts?: BrowserOptions | undefined], void, import("../core/types.ts").NoEvents, Browser>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { chromium } from "playwright-core";
|
|
2
|
+
import { action } from "../core/action.js";
|
|
3
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
/**
|
|
5
|
+
* Launches a Chromium browser with Playwright and publishes the live {@link Browser}
|
|
6
|
+
* as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
|
|
7
|
+
* { url })` — can await `instance.ref` and open pages in it. This owns the browser it
|
|
8
|
+
* launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
|
|
9
|
+
* so teardown closes it. Replaces the external chromedriver/WebDriver dance.
|
|
10
|
+
*
|
|
11
|
+
* Daemon-shaped: launch + attach, stay alive until the run's `signal` aborts, then close
|
|
12
|
+
* the browser (bounded by a short race, so a wedged `close()` can't stall teardown).
|
|
13
|
+
*/
|
|
14
|
+
export const browser = action("browser")
|
|
15
|
+
.ref()
|
|
16
|
+
.run(async ({ signal, attach }, opts = {}) => {
|
|
17
|
+
const { headless = false, channel = "chrome", executablePath, args } = opts;
|
|
18
|
+
const launchOpts = { headless, args };
|
|
19
|
+
if (executablePath)
|
|
20
|
+
launchOpts.executablePath = executablePath;
|
|
21
|
+
else if (channel)
|
|
22
|
+
launchOpts.channel = channel;
|
|
23
|
+
const launched = await chromium.launch(launchOpts);
|
|
24
|
+
console.log(`launched ${executablePath || channel || "chromium"}${headless ? " (headless)" : ""}`);
|
|
25
|
+
attach(launched); // resolves instance.ref → the live Browser
|
|
26
|
+
// Daemon: hold the browser open until teardown, then close it (it's ours to close).
|
|
27
|
+
return new Promise((resolveRun) => {
|
|
28
|
+
const stop = () => {
|
|
29
|
+
void Promise.race([launched.close(), delay(2000)]).finally(resolveRun);
|
|
30
|
+
};
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
stop();
|
|
33
|
+
else
|
|
34
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -13,12 +13,16 @@ export interface CommandOptions {
|
|
|
13
13
|
inheritDebugger?: boolean;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
* The action name a command runs under: the shell string itself, or `file arg1 arg2`
|
|
17
|
+
* for the direct-exec form. Exposed so the naming rule stays independently checkable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function commandName(cmdOrFile: string, options?: CommandOptions): string;
|
|
20
|
+
/**
|
|
21
|
+
* Launches a command as an action, named after the command itself (add a semantic label
|
|
22
|
+
* with `.tag(...)`), and returns the live instance — a normal action call, no extra `()`.
|
|
23
|
+
* Must run inside an `om(...)` body, like any other action.
|
|
20
24
|
*
|
|
21
|
-
* If `options.args` is provided,
|
|
22
|
-
* launched directly without a shell.
|
|
25
|
+
* By default the command is executed through the shell. If `options.args` is provided,
|
|
26
|
+
* `cmdOrFile` is treated as an executable and launched directly without a shell.
|
|
23
27
|
*/
|
|
24
|
-
export declare function command(cmdOrFile: string, options?: CommandOptions): import("../index.ts").
|
|
28
|
+
export declare function command(cmdOrFile: string, options?: CommandOptions): import("../index.ts").Activity<import("zx").ProcessOutput, import("../core/types.ts").NoEvents, void> | import("../index.ts").Activity<void, import("../core/types.ts").NoEvents, void>;
|
package/dist/actions/command.js
CHANGED
|
@@ -7,29 +7,35 @@ import { debuggerFreeEnv, killTree, streamLines } from "../system/proc.js";
|
|
|
7
7
|
*/
|
|
8
8
|
const verbatim = (cmd) => Object.assign([cmd], { raw: [cmd] });
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
* The action name a command runs under: the shell string itself, or `file arg1 arg2`
|
|
11
|
+
* for the direct-exec form. Exposed so the naming rule stays independently checkable.
|
|
12
|
+
*/
|
|
13
|
+
export function commandName(cmdOrFile, options = {}) {
|
|
14
|
+
return options.args === undefined ? cmdOrFile : [cmdOrFile, ...options.args].join(" ");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Launches a command as an action, named after the command itself (add a semantic label
|
|
18
|
+
* with `.tag(...)`), and returns the live instance — a normal action call, no extra `()`.
|
|
19
|
+
* Must run inside an `om(...)` body, like any other action.
|
|
14
20
|
*
|
|
15
|
-
* If `options.args` is provided,
|
|
16
|
-
* launched directly without a shell.
|
|
21
|
+
* By default the command is executed through the shell. If `options.args` is provided,
|
|
22
|
+
* `cmdOrFile` is treated as an executable and launched directly without a shell.
|
|
17
23
|
*/
|
|
18
24
|
export function command(cmdOrFile, options = {}) {
|
|
19
25
|
const cwd = path.resolve(options.cwd ?? ".");
|
|
26
|
+
const name = commandName(cmdOrFile, options);
|
|
20
27
|
// Shell command — the command string is the action (and proc) name.
|
|
21
28
|
if (options.args === undefined) {
|
|
22
|
-
return action(
|
|
29
|
+
return action(name).run((ctx) => ctx.proc(cmdOrFile, {
|
|
23
30
|
cwd,
|
|
24
31
|
...(options.env && { env: options.env }),
|
|
25
32
|
...(options.inheritDebugger && {
|
|
26
33
|
inheritDebugger: true,
|
|
27
34
|
}),
|
|
28
|
-
})(verbatim(cmdOrFile)));
|
|
35
|
+
})(verbatim(cmdOrFile)))();
|
|
29
36
|
}
|
|
30
37
|
// Direct executable — the file plus its args form the action name.
|
|
31
38
|
const args = options.args;
|
|
32
|
-
const name = [cmdOrFile, ...args].join(" ");
|
|
33
39
|
return action(name).run(async (ctx) => {
|
|
34
40
|
const inherit = options.inheritDebugger;
|
|
35
41
|
const env = inherit
|
|
@@ -77,5 +83,5 @@ export function command(cmdOrFile, options = {}) {
|
|
|
77
83
|
finished = true;
|
|
78
84
|
ctx.signal.removeEventListener("abort", onAbort);
|
|
79
85
|
}
|
|
80
|
-
});
|
|
86
|
+
})();
|
|
81
87
|
}
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -18,3 +18,5 @@ export { prompt } from "./prompt.ts";
|
|
|
18
18
|
export type { PromptOptions, InputPromptOptions, ChoicePromptOptions, PromptChoice, PromptVia, PromptEvents, } from "./prompt.ts";
|
|
19
19
|
export { chromePage } from "./chrome-page.ts";
|
|
20
20
|
export type { ChromePageOptions, ChromePageEvents, ChromePageSource, Page, Browser, BrowserContext, } from "./chrome-page.ts";
|
|
21
|
+
export { browser } from "./browser.ts";
|
|
22
|
+
export type { BrowserOptions } from "./browser.ts";
|
package/dist/actions/index.js
CHANGED
package/dist/actions/prompt.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { action } from "../core/action.js";
|
|
3
|
+
import { activeSupervisor } from "../core/interaction.js";
|
|
3
4
|
const asChoices = (choices) => choices.map((c) => (typeof c === "string" ? { label: c, value: c } : c));
|
|
5
|
+
/** Route the request over the supervisor channel, resolving the raw answer against the choices. */
|
|
6
|
+
async function askSupervisor(supervisor, ctx, waitSignal) {
|
|
7
|
+
const { opts, message, defaultValue, resolveRaw } = ctx;
|
|
8
|
+
const spec = {
|
|
9
|
+
kind: opts.kind === "choice" ? "choice" : "input",
|
|
10
|
+
message,
|
|
11
|
+
default: defaultValue,
|
|
12
|
+
...(opts.kind === "choice" ? { choices: asChoices(opts.choices) } : {}),
|
|
13
|
+
};
|
|
14
|
+
const answer = await supervisor.request(spec, waitSignal);
|
|
15
|
+
const resolved = resolveRaw(answer.value);
|
|
16
|
+
if (resolved === undefined)
|
|
17
|
+
return { value: defaultValue, via: "default" };
|
|
18
|
+
return { value: resolved, via: answer.via ?? "input" };
|
|
19
|
+
}
|
|
20
|
+
/** Read the answer straight from the terminal (the bare, unsupervised path). */
|
|
21
|
+
async function askTerminal(query, resolveRaw, defaultValue, waitSignal) {
|
|
22
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
try {
|
|
24
|
+
const raw = (await rl.question(query, { signal: waitSignal })).trim();
|
|
25
|
+
if (raw === "")
|
|
26
|
+
return { value: defaultValue, via: "default" };
|
|
27
|
+
const resolved = resolveRaw(raw);
|
|
28
|
+
if (resolved !== undefined)
|
|
29
|
+
return { value: resolved, via: "input" };
|
|
30
|
+
console.log(`invalid answer "${raw}", using default`);
|
|
31
|
+
return { value: defaultValue, via: "default" };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* Asks the terminal a question — free-text **input** or a **choice** — and resolves
|
|
6
39
|
* with the answer, which it also attaches as the handle (`prompt(...).ref` feeds
|
|
@@ -47,25 +80,19 @@ export const prompt = action("prompt")
|
|
|
47
80
|
emit("prompt", message);
|
|
48
81
|
const onTimeout = new AbortController();
|
|
49
82
|
const timer = finite ? setTimeout(() => onTimeout.abort(), timeoutMs) : undefined;
|
|
50
|
-
const
|
|
83
|
+
const waitSignal = AbortSignal.any([signal, onTimeout.signal]);
|
|
51
84
|
let value = defaultValue;
|
|
52
85
|
let via = "default";
|
|
86
|
+
const supervisor = activeSupervisor();
|
|
53
87
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
via = "input";
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
console.log(`invalid answer "${raw}", using default`);
|
|
66
|
-
via = "default";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
88
|
+
// Supervised: hand off to whoever owns the terminal (Ink app / MCP client). Bare: read
|
|
89
|
+
// the terminal directly. Either way the child keeps the timeout via `waitSignal`, so a
|
|
90
|
+
// silent supervisor (or nobody) can't wedge the run.
|
|
91
|
+
const outcome = supervisor
|
|
92
|
+
? await askSupervisor(supervisor, { opts, message, defaultValue, resolveRaw }, waitSignal)
|
|
93
|
+
: await askTerminal(query, resolveRaw, defaultValue, waitSignal);
|
|
94
|
+
value = outcome.value;
|
|
95
|
+
via = outcome.via;
|
|
69
96
|
}
|
|
70
97
|
catch {
|
|
71
98
|
via = onTimeout.signal.aborted ? "timeout" : "default";
|
|
@@ -73,7 +100,6 @@ export const prompt = action("prompt")
|
|
|
73
100
|
finally {
|
|
74
101
|
if (timer)
|
|
75
102
|
clearTimeout(timer);
|
|
76
|
-
rl.close();
|
|
77
103
|
}
|
|
78
104
|
console.log(via === "timeout" ? `timed out → ${value}` : `${value} (${via})`);
|
|
79
105
|
attach(value); // resolves instance.ref → the answer
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChildMessage, SupervisorMessage } from "../../core/interaction.ts";
|
|
2
|
+
import type { RunSession } from "./types.ts";
|
|
3
|
+
/** The minimal duplex the channel needs — satisfied by a real ChildProcess or a fake. */
|
|
4
|
+
export interface Transport {
|
|
5
|
+
send(message: SupervisorMessage): void;
|
|
6
|
+
onMessage(handler: (message: ChildMessage) => void): void;
|
|
7
|
+
onClose(handler: (code: number | null) => void): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child messages out to
|
|
11
|
+
* handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down the wire. A
|
|
12
|
+
* close before `settled` resolves a failed verdict so `result` never hangs.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createChannel(transport: Transport): RunSession;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child messages out to
|
|
3
|
+
* handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down the wire. A
|
|
4
|
+
* close before `settled` resolves a failed verdict so `result` never hangs.
|
|
5
|
+
*/
|
|
6
|
+
export function createChannel(transport) {
|
|
7
|
+
const handlers = {
|
|
8
|
+
log: [],
|
|
9
|
+
prompt: [],
|
|
10
|
+
settled: [],
|
|
11
|
+
};
|
|
12
|
+
let settled = false;
|
|
13
|
+
let resolveResult;
|
|
14
|
+
const result = new Promise((resolve) => (resolveResult = resolve));
|
|
15
|
+
transport.onMessage((message) => {
|
|
16
|
+
if (message.kind === "log") {
|
|
17
|
+
for (const h of handlers.log)
|
|
18
|
+
h(message.entry);
|
|
19
|
+
}
|
|
20
|
+
else if (message.kind === "prompt") {
|
|
21
|
+
for (const h of handlers.prompt)
|
|
22
|
+
h({ id: message.id, spec: message.spec });
|
|
23
|
+
}
|
|
24
|
+
else if (message.kind === "settled") {
|
|
25
|
+
settled = true;
|
|
26
|
+
const verdict = { ok: message.ok, folder: message.folder, summary: message.summary };
|
|
27
|
+
for (const h of handlers.settled)
|
|
28
|
+
h(verdict);
|
|
29
|
+
resolveResult(verdict);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
transport.onClose(() => {
|
|
33
|
+
if (!settled)
|
|
34
|
+
resolveResult({ ok: false, folder: "", summary: [] });
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
on(event, handler) {
|
|
38
|
+
handlers[event].push(handler);
|
|
39
|
+
},
|
|
40
|
+
answer(id, value, via = "input") {
|
|
41
|
+
transport.send({ kind: "answer", id, value, via });
|
|
42
|
+
},
|
|
43
|
+
cancel() {
|
|
44
|
+
transport.send({ kind: "cancel" });
|
|
45
|
+
},
|
|
46
|
+
result,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Typecheck the project (`tsc --noEmit`) and return per-file diagnostics. */
|
|
4
|
+
export function runCheck(tsconfigPath) {
|
|
5
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
6
|
+
if (configFile.error) {
|
|
7
|
+
return [{ file: tsconfigPath, line: 0, message: msg(configFile.error) }];
|
|
8
|
+
}
|
|
9
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.resolve(path.dirname(tsconfigPath)));
|
|
10
|
+
const program = ts.createProgram({
|
|
11
|
+
rootNames: parsed.fileNames,
|
|
12
|
+
options: { ...parsed.options, noEmit: true },
|
|
13
|
+
});
|
|
14
|
+
const canonical = (f) => ts.sys.useCaseSensitiveFileNames ? path.normalize(f) : path.normalize(f).toLowerCase();
|
|
15
|
+
const fileSet = new Set(parsed.fileNames.map((f) => canonical(f)));
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const sf of program.getSourceFiles()) {
|
|
18
|
+
if (!fileSet.has(canonical(sf.fileName)))
|
|
19
|
+
continue;
|
|
20
|
+
for (const d of [
|
|
21
|
+
...program.getSyntacticDiagnostics(sf),
|
|
22
|
+
...program.getSemanticDiagnostics(sf),
|
|
23
|
+
]) {
|
|
24
|
+
const line = d.start !== undefined ? sf.getLineAndCharacterOfPosition(d.start).line + 1 : 0;
|
|
25
|
+
out.push({ file: sf.fileName, line, message: msg(d) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function msg(d) {
|
|
31
|
+
return ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Registry } from "./registry.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
|
|
4
|
+
* actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
|
|
5
|
+
* project stays useful), but a **fatal config problem** — a missing, unparseable, or invalid
|
|
6
|
+
* tsconfig — throws, so the CLI crashes visibly instead of silently returning an empty project.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discover(tsconfigPath: string): Registry;
|