omkit 0.0.14 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -97
- package/dist/actions/chrome-page.d.ts +12 -29
- package/dist/actions/chrome-page.js +19 -32
- package/dist/actions/command.d.ts +8 -41
- package/dist/actions/command.js +15 -82
- package/dist/actions/healthcheck.d.ts +3 -11
- package/dist/actions/healthcheck.js +8 -22
- package/dist/actions/index.d.ts +6 -8
- package/dist/actions/index.js +4 -6
- package/dist/actions/prompt.d.ts +5 -19
- package/dist/actions/prompt.js +10 -31
- package/dist/actions/tail-log.d.ts +7 -21
- package/dist/actions/tail-log.js +13 -34
- package/dist/actions/until-log.d.ts +4 -7
- package/dist/actions/until-log.js +3 -3
- package/dist/actions/watch-dir.d.ts +5 -19
- package/dist/actions/watch-dir.js +13 -28
- package/dist/actions/watch.d.ts +4 -12
- package/dist/actions/watch.js +9 -16
- package/dist/core/ActionRun.d.ts +97 -0
- package/dist/core/ActionRun.js +266 -0
- package/dist/core/ExecutionTree.d.ts +88 -0
- package/dist/core/ExecutionTree.js +320 -0
- package/dist/core/action.d.ts +6 -0
- package/dist/core/action.js +72 -0
- package/dist/core/context.d.ts +9 -0
- package/dist/core/context.js +8 -0
- package/dist/core/om.d.ts +9 -0
- package/dist/core/om.js +25 -0
- package/dist/core/step.d.ts +9 -0
- package/dist/core/step.js +18 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/foundation/ActionRef.d.ts +18 -0
- package/dist/foundation/ActionRef.js +5 -0
- package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
- package/dist/{utils → foundation}/AsyncQueue.js +6 -19
- package/dist/foundation/CancelledError.d.ts +10 -0
- package/dist/foundation/CancelledError.js +13 -0
- package/dist/foundation/Deferred.d.ts +8 -0
- package/dist/foundation/Deferred.js +10 -0
- package/dist/foundation/LogEntry.d.ts +29 -0
- package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
- package/dist/{orchestration/events → foundation}/events.js +1 -7
- package/dist/{utils → foundation}/format.d.ts +3 -4
- package/dist/{utils → foundation}/format.js +12 -4
- package/dist/foundation/fsSafe.d.ts +7 -0
- package/dist/foundation/fsSafe.js +7 -0
- package/dist/foundation/ids.d.ts +9 -0
- package/dist/foundation/ids.js +10 -0
- package/dist/index.d.ts +11 -22
- package/dist/index.js +10 -30
- package/dist/output/LiveRenderer.d.ts +13 -0
- package/dist/output/LiveRenderer.js +56 -0
- package/dist/output/console/ConsoleCapture.d.ts +20 -0
- package/dist/output/console/ConsoleCapture.js +47 -0
- package/dist/output/folder/RunFolder.d.ts +11 -22
- package/dist/output/folder/RunFolder.js +18 -36
- package/dist/output/log/LogStore.d.ts +22 -0
- package/dist/output/log/LogStore.js +56 -0
- package/dist/output/log/RawStream.d.ts +11 -0
- package/dist/output/log/RawStream.js +18 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
- package/dist/output/snapshot/SnapshotStore.js +12 -52
- package/dist/output/writers/NodeLogWriter.d.ts +8 -0
- package/dist/output/writers/NodeLogWriter.js +36 -0
- package/dist/output/writers/ResultWriter.d.ts +3 -0
- package/dist/output/writers/ResultWriter.js +5 -0
- package/dist/output/writers/RollupWriter.d.ts +9 -0
- package/dist/output/writers/RollupWriter.js +29 -0
- package/dist/output/writers/entryLine.d.ts +10 -0
- package/dist/output/writers/entryLine.js +39 -0
- package/dist/output/writers/views.d.ts +38 -0
- package/dist/output/writers/views.js +6 -0
- package/dist/system/fs/FolderCache.d.ts +6 -34
- package/dist/system/fs/FolderCache.js +7 -38
- package/dist/system/proc.d.ts +51 -0
- package/dist/system/proc.js +148 -0
- package/dist/system/procRegistry.d.ts +52 -0
- package/dist/system/procRegistry.js +117 -0
- package/package.json +3 -1
- package/dist/orchestration/action/action.d.ts +0 -23
- package/dist/orchestration/action/action.js +0 -256
- package/dist/orchestration/action/types.d.ts +0 -182
- package/dist/orchestration/assert.d.ts +0 -22
- package/dist/orchestration/assert.js +0 -26
- package/dist/orchestration/spin/SpinHost.d.ts +0 -109
- package/dist/orchestration/spin/SpinHost.js +0 -337
- package/dist/orchestration/spin/spin.d.ts +0 -24
- package/dist/orchestration/spin/spin.js +0 -57
- package/dist/orchestration/spin/types.d.ts +0 -63
- package/dist/output/Output.d.ts +0 -24
- package/dist/output/Output.js +0 -27
- package/dist/output/index.d.ts +0 -11
- package/dist/output/index.js +0 -11
- package/dist/output/log/LogRenderer.d.ts +0 -73
- package/dist/output/log/LogRenderer.js +0 -133
- package/dist/output/log/LogsCollector.d.ts +0 -79
- package/dist/output/log/LogsCollector.js +0 -141
- package/dist/output/log/RunLog.d.ts +0 -47
- package/dist/output/log/RunLog.js +0 -142
- package/dist/output/log/console-capture.d.ts +0 -24
- package/dist/output/log/console-capture.js +0 -53
- package/dist/system/fs/streams.d.ts +0 -11
- package/dist/system/fs/streams.js +0 -14
- package/dist/system/process/process.d.ts +0 -35
- package/dist/system/process/process.js +0 -104
- package/dist/utils/serialize.d.ts +0 -2
- package/dist/utils/serialize.js +0 -12
- /package/dist/{orchestration/action → core}/types.js +0 -0
- /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
package/README.md
CHANGED
|
@@ -2,136 +2,147 @@
|
|
|
2
2
|
|
|
3
3
|
**The operational-model kit.** _Typed actions. One run. One log._
|
|
4
4
|
|
|
5
|
-
A runtime for the workflows
|
|
6
|
-
a browser, inspect state, tear it all down together. `omkit` is the running,
|
|
7
|
-
typed model of your system in motion. Where
|
|
8
|
-
[tskb](https://www.npmjs.com/package/tskb) is the _knowledge_ layer, `omkit` is
|
|
9
|
-
the _operational_ one.
|
|
5
|
+
A tiny runtime for the workflows _around_ your code — build, watch, spawn, probe, drive a browser, read state back, and tear it all down together. Where [tskb](https://www.npmjs.com/package/tskb) is the _knowledge_ layer, `omkit` is the _operational_ one: the running, typed model of your system in motion.
|
|
10
6
|
|
|
11
7
|
## The problem
|
|
12
8
|
|
|
13
|
-
Real developer workflows aren't linear. They start processes, wait for health
|
|
14
|
-
checks, connect a browser, read state back, watch files, react to events — and
|
|
15
|
-
shut everything down as one. Most teams glue this together with shell scripts,
|
|
16
|
-
npm scripts, and ad-hoc Node.
|
|
9
|
+
Real developer workflows aren't linear. They start processes, wait for health checks, connect a browser, read state back, watch files, react to events — and shut everything down as one. Most teams glue this together with shell scripts, npm scripts, and ad-hoc Node, and get back a pile of opaque process output.
|
|
17
10
|
|
|
18
|
-
`omkit` models the workflow instead. Each step is a typed **action** running
|
|
19
|
-
inside one runtime, and actions don't share globals — they hand each other typed
|
|
20
|
-
runtime **capabilities** (a port, a client, a live browser page) and emit typed
|
|
21
|
-
**events**. So a running system exposes structured handoffs and observations
|
|
22
|
-
rather than a pile of opaque process output — which makes the whole thing
|
|
23
|
-
observable, reproducible, and scriptable.
|
|
11
|
+
`omkit` models the workflow instead. Each step is a typed **action** running inside one runtime. Actions don't share globals — they hand each other typed runtime **capabilities** (a port, a client, a live browser page) and emit typed **events**, all onto one log. So a running system exposes structured handoffs and observations, which makes it observable, reproducible, and scriptable.
|
|
24
12
|
|
|
25
13
|
```ts
|
|
26
|
-
import {
|
|
27
|
-
import { command, healthcheck,
|
|
14
|
+
import { om } from "omkit";
|
|
15
|
+
import { command, healthcheck, chromePage } from "omkit/actions";
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await nod(healthcheck({ port: 3000 })).once("healthy"); // …gate on it…
|
|
17
|
+
om(async () => {
|
|
18
|
+
command("api", "npm run dev:api").exec().tag("api:daemon"); // start a daemon…
|
|
32
19
|
|
|
33
|
-
const
|
|
34
|
-
|
|
20
|
+
const ready = await healthcheck({ port: 3000 }).exec().result; // …gate on it…
|
|
21
|
+
if (!ready.ok) return;
|
|
22
|
+
|
|
23
|
+
// …then hand the live page downstream — typed, not scraped from a log.
|
|
24
|
+
const page = chromePage("app", "localhost:9222", { url: "http://localhost:3000" }).exec();
|
|
25
|
+
await inspect(page.ref); // whatever drives the app receives the live Page
|
|
26
|
+
|
|
27
|
+
console.log("up — press Ctrl+C to stop");
|
|
35
28
|
});
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
typed capability the next action receives and drives. Few workflow tools pass
|
|
40
|
-
anything but strings between steps.
|
|
31
|
+
`page.ref` isn't a URL scraped from stdout — it's a typed capability the next step receives and drives. Few workflow tools pass anything but strings between steps.
|
|
41
32
|
|
|
42
33
|
```
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
▼
|
|
47
|
-
command
|
|
48
|
-
|
|
49
|
-
▼
|
|
50
|
-
chromePage
|
|
51
|
-
│ .ref
|
|
34
|
+
om()
|
|
35
|
+
│
|
|
36
|
+
┌──────────────┼──────────────┐
|
|
37
|
+
▼ ▼ ▼
|
|
38
|
+
command healthcheck chromePage
|
|
39
|
+
(daemon) (gate) │ .ref
|
|
52
40
|
▼
|
|
53
|
-
|
|
41
|
+
your driver
|
|
54
42
|
```
|
|
55
43
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
imperative handle. Calling it constructs an instance (deferred — the run
|
|
60
|
-
executes it).
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
const build = action("Build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
- **Spin** — hosts an async orchestration _inside_ the runtime. You write normal
|
|
67
|
-
`await` / `if` / loops / variables, while the actions you launch keep running
|
|
68
|
-
in parallel. omkit runs your function as one in-flight action and hands it
|
|
69
|
-
`nod`; because that host stays in-flight while you `await`, the run never idles
|
|
70
|
-
shut between steps. `nod(instance)` launches an action and returns it, so you
|
|
71
|
-
can `await` its `.once(...)` or its `.ref`:
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
spin({ failFast: false }, async ({ nod }) => {
|
|
75
|
-
if ((await nod(prompt({ … })).once("done")) === "yes") await nod(test).once("done");
|
|
76
|
-
nod(devServer); // daemon: keeps the run alive
|
|
77
|
-
const chrome = nod(chromedriver({ … }));
|
|
78
|
-
nod(chromePage(chrome.ref));
|
|
79
|
-
});
|
|
80
|
-
```
|
|
44
|
+
## Core concepts
|
|
45
|
+
|
|
46
|
+
### Actions & Activities
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
48
|
+
An **Action** is an immutable description of work — named, typed, optionally declaring events and an imperative handle. An **Activity** is a live execution of one, created synchronously by `.exec()`. Everything _before_ `exec()` configures the Action; everything _after_ configures or observes the Activity.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { action } from "omkit";
|
|
52
|
+
|
|
53
|
+
const Build = action("Build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
54
|
+
|
|
55
|
+
const activity = Build().exec(); // launch; returns the live Activity
|
|
56
|
+
```
|
|
85
57
|
|
|
86
|
-
|
|
87
|
-
const Server = action("Server")
|
|
88
|
-
.ref<number>()
|
|
89
|
-
.run(async ({ attach, signal }) => {
|
|
90
|
-
attach(await listen()); // publish the capability…
|
|
91
|
-
await until(signal); // …and stay alive
|
|
92
|
-
});
|
|
58
|
+
`om(async (ctx) => …)` hosts a linear orchestration as the root of a run. You write normal `await` / `if` / loops / variables; the Activities you launch keep running in parallel. Because the body stays in-flight while you `await`, the run never idles shut between steps. For a one-off inline step, `step(name, fn)` runs `fn` as its own node without a reusable definition.
|
|
93
59
|
|
|
94
|
-
|
|
60
|
+
### Typed capabilities
|
|
95
61
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
62
|
+
An action can `attach` a value — a port, a client, a page — that downstream actions receive by awaiting `activity.ref`. Not a file descriptor: a typed runtime handoff, so actions chain without globals or string-scraping.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const Server = action("Server")
|
|
66
|
+
.ref<number>()
|
|
67
|
+
.run(async ({ attach, signal }) => {
|
|
68
|
+
attach(await listen()); // publish the capability…
|
|
69
|
+
await until(signal); // …and stay alive until teardown
|
|
99
70
|
});
|
|
100
|
-
```
|
|
101
71
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
72
|
+
const Probe = action("Probe").run((_ctx, port: Promise<number>) => drive(port));
|
|
73
|
+
|
|
74
|
+
om(async () => {
|
|
75
|
+
const server = Server().exec();
|
|
76
|
+
Probe(server.ref).exec(); // Probe receives Server's port, typed
|
|
77
|
+
});
|
|
78
|
+
```
|
|
106
79
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
80
|
+
### Outcomes — a result that doesn't throw
|
|
81
|
+
|
|
82
|
+
Every Activity settles to a typed **`Outcome`**, read via `activity.result` — which **never throws**:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
type Outcome<T> = { ok: true; value: T } | { ok: false; error: unknown };
|
|
86
|
+
|
|
87
|
+
const r = await runTests().exec().result;
|
|
88
|
+
if (!r.ok) {
|
|
89
|
+
// an expected, operational failure — inspect it and carry on
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Awaiting `.result` also _observes_ the Activity (see teardown, below). A cancelled Activity resolves `{ ok: false, error: CancelledError }`.
|
|
94
|
+
|
|
95
|
+
### Failure & teardown
|
|
96
|
+
|
|
97
|
+
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` (or `.ref`), attached an `on("error")` listener, or attached `.handleFailure`.
|
|
98
|
+
|
|
99
|
+
- **Await it** (`await x.exec().result`) → the failure is yours to inspect as `{ ok: false }`; the run stays green.
|
|
100
|
+
- **Fire-and-forget it** (`x.exec()`, never awaited) → an unobserved failure tears the run down. This is the guardrail for daemons: a crashed daemon fails the run instead of leaving it wedged.
|
|
101
|
+
- **`.handleFailure(fn)`** → own a fire-and-forget Activity's failure: `fn(error)` runs instead of tearing down.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
watchDir(".build").exec().tag("watch:daemon"); // crash here → run tears down
|
|
105
|
+
someFlakyDaemon()
|
|
106
|
+
.exec()
|
|
107
|
+
.handleFailure((e) => log(e)); // …unless you own it
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Ctrl+C** and **`ctx.cancel()`** tear everything down gracefully — the log is always written. And **success is keep-alive**: when your `om` body returns, daemons it launched keep running until Ctrl+C or `cancel()`, so a body that finishes wiring things up doesn't kill the servers it started.
|
|
111
|
+
|
|
112
|
+
### One log, on disk
|
|
113
|
+
|
|
114
|
+
Every action's output, events, asserts, snapshots, and lifecycle land on **one timeline**. It streams live to the terminal (curated milestones) and is written to a per-run folder:
|
|
115
|
+
|
|
116
|
+
- `result.json` — the run's tree, per-node status, and verdict.
|
|
117
|
+
- `raw.jsonl` — every entry, machine-readable.
|
|
118
|
+
- `main.log` + one `.log` per node — the human-readable timelines.
|
|
119
|
+
- `events.log` / `asserts.log` / `snapshots.log` — cross-cutting rollups.
|
|
120
|
+
|
|
121
|
+
`ctx` also gives each action `assert(...)` (tallies into the verdict) and `snapshot(name, value)` (captures JSON state to the run folder) — so a run is an inspectable artifact, not just exit codes.
|
|
112
122
|
|
|
113
123
|
## Events
|
|
114
124
|
|
|
115
|
-
An
|
|
116
|
-
`error`, `attached`):
|
|
125
|
+
An Activity exposes its declared events plus the lifecycle ones (`done`, `error`, `attached`):
|
|
117
126
|
|
|
118
|
-
- **`
|
|
119
|
-
- **`
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
- **`activity.on(event, handler)`** — subscribe to every emit.
|
|
128
|
+
- **`activity.once(event)`** — a **promise** for the next emit; resolves `undefined` if the Activity settles first, so an `await` never hangs. A retained snapshot (like `healthy`) is replayed immediately even if you subscribe late.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const probe = healthcheck({ port: 3000 }).exec();
|
|
132
|
+
probe.on("healthy", (r) => console.log(`up after ${r.attempts} tries`));
|
|
133
|
+
```
|
|
123
134
|
|
|
124
135
|
## Batteries — `omkit/actions`
|
|
125
136
|
|
|
126
|
-
|
|
137
|
+
Reusable actions built on the core engine. For anything beyond these, drop down to `action(...).run(...)`.
|
|
138
|
+
|
|
139
|
+
- **`command`** — run a shell command; output streams to the log, killed on teardown.
|
|
127
140
|
- **`watch`** / **`watchDir`** — watch a file or directory for changes (survives wipe + recreate).
|
|
128
141
|
- **`untilLog`** — gate on the first log entry matching a predicate.
|
|
129
|
-
- **`tailLog`** — tail a file another process writes,
|
|
130
|
-
- **`healthcheck`** — poll a URL/port until status (and optionally body) matches.
|
|
142
|
+
- **`tailLog`** — tail a file another process writes, folding its lines into the combined log.
|
|
143
|
+
- **`healthcheck`** — poll a URL/port until the status (and optionally body) matches.
|
|
131
144
|
- **`prompt`** — ask the terminal for input or a choice, with a timeout that falls back to a default; the answer is its handle.
|
|
132
|
-
- **`chromePage`** — attach to Chrome over CDP — or to an existing Playwright
|
|
133
|
-
`Page`/`Browser`/`Context` (including Electron windows) — and expose the live
|
|
134
|
-
`Page` as a handle.
|
|
145
|
+
- **`chromePage`** — attach to Chrome over CDP — or to an existing Playwright `Page`/`Browser`/`Context`, including Electron windows — and expose the live `Page` as a handle.
|
|
135
146
|
|
|
136
147
|
## Install
|
|
137
148
|
|
|
@@ -139,6 +150,8 @@ An instance exposes its declared events plus the lifecycle ones (`done`,
|
|
|
139
150
|
npm install omkit
|
|
140
151
|
```
|
|
141
152
|
|
|
153
|
+
Requires Node ≥ 20.11. ESM only.
|
|
154
|
+
|
|
142
155
|
## License
|
|
143
156
|
|
|
144
157
|
MIT
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
import { type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
2
2
|
export type { Page, Browser, BrowserContext } from "playwright-core";
|
|
3
3
|
/**
|
|
4
|
-
* Where {@link chromePage} gets its page
|
|
5
|
-
* - a **CDP endpoint** (`host:port` or `http://host:port`) —
|
|
6
|
-
*
|
|
7
|
-
* - an existing **`Page`**
|
|
4
|
+
* Where {@link chromePage} gets its page:
|
|
5
|
+
* - a **CDP endpoint** (`host:port` or `http://host:port`) — connected and owned
|
|
6
|
+
* (closed on teardown);
|
|
7
|
+
* - an existing **`Page`** (e.g. an Electron window) — used as-is;
|
|
8
8
|
* - a **`BrowserContext`** or **`Browser`** — a page is taken (or opened) from it.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* didn't open, leaving its lifecycle to whoever launched it.
|
|
10
|
+
* Handles aren't owned: teardown never closes a browser/window the action didn't open.
|
|
12
11
|
*/
|
|
13
12
|
export type ChromePageSource = string | Page | BrowserContext | Browser;
|
|
14
|
-
/** Events {@link chromePage}
|
|
13
|
+
/** Events {@link chromePage} emits. */
|
|
15
14
|
export interface ChromePageEvents {
|
|
16
|
-
/** A `console.*` call in the page; payload is the rendered text. */
|
|
17
15
|
console: string;
|
|
18
|
-
/** An uncaught error in the page; payload is its message. */
|
|
19
16
|
pageerror: string;
|
|
20
|
-
/** A top-level navigation settled; payload is the new URL. */
|
|
21
17
|
navigated: string;
|
|
22
18
|
}
|
|
23
19
|
export interface ChromePageOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Navigate the attached page here once acquired. Omit to drive the page as-is
|
|
26
|
-
* (the existing window, or the first page of a connected/handed-in browser).
|
|
27
|
-
*/
|
|
20
|
+
/** Navigate the attached page here once acquired. Omit to drive it as-is. */
|
|
28
21
|
url?: string;
|
|
29
22
|
}
|
|
30
23
|
/**
|
|
31
24
|
* Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
|
|
32
|
-
* actions can await `instance.ref` and drive the same live page. The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* chromedriver.ref)` or `chromePage("App", electronApp.ref)`).
|
|
38
|
-
*
|
|
39
|
-
* Daemon-shaped like the watch actions: acquire the page, attach it, then stay
|
|
40
|
-
* alive on a promise that only resolves when the run's `signal` aborts. On
|
|
41
|
-
* teardown it closes the CDP connection *only* when it opened one — a handed-in
|
|
42
|
-
* browser or window is left untouched for its owner to close.
|
|
43
|
-
*
|
|
44
|
-
* `label` names this page in the log (its `source`), so several `chromePage`
|
|
45
|
-
* instances in one run stay distinguishable — e.g. `chromePage("Explorer",
|
|
46
|
-
* chrome.ref)` logs its lines under `Explorer` rather than a shared default.
|
|
25
|
+
* actions can await `instance.ref` and drive the same live page. The source may be
|
|
26
|
+
* a promise, so it wires straight from another action's `.ref` (e.g.
|
|
27
|
+
* `chromePage("Explorer", chromedriver.ref)`). `label` tags this run so several
|
|
28
|
+
* pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
|
|
29
|
+
* until teardown, then close the CDP connection only when it opened one.
|
|
47
30
|
*/
|
|
48
31
|
export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
|
|
@@ -1,38 +1,26 @@
|
|
|
1
1
|
import { chromium } from "playwright-core";
|
|
2
|
-
import { action } from "../
|
|
3
|
-
|
|
4
|
-
// has `contexts()`, only Page has `mainFrame()`, and a BrowserContext has
|
|
5
|
-
// `newPage()` without either — enough to tell a handed-in handle apart.
|
|
2
|
+
import { action } from "../core/action.js";
|
|
3
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
4
|
const isPage = (x) => typeof x.mainFrame === "function";
|
|
7
5
|
const isBrowser = (x) => typeof x.contexts === "function";
|
|
8
6
|
/** Take the first existing page of a context, or open one. */
|
|
9
7
|
const pageOf = (context, fresh) => fresh ? context.newPage() : (context.pages()[0] ?? context.newPage());
|
|
10
8
|
/**
|
|
11
9
|
* Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
|
|
12
|
-
* actions can await `instance.ref` and drive the same live page. The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* chromedriver.ref)` or `chromePage("App", electronApp.ref)`).
|
|
18
|
-
*
|
|
19
|
-
* Daemon-shaped like the watch actions: acquire the page, attach it, then stay
|
|
20
|
-
* alive on a promise that only resolves when the run's `signal` aborts. On
|
|
21
|
-
* teardown it closes the CDP connection *only* when it opened one — a handed-in
|
|
22
|
-
* browser or window is left untouched for its owner to close.
|
|
23
|
-
*
|
|
24
|
-
* `label` names this page in the log (its `source`), so several `chromePage`
|
|
25
|
-
* instances in one run stay distinguishable — e.g. `chromePage("Explorer",
|
|
26
|
-
* chrome.ref)` logs its lines under `Explorer` rather than a shared default.
|
|
10
|
+
* actions can await `instance.ref` and drive the same live page. The source may be
|
|
11
|
+
* a promise, so it wires straight from another action's `.ref` (e.g.
|
|
12
|
+
* `chromePage("Explorer", chromedriver.ref)`). `label` tags this run so several
|
|
13
|
+
* pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
|
|
14
|
+
* until teardown, then close the CDP connection only when it opened one.
|
|
27
15
|
*/
|
|
28
|
-
export const chromePage = action("
|
|
16
|
+
export const chromePage = action("chromePage")
|
|
29
17
|
.emits()
|
|
30
18
|
.ref()
|
|
31
|
-
.run(async ({
|
|
19
|
+
.run(async ({ signal, emit, attach, tag }, label, source, opts = {}) => {
|
|
20
|
+
tag(label);
|
|
32
21
|
const { url } = opts;
|
|
33
22
|
const resolved = await source;
|
|
34
|
-
//
|
|
35
|
-
// CDP connection we open here is ours to close; handles belong to callers.
|
|
23
|
+
// Only a CDP connection we open here is ours to close; handles belong to callers.
|
|
36
24
|
let owned;
|
|
37
25
|
let page;
|
|
38
26
|
let from;
|
|
@@ -40,8 +28,6 @@ export const chromePage = action("Chrome Page")
|
|
|
40
28
|
const cdpUrl = resolved.includes("://") ? resolved : `http://${resolved}`;
|
|
41
29
|
from = cdpUrl;
|
|
42
30
|
try {
|
|
43
|
-
// connectOverCDP resolves the ws endpoint from /json/version under the
|
|
44
|
-
// hood, so the plain http URL is what to hand it.
|
|
45
31
|
owned = await chromium.connectOverCDP(cdpUrl);
|
|
46
32
|
}
|
|
47
33
|
catch (cause) {
|
|
@@ -64,11 +50,11 @@ export const chromePage = action("Chrome Page")
|
|
|
64
50
|
page = await pageOf(resolved, url !== undefined);
|
|
65
51
|
}
|
|
66
52
|
page.on("console", (msg) => {
|
|
67
|
-
|
|
53
|
+
console.log(`console: ${msg.text()}`);
|
|
68
54
|
emit("console", `from: ${label}: ${msg.text()}`);
|
|
69
55
|
});
|
|
70
56
|
page.on("pageerror", (err) => {
|
|
71
|
-
|
|
57
|
+
console.error(err.message);
|
|
72
58
|
emit("pageerror", `from: ${label}: ${err.message}`);
|
|
73
59
|
});
|
|
74
60
|
page.on("framenavigated", (frame) => {
|
|
@@ -77,16 +63,17 @@ export const chromePage = action("Chrome Page")
|
|
|
77
63
|
});
|
|
78
64
|
if (url) {
|
|
79
65
|
await page.goto(url);
|
|
80
|
-
|
|
66
|
+
console.log(`navigated ${url}`);
|
|
81
67
|
}
|
|
82
68
|
attach(page); // resolves instance.ref for every downstream action
|
|
83
|
-
|
|
84
|
-
// Daemon: hold the page open until teardown, then drop the CDP session if
|
|
85
|
-
// was ours; never close a handed-in browser/window.
|
|
69
|
+
console.log(`attached ${from}`);
|
|
70
|
+
// Daemon: hold the page open until teardown, then drop the CDP session if ours.
|
|
86
71
|
return new Promise((resolveRun) => {
|
|
87
72
|
const stop = () => {
|
|
73
|
+
// Bound the close: on a broken CDP connection `close()` can hang, which
|
|
74
|
+
// would wedge teardown — settle after a short race regardless.
|
|
88
75
|
if (owned)
|
|
89
|
-
void owned.close().finally(resolveRun);
|
|
76
|
+
void Promise.race([owned.close(), delay(2000)]).finally(resolveRun);
|
|
90
77
|
else
|
|
91
78
|
resolveRun();
|
|
92
79
|
};
|
|
@@ -1,48 +1,15 @@
|
|
|
1
|
-
import type { AnyActionInstance } from "../orchestration/action/types.ts";
|
|
2
1
|
/** Tunes an action built with {@link command}. */
|
|
3
2
|
export interface CommandOptions {
|
|
4
|
-
/**
|
|
5
|
-
* Directory to run the command in. Relative paths resolve against the directory
|
|
6
|
-
* node was executed from (process.cwd()). Default ".".
|
|
7
|
-
*/
|
|
3
|
+
/** Directory to run in; relative paths resolve against `process.cwd()`. Default ".". */
|
|
8
4
|
cwd?: string;
|
|
9
|
-
/**
|
|
10
|
-
* Environment for the child. Pass the full set you want (e.g.
|
|
11
|
-
* `{ ...process.env, NODE_ENV: "development" }`) — it's handed to the child
|
|
12
|
-
* as-is, not merged. Omit to inherit the parent's `process.env`.
|
|
13
|
-
*/
|
|
5
|
+
/** Full environment for the child (handed as-is, not merged). Omit to inherit. */
|
|
14
6
|
env?: NodeJS.ProcessEnv;
|
|
7
|
+
/** Let this command inherit the orchestrator's debugger (default false). */
|
|
8
|
+
inheritDebugger?: boolean;
|
|
15
9
|
}
|
|
16
10
|
/**
|
|
17
|
-
* Runs a fixed command as an action
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* (the `launch <name>` line and the child's log source).
|
|
21
|
-
*
|
|
22
|
-
* Two forms:
|
|
23
|
-
*
|
|
24
|
-
* - **Shell form** — `command(name, cmd, opts)`. The string runs verbatim
|
|
25
|
-
* through a shell (PowerShell on Windows), so pipes, globs, and `&&` work:
|
|
26
|
-
*
|
|
27
|
-
* command("TSKB:dev", "npm run dev:explorer", { cwd: pkg })
|
|
28
|
-
*
|
|
29
|
-
* The direct child is the *shell*, and the real program is a grandchild. On
|
|
30
|
-
* Windows teardown reaps the tree only while that shell is alive — so a GUI
|
|
31
|
-
* app that ignores console Ctrl-C (Electron) can be **orphaned and survive**.
|
|
32
|
-
*
|
|
33
|
-
* - **Direct form** — `command(name, file, args, opts)`. Spawns `file` with
|
|
34
|
-
* `args` **without a shell**, so the action's direct child *is* the program.
|
|
35
|
-
* On teardown it's force-reaped by its own PID (`taskkill /F /T`), so it dies
|
|
36
|
-
* even when a shell would have orphaned it. Use this for long-lived apps you
|
|
37
|
-
* launch and must be able to kill on Ctrl-C:
|
|
38
|
-
*
|
|
39
|
-
* command("platform", electronExe, [".", "--inspect"], { cwd: app })
|
|
40
|
-
*
|
|
41
|
-
* `file` must be a real executable (an actual `electron.exe`), not a `.cmd`
|
|
42
|
-
* shim — shims need a shell to run.
|
|
43
|
-
*
|
|
44
|
-
* For anything beyond a static command — interpolated args, emitted events, a
|
|
45
|
-
* daemon loop — drop down to `action(...).run(...)` directly.
|
|
11
|
+
* Runs a fixed shell command as an action instance. Output streams into the run's
|
|
12
|
+
* log under `name`; a non-zero exit fails the action; teardown kills the child.
|
|
13
|
+
* For interpolated args, events, or a daemon loop, drop to `action(...).run(...)`.
|
|
46
14
|
*/
|
|
47
|
-
export declare function command(name: string, cmd: string, options?: CommandOptions):
|
|
48
|
-
export declare function command(name: string, file: string, args: string[], options?: CommandOptions): AnyActionInstance;
|
|
15
|
+
export declare function command(name: string, cmd: string, options?: CommandOptions): import("../index.ts").ActionInstance<import("zx").ProcessOutput, import("../core/types.ts").NoEvents, void>;
|
package/dist/actions/command.js
CHANGED
|
@@ -1,90 +1,23 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { action } from "../
|
|
4
|
-
import { killTree } from "../system/process/process.js";
|
|
2
|
+
import { action } from "../core/action.js";
|
|
5
3
|
/**
|
|
6
|
-
* Build
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `` proc`cmd` ``. The command is developer-supplied (not user input), so
|
|
10
|
-
* verbatim is exactly what we want.
|
|
4
|
+
* Build a zx tagged template that runs `cmd` verbatim — zx only quotes interpolated
|
|
5
|
+
* args, not the template's `pieces`, so the whole (developer-supplied) command in
|
|
6
|
+
* `pieces` runs exactly as written (pipes, globs, `&&`).
|
|
11
7
|
*/
|
|
12
8
|
const verbatim = (cmd) => Object.assign([cmd], { raw: [cmd] });
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return action(name).run((
|
|
20
|
-
// path.resolve keeps an absolute cwd as-is and resolves a relative one
|
|
21
|
-
// against process.cwd() — exactly the "absolute from where node runs" rule.
|
|
9
|
+
/**
|
|
10
|
+
* Runs a fixed shell command as an action instance. Output streams into the run's
|
|
11
|
+
* log under `name`; a non-zero exit fails the action; teardown kills the child.
|
|
12
|
+
* For interpolated args, events, or a daemon loop, drop to `action(...).run(...)`.
|
|
13
|
+
*/
|
|
14
|
+
export function command(name, cmd, options = {}) {
|
|
15
|
+
return action(name).run((ctx) => {
|
|
22
16
|
const cwd = path.resolve(options.cwd ?? ".");
|
|
23
|
-
|
|
24
|
-
return proc(name, {
|
|
17
|
+
return ctx.proc(name, {
|
|
25
18
|
cwd,
|
|
26
19
|
...(options.env ? { env: options.env } : {}),
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
...(options.inheritDebugger ? { inheritDebugger: true } : {}),
|
|
21
|
+
})(verbatim(cmd));
|
|
22
|
+
})();
|
|
29
23
|
}
|
|
30
|
-
/**
|
|
31
|
-
* The direct (shell-less) launcher behind {@link command}'s four-arg form.
|
|
32
|
-
* Spawns `file`/`args` with no shell — so the returned child *is* the program,
|
|
33
|
-
* killable by its own PID on teardown — streams its stdout/stderr into the run's
|
|
34
|
-
* log, and resolves when it exits (0 = ok; a non-zero exit is a genuine failure,
|
|
35
|
-
* unless the run is tearing down, where the abort-kill is the reason).
|
|
36
|
-
*/
|
|
37
|
-
const spawnDirect = (snapshots, logs, signal, name, file, args, options) => {
|
|
38
|
-
const cwd = path.resolve(options.cwd ?? ".");
|
|
39
|
-
const source = name;
|
|
40
|
-
// windowsHide keeps a stray console window from flashing for the child.
|
|
41
|
-
// env omitted -> spawn inherits the parent's process.env (Node's default).
|
|
42
|
-
const child = spawn(file, args, { cwd, windowsHide: true, env: options.env });
|
|
43
|
-
// Mirror proc's launch line: a filterable milestone on the timeline, with the
|
|
44
|
-
// structured args snapshotted and linked (see process.ts for the rationale).
|
|
45
|
-
const snap = snapshots.captureJson(`proc-${name}-start`, { cmd: file, args, cwd });
|
|
46
|
-
logs.append({
|
|
47
|
-
source: name,
|
|
48
|
-
level: "event",
|
|
49
|
-
message: `${name} · start · ${file} ${args.join(" ")} · in ${cwd} · → ${snap.rel}`,
|
|
50
|
-
});
|
|
51
|
-
if (child.stdout)
|
|
52
|
-
logs.attach(child.stdout, source, "info");
|
|
53
|
-
if (child.stderr)
|
|
54
|
-
logs.attach(child.stderr, source, "error");
|
|
55
|
-
// Force-reap the child's tree on teardown, by its own PID. Guarded so a child
|
|
56
|
-
// that has already exited isn't re-killed (its PID may have been reused).
|
|
57
|
-
let finished = false;
|
|
58
|
-
const onAbort = () => {
|
|
59
|
-
if (finished || child.pid == null)
|
|
60
|
-
return;
|
|
61
|
-
killTree(child.pid);
|
|
62
|
-
};
|
|
63
|
-
const done = () => {
|
|
64
|
-
finished = true;
|
|
65
|
-
signal.removeEventListener("abort", onAbort);
|
|
66
|
-
};
|
|
67
|
-
if (signal.aborted)
|
|
68
|
-
killTree(child.pid ?? -1);
|
|
69
|
-
else
|
|
70
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
child.on("error", (error) => {
|
|
73
|
-
done();
|
|
74
|
-
reject(error);
|
|
75
|
-
});
|
|
76
|
-
child.on("exit", (code, sig) => {
|
|
77
|
-
done();
|
|
78
|
-
// During teardown the abort-kill is why it exited — a clean shutdown, not a
|
|
79
|
-
// fault; let it settle successfully so it isn't recorded as a failure.
|
|
80
|
-
if (signal.aborted || code === 0)
|
|
81
|
-
return resolve();
|
|
82
|
-
// Non-zero exit: reject with an error carrying `exitCode`, so the framework
|
|
83
|
-
// surfaces it on the action's Outcome (`{ ok: false, exitCode }`).
|
|
84
|
-
const error = Object.assign(new Error(`${name} exited with ${code ?? sig}`), {
|
|
85
|
-
exitCode: code ?? undefined,
|
|
86
|
-
});
|
|
87
|
-
reject(error);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
};
|
|
@@ -28,11 +28,8 @@ export interface HealthcheckOptions {
|
|
|
28
28
|
}
|
|
29
29
|
/** The passing probe — also this action's result and `healthy` event payload. */
|
|
30
30
|
export interface HealthcheckResult {
|
|
31
|
-
/** The URL that was probed. */
|
|
32
31
|
url: string;
|
|
33
|
-
/** Status of the passing probe. */
|
|
34
32
|
status: number;
|
|
35
|
-
/** Probes made, counting the passing one. */
|
|
36
33
|
attempts: number;
|
|
37
34
|
}
|
|
38
35
|
/** Events emitted by {@link healthcheck}. */
|
|
@@ -42,14 +39,9 @@ export interface HealthcheckEvents {
|
|
|
42
39
|
}
|
|
43
40
|
/**
|
|
44
41
|
* Gate: polls an HTTP endpoint until it answers in a healthy way (status and,
|
|
45
|
-
* optionally, body), then resolves with that probe and emits `healthy`.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* track, no ANSI to strip).
|
|
49
|
-
*
|
|
50
|
-
* Connection refusals and slow probes during startup are expected and simply
|
|
51
|
-
* retried; each request is bounded by `requestTimeoutMs` (so a hung socket can't
|
|
52
|
-
* stall the loop) and the whole wait by the run's abort signal and an optional
|
|
42
|
+
* optionally, body), then resolves with that probe and emits `healthy`. Refusals
|
|
43
|
+
* and slow probes during startup are retried; each request is bounded by
|
|
44
|
+
* `requestTimeoutMs` and the whole wait by the run's abort signal and optional
|
|
53
45
|
* `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
|
|
54
46
|
*/
|
|
55
47
|
export declare const healthcheck: import("../index.ts").Action<[opts?: HealthcheckOptions | undefined], HealthcheckResult, HealthcheckEvents, void>;
|