omkit 0.4.0 → 0.4.2
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/dist/actions/chrome-page.js +8 -0
- package/dist/cli/client/discovery.d.ts +3 -2
- package/dist/cli/client/discovery.js +27 -11
- package/dist/cli/commands/check.d.ts +8 -3
- package/dist/cli/commands/check.js +12 -9
- package/dist/cli/commands/run.d.ts +8 -0
- package/dist/cli/commands/run.js +17 -0
- package/dist/cli/commands/ui.d.ts +6 -1
- package/dist/cli/commands/ui.js +21 -2
- package/dist/cli/index.js +48 -9
- package/dist/cli/ui/Report.d.ts +40 -0
- package/dist/cli/ui/Report.js +63 -0
- package/dist/cli/ui/app.d.ts +4 -1
- package/dist/cli/ui/app.js +5 -3
- package/dist/core/ActionRun.js +7 -2
- package/package.json +1 -1
|
@@ -65,6 +65,14 @@ export const chromePage = action("chromePage")
|
|
|
65
65
|
await page.goto(url);
|
|
66
66
|
console.log(`navigated ${url}`);
|
|
67
67
|
}
|
|
68
|
+
// Oms run under tsx, which forces esbuild `keepNames`: every function serialized into
|
|
69
|
+
// page.evaluate() carries a bare `__name(...)` wrapper that is undefined in the page and
|
|
70
|
+
// throws `ReferenceError: __name is not defined`. Define a no-op __name so those bodies
|
|
71
|
+
// run — seed the current document, and register it for any later navigation. Passed as a
|
|
72
|
+
// string so tsx doesn't rewrite (and re-break) the shim itself.
|
|
73
|
+
const nameShim = "globalThis.__name = globalThis.__name || function (f) { return f; };";
|
|
74
|
+
await page.evaluate(nameShim);
|
|
75
|
+
await page.addInitScript(nameShim);
|
|
68
76
|
attach(page); // resolves instance.ref for every downstream action
|
|
69
77
|
console.log(`attached ${from}`);
|
|
70
78
|
// Daemon: hold the page open until teardown, then drop the CDP session if ours.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Registry } from "./registry.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
|
|
4
|
-
* actions.
|
|
5
|
-
*
|
|
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.
|
|
6
7
|
*/
|
|
7
8
|
export declare function discover(tsconfigPath: string): Registry;
|
|
@@ -2,22 +2,16 @@ import ts from "typescript";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
/**
|
|
4
4
|
* Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
|
|
5
|
-
* actions.
|
|
6
|
-
*
|
|
5
|
+
* actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
|
|
6
|
+
* project stays useful), but a **fatal config problem** — a missing, unparseable, or invalid
|
|
7
|
+
* tsconfig — throws, so the CLI crashes visibly instead of silently returning an empty project.
|
|
7
8
|
*/
|
|
8
9
|
export function discover(tsconfigPath) {
|
|
9
10
|
const oms = [];
|
|
10
11
|
const actions = [];
|
|
11
12
|
const warnings = [];
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
warnings.push(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
|
|
15
|
-
return { oms, actions, warnings };
|
|
16
|
-
}
|
|
17
|
-
// Resolve to an absolute base so `parsed.fileNames` are absolute. Otherwise root files stay
|
|
18
|
-
// relative while imported files (e.g. an action pulled in by an om) resolve to absolute paths,
|
|
19
|
-
// and the two never match in `fileSet`.
|
|
20
|
-
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.resolve(path.dirname(tsconfigPath)));
|
|
13
|
+
const { parsed, warnings: configWarnings } = loadProject(tsconfigPath);
|
|
14
|
+
warnings.push(...configWarnings);
|
|
21
15
|
const program = ts.createProgram({
|
|
22
16
|
rootNames: parsed.fileNames,
|
|
23
17
|
options: { ...parsed.options, noEmit: true },
|
|
@@ -62,6 +56,28 @@ export function discover(tsconfigPath) {
|
|
|
62
56
|
}
|
|
63
57
|
return { oms, actions, warnings };
|
|
64
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Load and validate the tsconfig, throwing on a fatal config problem — a missing/unparseable file,
|
|
61
|
+
* or a config-level error (bad compilerOptions, an unresolvable `extends`). The benign "no inputs
|
|
62
|
+
* found" (TS18003) is not fatal: an empty-but-valid project comes back as a warning instead.
|
|
63
|
+
*
|
|
64
|
+
* Parses against an absolute base dir so `parsed.fileNames` are absolute — otherwise root files
|
|
65
|
+
* stay relative while imported files (an action pulled in by an om) resolve absolute, and the two
|
|
66
|
+
* never match in the caller's `fileSet`.
|
|
67
|
+
*/
|
|
68
|
+
function loadProject(tsconfigPath) {
|
|
69
|
+
const flatten = (d) => ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
70
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
71
|
+
if (configFile.error)
|
|
72
|
+
throw new Error(`could not read ${tsconfigPath}: ${flatten(configFile.error)}`);
|
|
73
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.resolve(path.dirname(tsconfigPath)));
|
|
74
|
+
const fatal = parsed.errors.filter((e) => e.category === ts.DiagnosticCategory.Error && e.code !== 18003);
|
|
75
|
+
if (fatal.length) {
|
|
76
|
+
throw new Error(`invalid ${tsconfigPath}:\n${fatal.map((e) => ` ${flatten(e)}`).join("\n")}`);
|
|
77
|
+
}
|
|
78
|
+
const warnings = parsed.errors.filter((e) => e.code === 18003).map(flatten);
|
|
79
|
+
return { parsed, warnings };
|
|
80
|
+
}
|
|
65
81
|
/** Collect the local names bound to om/step/action from `import … from "omkit"`. */
|
|
66
82
|
function omkitImports(sf) {
|
|
67
83
|
const names = {};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Diagnostic } from "../client/types.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type { ReportItem } from "../ui/Report.tsx";
|
|
3
|
+
/**
|
|
4
|
+
* Turn typecheck diagnostics into a report: a headline, one item per diagnostic (location + message),
|
|
5
|
+
* and the process exit code — **1 when there are any errors**, 0 when the project is clean.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkReport(diagnostics: Diagnostic[]): {
|
|
8
|
+
title: string;
|
|
9
|
+
items: ReportItem[];
|
|
5
10
|
code: number;
|
|
6
11
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Turn typecheck diagnostics into a report: a headline, one item per diagnostic (location + message),
|
|
4
|
+
* and the process exit code — **1 when there are any errors**, 0 when the project is clean.
|
|
5
|
+
*/
|
|
6
|
+
export function checkReport(diagnostics) {
|
|
7
|
+
const n = diagnostics.length;
|
|
8
|
+
const items = diagnostics.map((d) => ({
|
|
9
|
+
head: `${path.basename(d.file)}:${d.line}`,
|
|
10
|
+
detail: d.message,
|
|
11
|
+
}));
|
|
12
|
+
const title = n === 0 ? "no type errors" : `${n} type error${n === 1 ? "" : "s"}`;
|
|
13
|
+
return { title, items, code: n === 0 ? 0 : 1 };
|
|
11
14
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { Registry } from "../client/registry.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
|
|
4
|
+
* omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
|
|
5
|
+
* lists any config warnings (e.g. TypeScript's "no inputs found" with the offending globs), and
|
|
6
|
+
* points at the usual cause: an `include` that doesn't reach the om files.
|
|
7
|
+
*/
|
|
8
|
+
export declare function reportNoOms(tsconfig: string, registry: Registry): Promise<void>;
|
|
1
9
|
/**
|
|
2
10
|
* Resolve a `run` argument to an absolute om file: an existing file path (relative to `cwd`)
|
|
3
11
|
* wins; otherwise match a discovered om by name. Returns undefined when nothing matches.
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { renderDiagnostics } from "../ui/Report.js";
|
|
4
|
+
/**
|
|
5
|
+
* Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
|
|
6
|
+
* omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
|
|
7
|
+
* lists any config warnings (e.g. TypeScript's "no inputs found" with the offending globs), and
|
|
8
|
+
* points at the usual cause: an `include` that doesn't reach the om files.
|
|
9
|
+
*/
|
|
10
|
+
export function reportNoOms(tsconfig, registry) {
|
|
11
|
+
return renderDiagnostics({
|
|
12
|
+
kind: "warning",
|
|
13
|
+
title: "no oms found",
|
|
14
|
+
subtitle: `config: ${path.resolve(tsconfig)}`,
|
|
15
|
+
items: registry.warnings.map((w) => ({ head: "", detail: w })),
|
|
16
|
+
hint: "Check the config's `include` — it must reach your om files, and a glob only recurses " +
|
|
17
|
+
'with `**` (e.g. "src/**/*.ts", not "src/*.ts").',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* Resolve a `run` argument to an absolute om file: an existing file path (relative to `cwd`)
|
|
5
22
|
* wins; otherwise match a discovered om by name. Returns undefined when nothing matches.
|
|
@@ -3,5 +3,10 @@ import type { OmkitClient } from "../client/index.ts";
|
|
|
3
3
|
* Render the interactive Ink app. Ink's own Ctrl+C handling is disabled so the app can tear the
|
|
4
4
|
* run down gracefully first; the final verdict + run-folder path are printed here, after Ink
|
|
5
5
|
* unmounts (it erases its own frame), so the pointer to the on-disk record survives on screen.
|
|
6
|
+
*
|
|
7
|
+
* Discovery runs first, before Ink takes the terminal: a fatal config problem throws here and the
|
|
8
|
+
* bin's error boundary turns it into a clean crash rather than an empty list. With no oms to show,
|
|
9
|
+
* it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
|
|
10
|
+
* are printed so they aren't lost behind the UI.
|
|
6
11
|
*/
|
|
7
|
-
export declare function launchUi(client: OmkitClient): void
|
|
12
|
+
export declare function launchUi(client: OmkitClient, tsconfig: string): Promise<void>;
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { App } from "../ui/app.js";
|
|
4
|
+
import { renderDiagnostics, withSpinner } from "../ui/Report.js";
|
|
5
|
+
import { reportNoOms } from "./run.js";
|
|
4
6
|
/**
|
|
5
7
|
* Render the interactive Ink app. Ink's own Ctrl+C handling is disabled so the app can tear the
|
|
6
8
|
* run down gracefully first; the final verdict + run-folder path are printed here, after Ink
|
|
7
9
|
* unmounts (it erases its own frame), so the pointer to the on-disk record survives on screen.
|
|
10
|
+
*
|
|
11
|
+
* Discovery runs first, before Ink takes the terminal: a fatal config problem throws here and the
|
|
12
|
+
* bin's error boundary turns it into a clean crash rather than an empty list. With no oms to show,
|
|
13
|
+
* it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
|
|
14
|
+
* are printed so they aren't lost behind the UI.
|
|
8
15
|
*/
|
|
9
|
-
export function launchUi(client) {
|
|
16
|
+
export async function launchUi(client, tsconfig) {
|
|
17
|
+
const registry = await withSpinner("discovering…", () => client.discover());
|
|
18
|
+
if (registry.oms.length === 0) {
|
|
19
|
+
await reportNoOms(tsconfig, registry);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (registry.warnings.length > 0) {
|
|
23
|
+
await renderDiagnostics({
|
|
24
|
+
kind: "warning",
|
|
25
|
+
title: `${registry.warnings.length} warning${registry.warnings.length === 1 ? "" : "s"}`,
|
|
26
|
+
items: registry.warnings.map((w) => ({ head: "", detail: w })),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
10
29
|
let finalVerdict;
|
|
11
|
-
const app = render(_jsx(App, { client: client, onExit: (v) => (finalVerdict = v) }), {
|
|
30
|
+
const app = render(_jsx(App, { client: client, oms: registry.oms, onExit: (v) => (finalVerdict = v) }), {
|
|
12
31
|
exitOnCtrlC: false,
|
|
13
32
|
});
|
|
14
33
|
void app.waitUntilExit().then(() => {
|
package/dist/cli/index.js
CHANGED
|
@@ -36,13 +36,18 @@ async function main() {
|
|
|
36
36
|
}
|
|
37
37
|
if (cli.command === "ls") {
|
|
38
38
|
const { formatRegistry } = await import("./commands/ls.js");
|
|
39
|
-
|
|
39
|
+
const { withSpinner } = await import("./ui/Report.js");
|
|
40
|
+
const registry = await withSpinner("discovering…", () => client.discover());
|
|
41
|
+
console.log(formatRegistry(registry, { json: cli.json }));
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
if (cli.command === "check") {
|
|
43
|
-
const {
|
|
44
|
-
const {
|
|
45
|
-
|
|
45
|
+
const { checkReport } = await import("./commands/check.js");
|
|
46
|
+
const { renderDiagnostics, withSpinner } = await import("./ui/Report.js");
|
|
47
|
+
const diagnostics = await withSpinner("type-checking…", () => client.check());
|
|
48
|
+
const { title, items, code } = checkReport(diagnostics);
|
|
49
|
+
// Type errors fail the command (exit 1); a clean project reports ok and exits 0.
|
|
50
|
+
await renderDiagnostics({ kind: code === 0 ? "ok" : "error", title, items }, code === 0 ? process.stdout : process.stderr);
|
|
46
51
|
process.exitCode = code;
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
@@ -51,12 +56,18 @@ async function main() {
|
|
|
51
56
|
// A bare `omkit` lands here too, since `run` is the default command.
|
|
52
57
|
if (!cli.target) {
|
|
53
58
|
const { launchUi } = await import("./commands/ui.js");
|
|
54
|
-
launchUi(client);
|
|
59
|
+
await launchUi(client, cli.tsconfig);
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
|
-
const { resolveOm } = await import("./commands/run.js");
|
|
62
|
+
const { resolveOm, reportNoOms } = await import("./commands/run.js");
|
|
58
63
|
const { spawnBare } = await import("./client/runner.js");
|
|
59
|
-
const
|
|
64
|
+
const { withSpinner } = await import("./ui/Report.js");
|
|
65
|
+
const registry = await withSpinner("discovering…", () => client.discover());
|
|
66
|
+
if (registry.oms.length === 0) {
|
|
67
|
+
await reportNoOms(cli.tsconfig, registry);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
60
71
|
const omFile = resolveOm(cli.target, registry, process.cwd());
|
|
61
72
|
if (!omFile) {
|
|
62
73
|
const known = registry.oms.map((o) => o.name).join(", ");
|
|
@@ -87,5 +98,33 @@ function isEntryPoint() {
|
|
|
87
98
|
return false;
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
/** Print a fatal error to stderr in one consistent shape, just before the process exits non-zero. */
|
|
102
|
+
function reportFatal(err) {
|
|
103
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
104
|
+
process.stderr.write(`\nomkit: ${detail}\n`);
|
|
105
|
+
}
|
|
106
|
+
if (isEntryPoint()) {
|
|
107
|
+
// Nothing should fail silently. A fatal anywhere — a bad config, an uncaught throw, or a rejected
|
|
108
|
+
// promise in the interactive app — prints and crashes the CLI with a non-zero exit code.
|
|
109
|
+
//
|
|
110
|
+
// The controlled path (a rejection from `main`, e.g. a bad tsconfig) gets the pretty Ink panel;
|
|
111
|
+
// the last-resort sync handlers stay plain, since the process may be mid-crash and can't await.
|
|
112
|
+
process.on("uncaughtException", (err) => {
|
|
113
|
+
reportFatal(err);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
process.on("unhandledRejection", (err) => {
|
|
117
|
+
reportFatal(err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
main().catch(async (err) => {
|
|
121
|
+
const { renderDiagnostics } = await import("./ui/Report.js");
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
// Keep the panel clean by default (a bad tsconfig needs no stack); set OMKIT_DEBUG for the trace.
|
|
124
|
+
const stack = process.env.OMKIT_DEBUG && err instanceof Error && err.stack
|
|
125
|
+
? err.stack.split("\n").slice(1).join("\n")
|
|
126
|
+
: undefined;
|
|
127
|
+
await renderDiagnostics({ kind: "error", title: message, hint: stack });
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
export type ReportKind = "error" | "warning" | "ok" | "info";
|
|
3
|
+
/** One row of a report: a colored `head` (e.g. a file:line location) and a dim `detail`. */
|
|
4
|
+
export interface ReportItem {
|
|
5
|
+
head: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ReportProps {
|
|
9
|
+
kind: ReportKind;
|
|
10
|
+
/** The bold, colored headline (e.g. "3 type errors"). */
|
|
11
|
+
title: string;
|
|
12
|
+
/** A dim line under the title — e.g. the config file a diagnostic came from. */
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
/** The list of findings, shown under a separator. */
|
|
15
|
+
items?: ReportItem[];
|
|
16
|
+
/** A dim footer with next-step guidance. */
|
|
17
|
+
hint?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A colorful, bordered diagnostics panel: an icon + title header, an optional subtitle, a separator,
|
|
21
|
+
* a list of items (colored location + dim detail), and an optional dim hint footer. Kind picks the
|
|
22
|
+
* accent color (error red / warning yellow / ok green / info cyan).
|
|
23
|
+
*/
|
|
24
|
+
export declare function Report({ kind, title, subtitle, items, hint }: ReportProps): ReactElement;
|
|
25
|
+
/**
|
|
26
|
+
* Render a one-shot Ink element to `stream` (stderr by default) and resolve once it's painted.
|
|
27
|
+
* Paints a single frame then unmounts, leaving it in the scrollback — the right shape for
|
|
28
|
+
* non-interactive diagnostic output, unlike the live app.
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderReport(element: ReactElement, stream?: NodeJS.WriteStream): Promise<void>;
|
|
31
|
+
/** Build a {@link Report} from plain data and render it — so non-JSX call sites (the bin, run.ts)
|
|
32
|
+
* stay free of JSX while still using the Ink component. */
|
|
33
|
+
export declare function renderDiagnostics(props: ReportProps, stream?: NodeJS.WriteStream): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Show a spinner labelled `label` while `work` runs, then clear it and resolve `work`'s result.
|
|
36
|
+
* Good for the noticeable-but-quiet waits (discovery, typecheck). Note: a CPU-bound synchronous
|
|
37
|
+
* `work` blocks the event loop, so the spinner shows its label but can't animate until `work`
|
|
38
|
+
* yields — it still tells the user something is happening instead of a silent hang.
|
|
39
|
+
*/
|
|
40
|
+
export declare function withSpinner<T>(label: string, work: () => T | Promise<T>, stream?: NodeJS.WriteStream): Promise<T>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { Box, Text, render, useStdout } from "ink";
|
|
4
|
+
import { Spinner } from "./Spinner.js";
|
|
5
|
+
const THEME = {
|
|
6
|
+
error: { color: "red", icon: "✗" },
|
|
7
|
+
warning: { color: "yellow", icon: "⚠" },
|
|
8
|
+
ok: { color: "green", icon: "✓" },
|
|
9
|
+
info: { color: "cyan", icon: "ℹ" },
|
|
10
|
+
};
|
|
11
|
+
/** A horizontal rule sized to the terminal (bounded), used to separate the header from the list. */
|
|
12
|
+
function Divider({ color }) {
|
|
13
|
+
const { stdout } = useStdout();
|
|
14
|
+
const width = Math.max(Math.min((stdout?.columns ?? 80) - 6, 56), 12);
|
|
15
|
+
return (_jsx(Text, { color: color, dimColor: true, children: "─".repeat(width) }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A colorful, bordered diagnostics panel: an icon + title header, an optional subtitle, a separator,
|
|
19
|
+
* a list of items (colored location + dim detail), and an optional dim hint footer. Kind picks the
|
|
20
|
+
* accent color (error red / warning yellow / ok green / info cyan).
|
|
21
|
+
*/
|
|
22
|
+
export function Report({ kind, title, subtitle, items = [], hint }) {
|
|
23
|
+
const { color, icon } = THEME[kind];
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1, children: [_jsxs(Text, { color: color, bold: true, children: [icon, " ", title] }), subtitle ? _jsx(Text, { dimColor: true, children: subtitle }) : null, items.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Divider, { color: color }), items.map((it, i) => (_jsxs(Text, { children: [it.head ? _jsx(Text, { color: color, children: it.head }) : null, it.detail ? _jsx(Text, { dimColor: true, children: (it.head ? " " : "") + it.detail }) : null] }, i)))] })) : null, hint ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: hint }) })) : null] }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render a one-shot Ink element to `stream` (stderr by default) and resolve once it's painted.
|
|
28
|
+
* Paints a single frame then unmounts, leaving it in the scrollback — the right shape for
|
|
29
|
+
* non-interactive diagnostic output, unlike the live app.
|
|
30
|
+
*/
|
|
31
|
+
export async function renderReport(element, stream = process.stderr) {
|
|
32
|
+
const app = render(element, { stdout: stream, patchConsole: false });
|
|
33
|
+
await new Promise((resolve) => setImmediate(resolve)); // let the frame paint once
|
|
34
|
+
app.unmount();
|
|
35
|
+
await app.waitUntilExit();
|
|
36
|
+
}
|
|
37
|
+
/** Build a {@link Report} from plain data and render it — so non-JSX call sites (the bin, run.ts)
|
|
38
|
+
* stay free of JSX while still using the Ink component. */
|
|
39
|
+
export function renderDiagnostics(props, stream = process.stderr) {
|
|
40
|
+
return renderReport(_jsx(Report, { ...props }), stream);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Show a spinner labelled `label` while `work` runs, then clear it and resolve `work`'s result.
|
|
44
|
+
* Good for the noticeable-but-quiet waits (discovery, typecheck). Note: a CPU-bound synchronous
|
|
45
|
+
* `work` blocks the event loop, so the spinner shows its label but can't animate until `work`
|
|
46
|
+
* yields — it still tells the user something is happening instead of a silent hang.
|
|
47
|
+
*/
|
|
48
|
+
export async function withSpinner(label, work, stream = process.stderr) {
|
|
49
|
+
// No TTY (piped/CI) → a spinner can't animate or erase itself; skip it and just do the work,
|
|
50
|
+
// so the output stays clean rather than leaving a stray "…" line above the result.
|
|
51
|
+
if (!stream.isTTY)
|
|
52
|
+
return work();
|
|
53
|
+
const app = render(_jsx(Spinner, { label: label }), { stdout: stream, patchConsole: false });
|
|
54
|
+
try {
|
|
55
|
+
await new Promise((resolve) => setImmediate(resolve)); // let the first frame paint
|
|
56
|
+
return await work();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
app.clear(); // erase the spinner frame (unmount alone leaves the last frame on screen)
|
|
60
|
+
app.unmount();
|
|
61
|
+
await app.waitUntilExit();
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/cli/ui/app.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { OmkitClient, Verdict } from "../client/types.ts";
|
|
3
|
+
import type { DiscoveredOm } from "../client/registry.ts";
|
|
3
4
|
/** The interactive app: discover → list/search → run → live milestones + prompts. */
|
|
4
|
-
export declare function App({ client, onExit, }: {
|
|
5
|
+
export declare function App({ client, onExit, oms: initialOms, }: {
|
|
5
6
|
client: OmkitClient;
|
|
6
7
|
/** Called with the run's verdict (if any) just before Ink unmounts, so the caller can print
|
|
7
8
|
* a durable summary — Ink erases its own frame on exit. */
|
|
8
9
|
onExit?: (verdict: Verdict | undefined) => void;
|
|
10
|
+
/** Oms discovered by the caller (pre-flight). When given, the app skips its own discovery. */
|
|
11
|
+
oms?: DiscoveredOm[];
|
|
9
12
|
}): ReactElement;
|
package/dist/cli/ui/app.js
CHANGED
|
@@ -6,9 +6,9 @@ import { OmList } from "./views/OmList.js";
|
|
|
6
6
|
import { RunView } from "./views/RunView.js";
|
|
7
7
|
import { Spinner } from "./Spinner.js";
|
|
8
8
|
/** The interactive app: discover → list/search → run → live milestones + prompts. */
|
|
9
|
-
export function App({ client, onExit, }) {
|
|
9
|
+
export function App({ client, onExit, oms: initialOms, }) {
|
|
10
10
|
const { exit } = useApp();
|
|
11
|
-
const [oms, setOms] = useState([]);
|
|
11
|
+
const [oms, setOms] = useState(initialOms ?? []);
|
|
12
12
|
const [session, setSession] = useState(null);
|
|
13
13
|
const [lines, setLines] = useState([]);
|
|
14
14
|
const [prompt, setPrompt] = useState();
|
|
@@ -19,6 +19,8 @@ export function App({ client, onExit, }) {
|
|
|
19
19
|
const verdictRef = useRef(undefined);
|
|
20
20
|
const tearingRef = useRef(false);
|
|
21
21
|
useEffect(() => {
|
|
22
|
+
if (initialOms)
|
|
23
|
+
return; // provided by the caller's pre-flight discovery
|
|
22
24
|
let live = true;
|
|
23
25
|
void client.discover().then((r) => {
|
|
24
26
|
if (live)
|
|
@@ -27,7 +29,7 @@ export function App({ client, onExit, }) {
|
|
|
27
29
|
return () => {
|
|
28
30
|
live = false;
|
|
29
31
|
};
|
|
30
|
-
}, [client]);
|
|
32
|
+
}, [client, initialOms]);
|
|
31
33
|
const run = (om) => {
|
|
32
34
|
const s = client.run(om.file);
|
|
33
35
|
sessionRef.current = s;
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -131,8 +131,13 @@ export class ActionRun {
|
|
|
131
131
|
if (win())
|
|
132
132
|
resolve(undefined);
|
|
133
133
|
}, (error) => {
|
|
134
|
-
if (win())
|
|
135
|
-
|
|
134
|
+
if (!win())
|
|
135
|
+
return;
|
|
136
|
+
// Awaiting the `error` event hands back the error; any other gate rejects on failure.
|
|
137
|
+
if (key === "error")
|
|
138
|
+
resolve(error);
|
|
139
|
+
else
|
|
140
|
+
reject(error);
|
|
136
141
|
});
|
|
137
142
|
});
|
|
138
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|