@termwright/probe-ink 0.2.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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/bun-preload.d.ts +5 -0
- package/dist/bun-preload.js +30 -0
- package/dist/bun-preload.js.map +1 -0
- package/dist/chunk-IUFXTMZ7.js +31 -0
- package/dist/chunk-IUFXTMZ7.js.map +1 -0
- package/dist/chunk-LO7YF74P.js +12 -0
- package/dist/chunk-LO7YF74P.js.map +1 -0
- package/dist/chunk-Y5WYMWRU.js +358 -0
- package/dist/chunk-Y5WYMWRU.js.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts +26 -0
- package/dist/instrument.js +134 -0
- package/dist/instrument.js.map +1 -0
- package/dist/node-hook.d.ts +14 -0
- package/dist/node-hook.js +38 -0
- package/dist/node-hook.js.map +1 -0
- package/dist/observe-C853n-dp.d.ts +71 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gorce-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @termwright/probe-ink
|
|
2
|
+
|
|
3
|
+
Zero-config semantics for an Ink 7 application that imports nothing from
|
|
4
|
+
termwright. The application keeps its normal `import {render} from 'ink'` and
|
|
5
|
+
normal `render(<App />)` call; the launcher adds one preload flag.
|
|
6
|
+
|
|
7
|
+
## Install and launch
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install --save-dev @termwright/probe-ink
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peers: `ink >= 7.1 < 8`, React >= 19.2. Node >= 22, or Bun.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {withProbe} from '@termwright/probe-ink';
|
|
17
|
+
|
|
18
|
+
const {command} = withProbe('node', ['node', 'app.mjs']);
|
|
19
|
+
await launchTerminal({
|
|
20
|
+
command,
|
|
21
|
+
env: {
|
|
22
|
+
TERMWRIGHT_ENDPOINT: endpoint,
|
|
23
|
+
TERMWRIGHT_TOKEN: token,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For Bun, pass `withProbe('bun', ['bun', 'app.tsx'])`; the launcher places
|
|
29
|
+
`--preload` before the application entry, where Bun requires it. Node uses
|
|
30
|
+
`--import`. The returned preload path is a `file://` URL, including on Windows.
|
|
31
|
+
|
|
32
|
+
Early Node 22 releases do not have `module.registerHooks`; the preload detects
|
|
33
|
+
that case and uses `module.register`. It does not change Node's warning policy.
|
|
34
|
+
Pinned Ink itself emits a JSON-import ExperimentalWarning on Node 22.9, with or
|
|
35
|
+
without the probe, and byte parity retains it.
|
|
36
|
+
|
|
37
|
+
## Dormant and failure behaviour
|
|
38
|
+
|
|
39
|
+
Without both `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN`, the preload installs
|
|
40
|
+
no loader hook and `ink.render` is untouched. If the driver is unavailable or
|
|
41
|
+
the observed Ink internals move, semantics disable themselves and the
|
|
42
|
+
application continues. Process tests assert byte identity for dormant and
|
|
43
|
+
faulted runs under both Node and Bun.
|
|
44
|
+
|
|
45
|
+
## What is observed
|
|
46
|
+
|
|
47
|
+
Every retained Ink host survives into Probe IR:
|
|
48
|
+
|
|
49
|
+
- `ink-root` becomes `application`;
|
|
50
|
+
- `ink-text` and `ink-virtual-text` become `text`;
|
|
51
|
+
- every unannotated `ink-box` remains a `generic` node with its subtree;
|
|
52
|
+
- retained `aria-role` plus `checked`, `disabled`, `expanded`, `readonly`,
|
|
53
|
+
`selected`, `busy`, and `multiline` aria-state facts are read directly;
|
|
54
|
+
- host object identity is kept in a `WeakMap`, so it is stable across renders.
|
|
55
|
+
|
|
56
|
+
An application may optionally import `@termwright/ink` and attach developer
|
|
57
|
+
intent with `useSemantic` or `<Semantic>`. The injected probe consumes that
|
|
58
|
+
process-local weak registry and merges `role`, `name`, `description`, `testId`,
|
|
59
|
+
domain `extended` state, actions, and relationships with the retained tree.
|
|
60
|
+
Developer annotations have annotation provenance; Ink-retained ARIA hints keep
|
|
61
|
+
framework provenance. Physical facts such as text, state, focus, visibility,
|
|
62
|
+
value, and bounds cannot be supplied by the annotation SDK.
|
|
63
|
+
|
|
64
|
+
Each live update publishes a full snapshot and revision commit. Ink calls
|
|
65
|
+
`onRender` before it writes frame bytes, so the probe freezes the laid-out host
|
|
66
|
+
tree synchronously, then drains the output stream and writes the authenticated
|
|
67
|
+
marker last. A newer render that arrives during drain suppresses the stale
|
|
68
|
+
marker. Instrumented output is byte-identical to vanilla output after those
|
|
69
|
+
markers are removed.
|
|
70
|
+
|
|
71
|
+
Bounds are published only when Ink runs interactively in the alternate screen
|
|
72
|
+
buffer and no `<Static>` content shifts the live region. Otherwise coordinates
|
|
73
|
+
are omitted rather than reported as terminal-absolute when they are not.
|
|
74
|
+
With `TERMWRIGHT_PROTOCOL=termwright/2`, those facts use tagged observations:
|
|
75
|
+
display is known, intended bounds are known or explicitly unsupported, and
|
|
76
|
+
visible clipping plus pointer ownership are unsupported rather than inferred.
|
|
77
|
+
|
|
78
|
+
## Deliberate limits
|
|
79
|
+
|
|
80
|
+
Ink's reconciler discards application component names before creating the host
|
|
81
|
+
tree. A source component such as `ApproveButton` therefore cannot be reported:
|
|
82
|
+
`frameworkType` honestly remains `ink-box`, never a guessed component name or
|
|
83
|
+
stack. The same boundary prevents attributing Ink's active focus id to a host.
|
|
84
|
+
Text selection and third-party input values also remain unobservable without a
|
|
85
|
+
widget-library probe or author annotation. Ink retains `required` and
|
|
86
|
+
`multiselectable`, but the current semantic wire has no corresponding state
|
|
87
|
+
fields, so this probe omits them rather than repurposing a different fact.
|
|
88
|
+
|
|
89
|
+
Ink does not retain `aria-label` on a host element. A role that takes its name
|
|
90
|
+
from content uses rendered text; containers keep an empty name. These limits are
|
|
91
|
+
upstream facts documented in `docs/architecture/audit/ink.md`, not heuristics.
|
|
92
|
+
|
|
93
|
+
`@termwright/ink` is only the optional annotation SDK. Rendering, observation,
|
|
94
|
+
transport, and revision publication remain owned by this injected probe.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Bun entry: `bun --preload @termwright/probe-ink/bun-preload app.js`. */
|
|
2
|
+
/** Register the Ink entry replacement before the application's first import. */
|
|
3
|
+
declare function installBunPreload(env?: Record<string, string | undefined>): boolean;
|
|
4
|
+
|
|
5
|
+
export { installBunPreload };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INK_ENTRY_PATTERN,
|
|
3
|
+
buildShimSource,
|
|
4
|
+
shouldShim
|
|
5
|
+
} from "./chunk-IUFXTMZ7.js";
|
|
6
|
+
import {
|
|
7
|
+
isInstrumented
|
|
8
|
+
} from "./chunk-LO7YF74P.js";
|
|
9
|
+
|
|
10
|
+
// src/bun-preload.ts
|
|
11
|
+
function installBunPreload(env = process.env) {
|
|
12
|
+
if (!isInstrumented(env)) return false;
|
|
13
|
+
const bun = globalThis.Bun;
|
|
14
|
+
if (bun === void 0) return false;
|
|
15
|
+
bun.plugin({
|
|
16
|
+
name: "termwright-ink",
|
|
17
|
+
setup(build) {
|
|
18
|
+
build.onLoad({ filter: INK_ENTRY_PATTERN }, async (args) => {
|
|
19
|
+
if (!shouldShim(args.path)) return void 0;
|
|
20
|
+
return { loader: "js", contents: buildShimSource(args.path) };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
installBunPreload();
|
|
27
|
+
export {
|
|
28
|
+
installBunPreload
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=bun-preload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bun-preload.ts"],"sourcesContent":["/** Bun entry: `bun --preload @termwright/probe-ink/bun-preload app.js`. */\n\nimport { buildShimSource, INK_ENTRY_PATTERN, shouldShim } from './shim.js';\nimport { isInstrumented } from './runtime.js';\n\ninterface BunPluginBuild {\n onLoad(\n options: { filter: RegExp },\n callback: (args: { path: string }) => Promise<{ loader: string; contents: string } | undefined>,\n ): void;\n}\n\ninterface BunGlobal {\n plugin(definition: { name: string; setup(build: BunPluginBuild): void }): void;\n}\n\n/** Register the Ink entry replacement before the application's first import. */\nexport function installBunPreload(\n env: Record<string, string | undefined> = process.env,\n): boolean {\n if (!isInstrumented(env)) return false;\n const bun = (globalThis as { Bun?: BunGlobal }).Bun;\n if (bun === undefined) return false;\n\n bun.plugin({\n name: 'termwright-ink',\n setup(build) {\n build.onLoad({ filter: INK_ENTRY_PATTERN }, async (args) => {\n if (!shouldShim(args.path)) return undefined;\n return { loader: 'js', contents: buildShimSource(args.path) };\n });\n },\n });\n return true;\n}\n\ninstallBunPreload();\n"],"mappings":";;;;;;;;;;AAiBO,SAAS,kBACd,MAA0C,QAAQ,KACzC;AACT,MAAI,CAAC,eAAe,GAAG,EAAG,QAAO;AACjC,QAAM,MAAO,WAAmC;AAChD,MAAI,QAAQ,OAAW,QAAO;AAE9B,MAAI,OAAO;AAAA,IACT,MAAM;AAAA,IACN,MAAM,OAAO;AACX,YAAM,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,SAAS;AAC1D,YAAI,CAAC,WAAW,KAAK,IAAI,EAAG,QAAO;AACnC,eAAO,EAAE,QAAQ,MAAM,UAAU,gBAAgB,KAAK,IAAI,EAAE;AAAA,MAC9D,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,kBAAkB;","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/shim.ts
|
|
2
|
+
var ORIGINAL_MARKER = "termwright-original=1";
|
|
3
|
+
var INK_ENTRY_PATTERN = /[\\/](?:ink|ink@[^\\/]+)[\\/]build[\\/]index\.js$/u;
|
|
4
|
+
var INSTRUMENT_URL = new URL("./instrument.js", import.meta.url).href;
|
|
5
|
+
function shouldShim(urlOrPath) {
|
|
6
|
+
if (urlOrPath.includes(ORIGINAL_MARKER)) return false;
|
|
7
|
+
return INK_ENTRY_PATTERN.test(urlOrPath.split("?")[0] ?? "");
|
|
8
|
+
}
|
|
9
|
+
function originalUrl(urlOrPath) {
|
|
10
|
+
return `${urlOrPath}${urlOrPath.includes("?") ? "&" : "?"}${ORIGINAL_MARKER}`;
|
|
11
|
+
}
|
|
12
|
+
function buildShimSource(target, instrumentUrl = INSTRUMENT_URL) {
|
|
13
|
+
const original = JSON.stringify(originalUrl(target));
|
|
14
|
+
const instrument = JSON.stringify(instrumentUrl);
|
|
15
|
+
return `import * as __termwright_original from ${original};
|
|
16
|
+
import {wrapInkRender as __termwright_wrap} from ${instrument};
|
|
17
|
+
export * from ${original};
|
|
18
|
+
|
|
19
|
+
export const render = __termwright_wrap(__termwright_original);
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
ORIGINAL_MARKER,
|
|
25
|
+
INK_ENTRY_PATTERN,
|
|
26
|
+
INSTRUMENT_URL,
|
|
27
|
+
shouldShim,
|
|
28
|
+
originalUrl,
|
|
29
|
+
buildShimSource
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-IUFXTMZ7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shim.ts"],"sourcesContent":["/** Replacement source for Ink's public entry module. */\n\n/** Marker on the shim's re-import of the untouched module. */\nexport const ORIGINAL_MARKER = 'termwright-original=1';\n\n/**\n * Ink's ESM entry under ordinary node_modules and Bun's versioned cache.\n * Matching the resolved path, rather than the bare specifier, is required by\n * both loader APIs used here.\n */\nexport const INK_ENTRY_PATTERN = /[\\\\/](?:ink|ink@[^\\\\/]+)[\\\\/]build[\\\\/]index\\.js$/u;\n\n/** The separately-built runtime imported by replacement module source. */\nexport const INSTRUMENT_URL = new URL('./instrument.js', import.meta.url).href;\n\nexport function shouldShim(urlOrPath: string): boolean {\n if (urlOrPath.includes(ORIGINAL_MARKER)) return false;\n return INK_ENTRY_PATTERN.test(urlOrPath.split('?')[0] ?? '');\n}\n\nexport function originalUrl(urlOrPath: string): string {\n return `${urlOrPath}${urlOrPath.includes('?') ? '&' : '?'}${ORIGINAL_MARKER}`;\n}\n\n/**\n * Forward the complete Ink namespace and shadow only `render`.\n *\n * The wrapper receives the already-marked original namespace, so the runtime\n * never imports `ink` itself and cannot recurse through the loader hook.\n */\nexport function buildShimSource(target: string, instrumentUrl = INSTRUMENT_URL): string {\n const original = JSON.stringify(originalUrl(target));\n const instrument = JSON.stringify(instrumentUrl);\n return `import * as __termwright_original from ${original};\nimport {wrapInkRender as __termwright_wrap} from ${instrument};\nexport * from ${original};\n\nexport const render = __termwright_wrap(__termwright_original);\n`;\n}\n"],"mappings":";AAGO,IAAM,kBAAkB;AAOxB,IAAM,oBAAoB;AAG1B,IAAM,iBAAiB,IAAI,IAAI,mBAAmB,YAAY,GAAG,EAAE;AAEnE,SAAS,WAAW,WAA4B;AACrD,MAAI,UAAU,SAAS,eAAe,EAAG,QAAO;AAChD,SAAO,kBAAkB,KAAK,UAAU,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE;AAC7D;AAEO,SAAS,YAAY,WAA2B;AACrD,SAAO,GAAG,SAAS,GAAG,UAAU,SAAS,GAAG,IAAI,MAAM,GAAG,GAAG,eAAe;AAC7E;AAQO,SAAS,gBAAgB,QAAgB,gBAAgB,gBAAwB;AACtF,QAAM,WAAW,KAAK,UAAU,YAAY,MAAM,CAAC;AACnD,QAAM,aAAa,KAAK,UAAU,aAAa;AAC/C,SAAO,0CAA0C,QAAQ;AAAA,mDACR,UAAU;AAAA,gBAC7C,QAAQ;AAAA;AAAA;AAAA;AAIxB;","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
import { ENV_ENDPOINT, ENV_TOKEN } from "@termwright/protocol";
|
|
3
|
+
function isInstrumented(env) {
|
|
4
|
+
const endpoint = env[ENV_ENDPOINT];
|
|
5
|
+
const token = env[ENV_TOKEN];
|
|
6
|
+
return typeof endpoint === "string" && endpoint.length > 0 && typeof token === "string" && token.length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
isInstrumented
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=chunk-LO7YF74P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["/** Runtime activation shared by the two preload entry points. */\n\nimport { ENV_ENDPOINT, ENV_TOKEN } from '@termwright/protocol';\n\n/** Runtimes into which the Ink probe can be injected. */\nexport type ProbeRuntime = 'bun' | 'node';\n\n/** Read-only environment view, so activation is testable without mutation. */\nexport type EnvSource = Readonly<Record<string, string | undefined>>;\n\n/** Both secrets are required. A partial environment remains fully dormant. */\nexport function isInstrumented(env: EnvSource): boolean {\n const endpoint = env[ENV_ENDPOINT];\n const token = env[ENV_TOKEN];\n return typeof endpoint === 'string' && endpoint.length > 0\n && typeof token === 'string' && token.length > 0;\n}\n"],"mappings":";AAEA,SAAS,cAAc,iBAAiB;AASjC,SAAS,eAAe,KAAyB;AACtD,QAAM,WAAW,IAAI,YAAY;AACjC,QAAM,QAAQ,IAAI,SAAS;AAC3B,SAAO,OAAO,aAAa,YAAY,SAAS,SAAS,KACpD,OAAO,UAAU,YAAY,MAAM,SAAS;AACnD;","names":[]}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// src/annotations.ts
|
|
2
|
+
import { validateProbeAnnotations } from "@termwright/protocol";
|
|
3
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("termwright.annotation.ink.v1");
|
|
4
|
+
function channel() {
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const present = scope[REGISTRY];
|
|
7
|
+
if (present?.entries instanceof WeakMap && present.listeners instanceof Set) {
|
|
8
|
+
return present;
|
|
9
|
+
}
|
|
10
|
+
const created = {
|
|
11
|
+
entries: /* @__PURE__ */ new WeakMap(),
|
|
12
|
+
listeners: /* @__PURE__ */ new Set()
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(scope, REGISTRY, { configurable: true, value: created });
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
function onInkAnnotationChange(handler) {
|
|
18
|
+
const listeners = channel().listeners;
|
|
19
|
+
listeners.add(handler);
|
|
20
|
+
return () => listeners.delete(handler);
|
|
21
|
+
}
|
|
22
|
+
function strings(refs, idFor, maxTargets) {
|
|
23
|
+
if (refs === void 0) return void 0;
|
|
24
|
+
if (!Array.isArray(refs)) return null;
|
|
25
|
+
const length = Object.getOwnPropertyDescriptor(refs, "length")?.value;
|
|
26
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maxTargets) return null;
|
|
27
|
+
const ids = [];
|
|
28
|
+
for (let index = 0; index < length; index += 1) {
|
|
29
|
+
try {
|
|
30
|
+
const descriptor = Object.getOwnPropertyDescriptor(refs, String(index));
|
|
31
|
+
if (descriptor === void 0 || !("value" in descriptor)) return null;
|
|
32
|
+
const ref = descriptor.value;
|
|
33
|
+
if (!(ref instanceof WeakRef)) return null;
|
|
34
|
+
const target = WeakRef.prototype.deref.call(ref);
|
|
35
|
+
if (target !== void 0) ids.push(idFor(target));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ids.length === 0 ? void 0 : ids;
|
|
41
|
+
}
|
|
42
|
+
function ownData(value, key) {
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
44
|
+
return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
|
|
45
|
+
}
|
|
46
|
+
function annotationForInkNode(node, idFor, limits) {
|
|
47
|
+
try {
|
|
48
|
+
const slot = channel().entries.get(node);
|
|
49
|
+
const value = slot?.current;
|
|
50
|
+
if (value === void 0) return void 0;
|
|
51
|
+
const role = ownData(value, "role");
|
|
52
|
+
const name = ownData(value, "name");
|
|
53
|
+
const description = ownData(value, "description");
|
|
54
|
+
const testId = ownData(value, "testId");
|
|
55
|
+
const extended = ownData(value, "extended");
|
|
56
|
+
const actions = ownData(value, "actions");
|
|
57
|
+
const labelledBy = strings(ownData(value, "labelledBy"), idFor, limits.maxRelationTargets);
|
|
58
|
+
const describedBy = strings(ownData(value, "describedBy"), idFor, limits.maxRelationTargets);
|
|
59
|
+
const candidate = {
|
|
60
|
+
...role === void 0 ? {} : { role },
|
|
61
|
+
...name === void 0 ? {} : { name },
|
|
62
|
+
...description === void 0 ? {} : { description },
|
|
63
|
+
...testId === void 0 ? {} : { testId },
|
|
64
|
+
...extended === void 0 ? {} : { extended },
|
|
65
|
+
...actions === void 0 ? {} : { actions },
|
|
66
|
+
...labelledBy === void 0 ? {} : { labelledBy },
|
|
67
|
+
...describedBy === void 0 ? {} : { describedBy }
|
|
68
|
+
};
|
|
69
|
+
if (Object.keys(candidate).length === 0) return void 0;
|
|
70
|
+
const validated = validateProbeAnnotations(candidate, limits);
|
|
71
|
+
return validated.ok ? validated.annotations : void 0;
|
|
72
|
+
} catch {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/observe.ts
|
|
78
|
+
var isElement = (node) => node.nodeName !== "#text";
|
|
79
|
+
function observeInkTree(root, options) {
|
|
80
|
+
const objects = [];
|
|
81
|
+
const ids = identityStore(root);
|
|
82
|
+
let truncated = false;
|
|
83
|
+
const visit = (node, parent, depth, ancestorHidden) => {
|
|
84
|
+
if (node === options.excluded) return;
|
|
85
|
+
if (depth > options.limits.maxDepth || objects.length >= options.limits.maxNodes) {
|
|
86
|
+
truncated = true;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const hidden = ancestorHidden || node.style?.display === "none";
|
|
90
|
+
const state = observedState(node, !hidden);
|
|
91
|
+
const annotations = annotationForInkNode(
|
|
92
|
+
node,
|
|
93
|
+
(target) => ids.idFor(target),
|
|
94
|
+
options.limits
|
|
95
|
+
);
|
|
96
|
+
const accessibility = observedAccessibility(node);
|
|
97
|
+
const geometry = geometryOf(node, options);
|
|
98
|
+
const text = isTextHost(node) ? textOf(node, options.limits.maxStringBytes) : void 0;
|
|
99
|
+
const unobservable = unobservableFor(node, geometry !== void 0, text !== void 0);
|
|
100
|
+
objects.push({
|
|
101
|
+
identity: { kind: "stable", value: ids.idFor(node) },
|
|
102
|
+
frameworkType: node.nodeName,
|
|
103
|
+
...parent === void 0 ? {} : { parent: ids.idFor(parent) },
|
|
104
|
+
...geometry === void 0 ? {} : { geometry: { intendedRect: geometry } },
|
|
105
|
+
...state === void 0 ? {} : { state },
|
|
106
|
+
...text === void 0 ? {} : { text },
|
|
107
|
+
...accessibility === void 0 ? {} : { accessibility },
|
|
108
|
+
...annotations === void 0 ? {} : { annotations },
|
|
109
|
+
unobservable
|
|
110
|
+
});
|
|
111
|
+
for (const child of node.childNodes) {
|
|
112
|
+
if (isElement(child)) visit(child, node, depth + 1, hidden);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
visit(root, void 0, 0, false);
|
|
116
|
+
return { frame: { frame: options.frame, objects }, truncated };
|
|
117
|
+
}
|
|
118
|
+
var stores = /* @__PURE__ */ new WeakMap();
|
|
119
|
+
function identityStore(root) {
|
|
120
|
+
let store = stores.get(root);
|
|
121
|
+
if (store !== void 0) return store;
|
|
122
|
+
const ids = /* @__PURE__ */ new WeakMap();
|
|
123
|
+
let nextId = 0;
|
|
124
|
+
store = {
|
|
125
|
+
idFor(node) {
|
|
126
|
+
const existing = ids.get(node);
|
|
127
|
+
if (existing !== void 0) return existing;
|
|
128
|
+
nextId += 1;
|
|
129
|
+
const id = String(nextId);
|
|
130
|
+
ids.set(node, id);
|
|
131
|
+
return id;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
stores.set(root, store);
|
|
135
|
+
return store;
|
|
136
|
+
}
|
|
137
|
+
function isTextHost(node) {
|
|
138
|
+
return node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text";
|
|
139
|
+
}
|
|
140
|
+
function observedAccessibility(node) {
|
|
141
|
+
const role = node.internal_accessibility?.role;
|
|
142
|
+
return role === void 0 ? void 0 : { role };
|
|
143
|
+
}
|
|
144
|
+
function observedState(node, displayed) {
|
|
145
|
+
const accessibility = node.internal_accessibility?.state;
|
|
146
|
+
const state = {
|
|
147
|
+
displayed,
|
|
148
|
+
...accessibility?.checked === void 0 ? {} : { checked: accessibility.checked },
|
|
149
|
+
...accessibility?.disabled === void 0 ? {} : { disabled: accessibility.disabled },
|
|
150
|
+
...accessibility?.expanded === void 0 ? {} : { expanded: accessibility.expanded },
|
|
151
|
+
...accessibility?.readonly === void 0 ? {} : { readonly: accessibility.readonly },
|
|
152
|
+
...accessibility?.selected === void 0 ? {} : { selected: accessibility.selected },
|
|
153
|
+
...accessibility?.busy === void 0 ? {} : { busy: accessibility.busy },
|
|
154
|
+
...accessibility?.multiline === void 0 ? {} : { multiline: accessibility.multiline }
|
|
155
|
+
};
|
|
156
|
+
return state;
|
|
157
|
+
}
|
|
158
|
+
function geometryOf(node, options) {
|
|
159
|
+
if (options.includeGeometry !== true || options.measureElement === void 0) return void 0;
|
|
160
|
+
if (node.nodeName === "ink-virtual-text") return void 0;
|
|
161
|
+
try {
|
|
162
|
+
const measured = options.measureElement(node);
|
|
163
|
+
if (!Number.isFinite(measured.x) || !Number.isFinite(measured.y) || !Number.isFinite(measured.width) || !Number.isFinite(measured.height) || measured.width <= 0 || measured.height <= 0) return void 0;
|
|
164
|
+
return {
|
|
165
|
+
row: Math.trunc(measured.y),
|
|
166
|
+
column: Math.trunc(measured.x),
|
|
167
|
+
width: Math.trunc(measured.width),
|
|
168
|
+
height: Math.trunc(measured.height)
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function textOf(node, maxBytes) {
|
|
175
|
+
const parts = [];
|
|
176
|
+
let bytes = 0;
|
|
177
|
+
const append = (value) => {
|
|
178
|
+
for (const codePoint of value) {
|
|
179
|
+
const size = Buffer.byteLength(codePoint, "utf8");
|
|
180
|
+
if (bytes + size > maxBytes) return;
|
|
181
|
+
parts.push(codePoint);
|
|
182
|
+
bytes += size;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
for (const child of node.childNodes) {
|
|
186
|
+
if (bytes >= maxBytes) break;
|
|
187
|
+
if (!isElement(child)) append(child.nodeValue);
|
|
188
|
+
}
|
|
189
|
+
const text = parts.join("").replace(/\s+/gu, " ").trim();
|
|
190
|
+
return text.length === 0 ? void 0 : text;
|
|
191
|
+
}
|
|
192
|
+
function unobservableFor(node, hasGeometry, hasText) {
|
|
193
|
+
const result = [
|
|
194
|
+
"focused",
|
|
195
|
+
"value",
|
|
196
|
+
"selectedIndex",
|
|
197
|
+
"textSelection",
|
|
198
|
+
"scroll",
|
|
199
|
+
"scrollExtent",
|
|
200
|
+
"visibleRect",
|
|
201
|
+
"paintOrder"
|
|
202
|
+
];
|
|
203
|
+
const state = node.internal_accessibility?.state;
|
|
204
|
+
if (state?.disabled === void 0) result.push("disabled");
|
|
205
|
+
if (state?.checked === void 0) result.push("checked");
|
|
206
|
+
if (state?.expanded === void 0) result.push("expanded");
|
|
207
|
+
if (state?.readonly === void 0) result.push("readonly");
|
|
208
|
+
if (state?.selected === void 0) result.push("selected");
|
|
209
|
+
if (state?.busy === void 0) result.push("busy");
|
|
210
|
+
if (state?.multiline === void 0) result.push("multiline");
|
|
211
|
+
if (!hasGeometry) result.push("intendedRect");
|
|
212
|
+
if (!hasText && isTextHost(node)) result.push("text");
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
function hasStaticContent(root) {
|
|
216
|
+
const stack = [root];
|
|
217
|
+
while (stack.length > 0) {
|
|
218
|
+
const node = stack.pop();
|
|
219
|
+
if (node.internal_static === true || node.staticNode !== void 0) return true;
|
|
220
|
+
for (const child of node.childNodes) if (isElement(child)) stack.push(child);
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/geometry.ts
|
|
226
|
+
function canPublishInkGeometry(options) {
|
|
227
|
+
const interactive = options.interactive ?? (!(options.inCi ?? runningInCi()) && options.stdoutIsTTY);
|
|
228
|
+
return options.alternateScreen && interactive && options.stdoutIsTTY;
|
|
229
|
+
}
|
|
230
|
+
function runningInCi() {
|
|
231
|
+
return enabledEnvironmentFlag("CI") || enabledEnvironmentFlag("CONTINUOUS_INTEGRATION");
|
|
232
|
+
}
|
|
233
|
+
function enabledEnvironmentFlag(name) {
|
|
234
|
+
const value = process.env[name];
|
|
235
|
+
return value !== void 0 && value !== "0" && value !== "false";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/version.ts
|
|
239
|
+
var PACKAGE_VERSION = "0.2.0";
|
|
240
|
+
|
|
241
|
+
// src/session.ts
|
|
242
|
+
import { recognize } from "@termwright/recognizers";
|
|
243
|
+
function probeInfo() {
|
|
244
|
+
return {
|
|
245
|
+
framework: "ink",
|
|
246
|
+
probeVersion: PACKAGE_VERSION,
|
|
247
|
+
identityKind: "stable",
|
|
248
|
+
// The optional @termwright/ink SDK writes author intent to the shared weak
|
|
249
|
+
// registry. Ink's own aria metadata travels separately as framework facts.
|
|
250
|
+
capabilities: ["stable-identity", "annotations"]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function createInkSession(options) {
|
|
254
|
+
let revision = 0;
|
|
255
|
+
let frames = 0;
|
|
256
|
+
let latestFrame = 0;
|
|
257
|
+
let staticSeen = false;
|
|
258
|
+
let stopped = false;
|
|
259
|
+
let queue = Promise.resolve();
|
|
260
|
+
const fail = () => {
|
|
261
|
+
if (stopped) return;
|
|
262
|
+
stopped = true;
|
|
263
|
+
options.channel.close();
|
|
264
|
+
};
|
|
265
|
+
const writeMarker = async (frame, marker) => {
|
|
266
|
+
await nextMacrotask();
|
|
267
|
+
if (stopped || !options.channel.isOpen) return;
|
|
268
|
+
if (frame !== latestFrame) {
|
|
269
|
+
options.channel.recordCoalescedEvent();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
await drain(options.stdout);
|
|
273
|
+
if (stopped || !options.channel.isOpen) return;
|
|
274
|
+
if (frame !== latestFrame) {
|
|
275
|
+
options.channel.recordCoalescedEvent();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
options.stdout.write(marker);
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
get revision() {
|
|
282
|
+
return revision;
|
|
283
|
+
},
|
|
284
|
+
get frames() {
|
|
285
|
+
return frames;
|
|
286
|
+
},
|
|
287
|
+
notifyRender() {
|
|
288
|
+
if (stopped) return;
|
|
289
|
+
frames += 1;
|
|
290
|
+
const frame = frames;
|
|
291
|
+
latestFrame = frame;
|
|
292
|
+
try {
|
|
293
|
+
const root = options.resolveRoot();
|
|
294
|
+
if (root === null) return;
|
|
295
|
+
staticSeen ||= hasStaticContent(root);
|
|
296
|
+
const includeGeometry = options.includeGeometry && !staticSeen;
|
|
297
|
+
const excluded = options.resolveExcluded?.();
|
|
298
|
+
const observation = observeInkTree(root, {
|
|
299
|
+
frame,
|
|
300
|
+
limits: options.channel.session.limits,
|
|
301
|
+
...excluded === void 0 ? {} : { excluded },
|
|
302
|
+
measureElement: options.measureElement,
|
|
303
|
+
includeGeometry
|
|
304
|
+
});
|
|
305
|
+
revision += 1;
|
|
306
|
+
const snapshot = recognize(observation.frame, {
|
|
307
|
+
sessionId: options.channel.session.sessionId,
|
|
308
|
+
revision,
|
|
309
|
+
columns: options.stdout.columns ?? 80,
|
|
310
|
+
rows: options.stdout.rows ?? 24,
|
|
311
|
+
framework: "ink",
|
|
312
|
+
paintOrderKnown: false,
|
|
313
|
+
maxStringBytes: options.channel.session.limits.maxStringBytes,
|
|
314
|
+
qualified: options.channel.session.protocol === "termwright/2"
|
|
315
|
+
});
|
|
316
|
+
const marker = options.channel.publish(snapshot, {
|
|
317
|
+
probeEvents: observation.frame.objects.length + (observation.frame.operations?.length ?? 0)
|
|
318
|
+
});
|
|
319
|
+
if (marker === void 0) return;
|
|
320
|
+
queue = queue.then(() => writeMarker(frame, marker)).catch(fail);
|
|
321
|
+
} catch {
|
|
322
|
+
fail();
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
async flush() {
|
|
326
|
+
await queue.catch(() => void 0);
|
|
327
|
+
},
|
|
328
|
+
stop() {
|
|
329
|
+
fail();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function nextMacrotask() {
|
|
334
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
335
|
+
}
|
|
336
|
+
function drain(stream) {
|
|
337
|
+
return new Promise((resolve) => {
|
|
338
|
+
if (stream.writableEnded || stream.destroyed) {
|
|
339
|
+
resolve();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
stream.write("", () => resolve());
|
|
344
|
+
} catch {
|
|
345
|
+
resolve();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export {
|
|
351
|
+
onInkAnnotationChange,
|
|
352
|
+
observeInkTree,
|
|
353
|
+
canPublishInkGeometry,
|
|
354
|
+
PACKAGE_VERSION,
|
|
355
|
+
probeInfo,
|
|
356
|
+
createInkSession
|
|
357
|
+
};
|
|
358
|
+
//# sourceMappingURL=chunk-Y5WYMWRU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/annotations.ts","../src/observe.ts","../src/geometry.ts","../src/version.ts","../src/session.ts"],"sourcesContent":["import type {\n ProbeAnnotations,\n ProtocolLimits,\n} from '@termwright/protocol';\nimport { validateProbeAnnotations } from '@termwright/protocol';\n\nconst REGISTRY = Symbol.for('termwright.annotation.ink.v1');\n\ninterface StoredAnnotation {\n readonly role?: unknown;\n readonly name?: unknown;\n readonly description?: unknown;\n readonly testId?: unknown;\n readonly extended?: unknown;\n readonly actions?: unknown;\n readonly labelledBy?: readonly WeakRef<object>[];\n readonly describedBy?: readonly WeakRef<object>[];\n}\n\ninterface AnnotationSlot {\n readonly current?: StoredAnnotation;\n}\n\ninterface AnnotationChannel {\n readonly entries: WeakMap<object, AnnotationSlot>;\n readonly listeners: Set<() => void>;\n}\n\nfunction channel(): AnnotationChannel {\n const scope = globalThis as Record<PropertyKey, unknown>;\n const present = scope[REGISTRY] as Partial<AnnotationChannel> | undefined;\n if (present?.entries instanceof WeakMap && present.listeners instanceof Set) {\n return present as AnnotationChannel;\n }\n const created: AnnotationChannel = {\n entries: new WeakMap<object, AnnotationSlot>(),\n listeners: new Set<() => void>(),\n };\n Object.defineProperty(scope, REGISTRY, { configurable: true, value: created });\n return created;\n}\n\n/** Re-capture after an annotation attaches to a newly reconciled host. */\nexport function onInkAnnotationChange(handler: () => void): () => void {\n const listeners = channel().listeners;\n listeners.add(handler);\n return () => listeners.delete(handler);\n}\n\nfunction strings(\n refs: unknown,\n idFor: (node: object) => string,\n maxTargets: number,\n): string[] | null | undefined {\n if (refs === undefined) return undefined;\n if (!Array.isArray(refs)) return null;\n const length = Object.getOwnPropertyDescriptor(refs, 'length')?.value;\n if (!Number.isSafeInteger(length) || length < 0 || length > maxTargets) return null;\n const ids: string[] = [];\n for (let index = 0; index < length; index += 1) {\n try {\n const descriptor = Object.getOwnPropertyDescriptor(refs, String(index));\n if (descriptor === undefined || !('value' in descriptor)) return null;\n const ref = descriptor.value;\n if (!(ref instanceof WeakRef)) return null;\n const target = WeakRef.prototype.deref.call(ref) as object | undefined;\n if (target !== undefined) ids.push(idFor(target));\n } catch {\n return null;\n }\n }\n return ids.length === 0 ? undefined : ids;\n}\n\nfunction ownData(value: object, key: keyof StoredAnnotation): unknown {\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;\n}\n\n/** Read author intent without taking a runtime dependency on the optional SDK. */\nexport function annotationForInkNode(\n node: object,\n idFor: (node: object) => string,\n limits: ProtocolLimits,\n): ProbeAnnotations | undefined {\n try {\n const slot = channel().entries.get(node);\n const value = slot?.current;\n if (value === undefined) return undefined;\n const role = ownData(value, 'role');\n const name = ownData(value, 'name');\n const description = ownData(value, 'description');\n const testId = ownData(value, 'testId');\n const extended = ownData(value, 'extended');\n const actions = ownData(value, 'actions');\n const labelledBy = strings(ownData(value, 'labelledBy'), idFor, limits.maxRelationTargets);\n const describedBy = strings(ownData(value, 'describedBy'), idFor, limits.maxRelationTargets);\n const candidate = {\n ...(role === undefined ? {} : { role }),\n ...(name === undefined ? {} : { name }),\n ...(description === undefined ? {} : { description }),\n ...(testId === undefined ? {} : { testId }),\n ...(extended === undefined ? {} : { extended }),\n ...(actions === undefined ? {} : { actions }),\n ...(labelledBy === undefined ? {} : { labelledBy }),\n ...(describedBy === undefined ? {} : { describedBy }),\n };\n if (Object.keys(candidate).length === 0) return undefined;\n const validated = validateProbeAnnotations(candidate, limits);\n return validated.ok ? validated.annotations : undefined;\n } catch {\n return undefined;\n }\n}\n","/** Ink's retained host tree to framework-neutral Probe IR. */\n\nimport type {\n ProbeAccessibilityHints,\n ProbeFrame,\n ProbeObject,\n ProbeObservedState,\n ProbeRect,\n ProbeUnobservableField,\n ProtocolLimits,\n} from '@termwright/protocol';\nimport { annotationForInkNode } from './annotations.js';\n\n/** Structural subset of Ink's DOM node. No runtime import from `ink`. */\nexport interface InkDomElement {\n readonly nodeName: 'ink-root' | 'ink-box' | 'ink-text' | 'ink-virtual-text';\n readonly childNodes: readonly InkDomNode[];\n readonly parentNode?: InkDomElement;\n readonly style?: { readonly display?: string };\n readonly internal_static?: boolean;\n readonly staticNode?: InkDomElement;\n readonly internal_accessibility?: {\n readonly role?: string;\n readonly state?: {\n readonly checked?: boolean;\n readonly disabled?: boolean;\n readonly expanded?: boolean;\n readonly readonly?: boolean;\n readonly selected?: boolean;\n readonly busy?: boolean;\n readonly multiline?: boolean;\n };\n };\n}\n\nexport interface InkTextNode {\n readonly nodeName: '#text';\n readonly nodeValue: string;\n readonly parentNode?: InkDomElement;\n}\n\nexport type InkDomNode = InkDomElement | InkTextNode;\n\n/** Public Ink measurement function, kept injectable for tests and isolation. */\nexport type MeasureElement = (\n node: InkDomElement,\n) => { readonly x: number; readonly y: number; readonly width: number; readonly height: number };\n\nexport interface ObserveInkOptions {\n readonly frame: number;\n readonly limits: ProtocolLimits;\n /** The probe's own hidden Box. It is the sole injected node and is omitted. */\n readonly excluded?: InkDomElement | null;\n readonly measureElement?: MeasureElement;\n /** Only true when live-region coordinates are proven viewport-absolute. */\n readonly includeGeometry?: boolean;\n}\n\nexport interface InkObservation {\n readonly frame: ProbeFrame;\n readonly truncated: boolean;\n}\n\nconst isElement = (node: InkDomNode): node is InkDomElement => node.nodeName !== '#text';\n\n/**\n * Observe every Ink host element, including plain unannotated layout boxes.\n *\n * Source component names do not survive Ink's reconciler. `frameworkType` is\n * therefore deliberately one of Ink's four host kinds; inventing `Button` or\n * a component stack here would be false provenance.\n */\nexport function observeInkTree(root: InkDomElement, options: ObserveInkOptions): InkObservation {\n const objects: ProbeObject[] = [];\n const ids = identityStore(root);\n let truncated = false;\n\n const visit = (\n node: InkDomElement,\n parent: InkDomElement | undefined,\n depth: number,\n ancestorHidden: boolean,\n ): void => {\n if (node === options.excluded) return;\n if (depth > options.limits.maxDepth || objects.length >= options.limits.maxNodes) {\n truncated = true;\n return;\n }\n\n const hidden = ancestorHidden || node.style?.display === 'none';\n const state = observedState(node, !hidden);\n const annotations = annotationForInkNode(\n node,\n (target) => ids.idFor(target as InkDomElement),\n options.limits,\n );\n const accessibility = observedAccessibility(node);\n const geometry = geometryOf(node, options);\n // Probe IR's `text` is the object's own text, never a descendant-derived\n // accessible name. The recognizer applies name-from-content over the tree.\n const text = isTextHost(node) ? textOf(node, options.limits.maxStringBytes) : undefined;\n const unobservable = unobservableFor(node, geometry !== undefined, text !== undefined);\n\n objects.push({\n identity: { kind: 'stable', value: ids.idFor(node) },\n frameworkType: node.nodeName,\n ...(parent === undefined ? {} : { parent: ids.idFor(parent) }),\n ...(geometry === undefined ? {} : { geometry: { intendedRect: geometry } }),\n ...(state === undefined ? {} : { state }),\n ...(text === undefined ? {} : { text }),\n ...(accessibility === undefined ? {} : { accessibility }),\n ...(annotations === undefined ? {} : { annotations }),\n unobservable,\n });\n\n for (const child of node.childNodes) {\n // Raw `#text` values are payload owned by their `ink-text` host, not a\n // fifth host kind. The text is retained on that host above.\n if (isElement(child)) visit(child, node, depth + 1, hidden);\n }\n };\n\n visit(root, undefined, 0, false);\n return { frame: { frame: options.frame, objects }, truncated };\n}\n\n/** Weak identity is stable for exactly the lifetime of Ink's host object. */\nconst stores = new WeakMap<InkDomElement, IdentityStore>();\n\ninterface IdentityStore {\n idFor(node: InkDomElement): string;\n}\n\nfunction identityStore(root: InkDomElement): IdentityStore {\n let store = stores.get(root);\n if (store !== undefined) return store;\n const ids = new WeakMap<InkDomElement, string>();\n let nextId = 0;\n store = {\n idFor(node) {\n const existing = ids.get(node);\n if (existing !== undefined) return existing;\n nextId += 1;\n const id = String(nextId);\n ids.set(node, id);\n return id;\n },\n };\n stores.set(root, store);\n return store;\n}\n\nfunction isTextHost(node: InkDomElement): boolean {\n return node.nodeName === 'ink-text' || node.nodeName === 'ink-virtual-text';\n}\n\nfunction observedAccessibility(node: InkDomElement): ProbeAccessibilityHints | undefined {\n const role = node.internal_accessibility?.role;\n return role === undefined ? undefined : { role };\n}\n\nfunction observedState(node: InkDomElement, displayed: boolean): ProbeObservedState | undefined {\n const accessibility = node.internal_accessibility?.state;\n const state: ProbeObservedState = {\n displayed,\n ...(accessibility?.checked === undefined ? {} : { checked: accessibility.checked }),\n ...(accessibility?.disabled === undefined ? {} : { disabled: accessibility.disabled }),\n ...(accessibility?.expanded === undefined ? {} : { expanded: accessibility.expanded }),\n ...(accessibility?.readonly === undefined ? {} : { readonly: accessibility.readonly }),\n ...(accessibility?.selected === undefined ? {} : { selected: accessibility.selected }),\n ...(accessibility?.busy === undefined ? {} : { busy: accessibility.busy }),\n ...(accessibility?.multiline === undefined ? {} : { multiline: accessibility.multiline }),\n };\n return state;\n}\n\nfunction geometryOf(\n node: InkDomElement,\n options: ObserveInkOptions,\n): ProbeRect | undefined {\n if (options.includeGeometry !== true || options.measureElement === undefined) return undefined;\n if (node.nodeName === 'ink-virtual-text') return undefined;\n try {\n const measured = options.measureElement(node);\n if (\n !Number.isFinite(measured.x)\n || !Number.isFinite(measured.y)\n || !Number.isFinite(measured.width)\n || !Number.isFinite(measured.height)\n || measured.width <= 0\n || measured.height <= 0\n ) return undefined;\n return {\n row: Math.trunc(measured.y),\n column: Math.trunc(measured.x),\n width: Math.trunc(measured.width),\n height: Math.trunc(measured.height),\n };\n } catch {\n return undefined;\n }\n}\n\nfunction textOf(node: InkDomElement, maxBytes: number): string | undefined {\n const parts: string[] = [];\n let bytes = 0;\n\n const append = (value: string): void => {\n for (const codePoint of value) {\n const size = Buffer.byteLength(codePoint, 'utf8');\n if (bytes + size > maxBytes) return;\n parts.push(codePoint);\n bytes += size;\n }\n };\n\n // Raw #text children are this host's payload. Nested host elements retain\n // their own ProbeObjects, so folding them in here would violate the IR's\n // own-text contract and duplicate them during name-from-content inference.\n for (const child of node.childNodes) {\n if (bytes >= maxBytes) break;\n if (!isElement(child)) append(child.nodeValue);\n }\n const text = parts.join('').replace(/\\s+/gu, ' ').trim();\n return text.length === 0 ? undefined : text;\n}\n\nfunction unobservableFor(\n node: InkDomElement,\n hasGeometry: boolean,\n hasText: boolean,\n): readonly ProbeUnobservableField[] {\n const result: ProbeUnobservableField[] = [\n 'focused',\n 'value',\n 'selectedIndex',\n 'textSelection',\n 'scroll',\n 'scrollExtent',\n 'visibleRect',\n 'paintOrder',\n ];\n const state = node.internal_accessibility?.state;\n if (state?.disabled === undefined) result.push('disabled');\n if (state?.checked === undefined) result.push('checked');\n if (state?.expanded === undefined) result.push('expanded');\n if (state?.readonly === undefined) result.push('readonly');\n if (state?.selected === undefined) result.push('selected');\n if (state?.busy === undefined) result.push('busy');\n if (state?.multiline === undefined) result.push('multiline');\n if (!hasGeometry) result.push('intendedRect');\n if (!hasText && isTextHost(node)) result.push('text');\n return result;\n}\n\n/** `<Static>` moves the live region down by an offset Ink does not expose. */\nexport function hasStaticContent(root: InkDomElement): boolean {\n const stack: InkDomElement[] = [root];\n while (stack.length > 0) {\n const node = stack.pop() as InkDomElement;\n if (node.internal_static === true || node.staticNode !== undefined) return true;\n for (const child of node.childNodes) if (isElement(child)) stack.push(child);\n }\n return false;\n}\n","/** Truthful gate for Ink's live-region coordinates. */\n\nexport interface GeometryGateOptions {\n readonly alternateScreen: boolean;\n readonly interactive?: boolean;\n readonly stdoutIsTTY: boolean;\n /** Injectable only so the default-interactivity branch is deterministic. */\n readonly inCi?: boolean;\n}\n\n/**\n * Reproduce Ink 7's `resolveInteractiveOption` and\n * `resolveAlternateScreenOption`. Layout coordinates are terminal-absolute\n * only if Ink actually entered the alternate screen on a TTY.\n */\nexport function canPublishInkGeometry(options: GeometryGateOptions): boolean {\n const interactive = options.interactive\n ?? (!(options.inCi ?? runningInCi()) && options.stdoutIsTTY);\n return options.alternateScreen && interactive && options.stdoutIsTTY;\n}\n\nfunction runningInCi(): boolean {\n return enabledEnvironmentFlag('CI') || enabledEnvironmentFlag('CONTINUOUS_INTEGRATION');\n}\n\nfunction enabledEnvironmentFlag(name: string): boolean {\n const value = process.env[name];\n return value !== undefined && value !== '0' && value !== 'false';\n}\n","/** Synchronized from package.json by scripts/sync-protocol-version.mjs. */\nexport const PACKAGE_VERSION = '0.2.0';\n","/** A committed Ink host tree to snapshot/commit/marker publication. */\n\nimport type { ProbeInfo, ProtocolLimits, SemanticSnapshot } from '@termwright/protocol';\nimport { recognize } from '@termwright/recognizers';\nimport type { ProbeChannel } from '@termwright/probe-runtime';\nimport {\n hasStaticContent,\n observeInkTree,\n type InkDomElement,\n type MeasureElement,\n} from './observe.js';\nimport { PACKAGE_VERSION } from './version.js';\n\n/** What this probe truthfully offers at handshake time. */\nexport function probeInfo(): ProbeInfo {\n return {\n framework: 'ink',\n probeVersion: PACKAGE_VERSION,\n identityKind: 'stable',\n // The optional @termwright/ink SDK writes author intent to the shared weak\n // registry. Ink's own aria metadata travels separately as framework facts.\n capabilities: ['stable-identity', 'annotations'],\n };\n}\n\nexport interface InkSessionOptions {\n readonly channel: ProbeChannel;\n readonly resolveRoot: () => InkDomElement | null;\n readonly resolveExcluded?: () => InkDomElement | null;\n readonly measureElement: MeasureElement;\n readonly stdout: NodeJS.WriteStream;\n readonly includeGeometry: boolean;\n}\n\nexport interface InkProbeSession {\n readonly revision: number;\n readonly frames: number;\n notifyRender(): void;\n /** Settle all captures queued at the time of the call. Never rejects. */\n flush(): Promise<void>;\n stop(): void;\n}\n\n/**\n * Pair each observed commit with its output bytes.\n *\n * Ink invokes `onRender` after layout and before writing. The tree is frozen\n * synchronously in that callback; deferring observation would let a microtask\n * or a throttled commit mutate the host objects before they were read. Only\n * marker placement is deferred: after Ink returns and writes, stdout is\n * drained and the authenticated marker is appended.\n */\nexport function createInkSession(options: InkSessionOptions): InkProbeSession {\n let revision = 0;\n let frames = 0;\n let latestFrame = 0;\n let staticSeen = false;\n let stopped = false;\n let queue: Promise<void> = Promise.resolve();\n\n const fail = (): void => {\n if (stopped) return;\n stopped = true;\n options.channel.close();\n };\n\n const writeMarker = async (frame: number, marker: string): Promise<void> => {\n await nextMacrotask();\n if (stopped || !options.channel.isOpen) return;\n if (frame !== latestFrame) {\n options.channel.recordCoalescedEvent();\n return;\n }\n await drain(options.stdout);\n // A newer render may have written while this drain was pending. Marker N\n // after frame N+1 bytes is actively misleading, so drop it and let the\n // newer full snapshot establish the next pairing.\n if (stopped || !options.channel.isOpen) return;\n if (frame !== latestFrame) {\n options.channel.recordCoalescedEvent();\n return;\n }\n options.stdout.write(marker);\n };\n\n return {\n get revision() {\n return revision;\n },\n get frames() {\n return frames;\n },\n notifyRender() {\n if (stopped) return;\n frames += 1;\n const frame = frames;\n // Even an unobservable/failed frame supersedes a queued old marker.\n latestFrame = frame;\n\n try {\n const root = options.resolveRoot();\n if (root === null) return;\n // Static output scrolls the live region down. Removing <Static> later\n // does not erase bytes already written above it, so loss of absolute\n // coordinates is sticky for this session.\n staticSeen ||= hasStaticContent(root);\n const includeGeometry = options.includeGeometry && !staticSeen;\n const excluded = options.resolveExcluded?.();\n const observation = observeInkTree(root, {\n frame,\n limits: options.channel.session.limits as ProtocolLimits,\n ...(excluded === undefined ? {} : { excluded }),\n measureElement: options.measureElement,\n includeGeometry,\n });\n\n revision += 1;\n const snapshot: SemanticSnapshot = recognize(observation.frame, {\n sessionId: options.channel.session.sessionId,\n revision,\n columns: options.stdout.columns ?? 80,\n rows: options.stdout.rows ?? 24,\n framework: 'ink',\n paintOrderKnown: false,\n maxStringBytes: options.channel.session.limits.maxStringBytes,\n qualified: options.channel.session.protocol === 'termwright/2',\n });\n const marker = options.channel.publish(snapshot, {\n probeEvents: observation.frame.objects.length + (observation.frame.operations?.length ?? 0),\n });\n if (marker === undefined) return;\n queue = queue.then(() => writeMarker(frame, marker)).catch(fail);\n } catch {\n fail();\n }\n },\n async flush() {\n await queue.catch(() => undefined);\n },\n stop() {\n fail();\n },\n };\n}\n\nfunction nextMacrotask(): Promise<void> {\n return new Promise((resolve) => setImmediate(resolve));\n}\n\nfunction drain(stream: NodeJS.WriteStream): Promise<void> {\n return new Promise((resolve) => {\n if (stream.writableEnded || stream.destroyed) {\n resolve();\n return;\n }\n try {\n stream.write('', () => resolve());\n } catch {\n resolve();\n }\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { P as ProbeRuntime, I as InkDomElement, M as MeasureElement } from './observe-C853n-dp.js';
|
|
2
|
+
export { E as EnvSource, a as InkDomNode, b as InkObservation, i as isInstrumented, o as observeInkTree } from './observe-C853n-dp.js';
|
|
3
|
+
import { ProbeInfo } from '@termwright/protocol';
|
|
4
|
+
import { ProbeChannel } from '@termwright/probe-runtime';
|
|
5
|
+
|
|
6
|
+
/** Build an application command with the zero-config Ink preload attached. */
|
|
7
|
+
|
|
8
|
+
/** Published preload paths; callers never have to guess package layout. */
|
|
9
|
+
declare const PROBE_ENTRIES: {
|
|
10
|
+
readonly bun: string;
|
|
11
|
+
readonly node: string;
|
|
12
|
+
};
|
|
13
|
+
interface ProbeCommand {
|
|
14
|
+
readonly command: readonly string[];
|
|
15
|
+
readonly runtime: ProbeRuntime;
|
|
16
|
+
}
|
|
17
|
+
/** Prefix a normal Node or Bun command with the matching preload flag. */
|
|
18
|
+
declare function withProbe(runtime: ProbeRuntime, argv: readonly string[]): ProbeCommand;
|
|
19
|
+
|
|
20
|
+
/** Replacement source for Ink's public entry module. */
|
|
21
|
+
/** Marker on the shim's re-import of the untouched module. */
|
|
22
|
+
declare const ORIGINAL_MARKER = "termwright-original=1";
|
|
23
|
+
/**
|
|
24
|
+
* Ink's ESM entry under ordinary node_modules and Bun's versioned cache.
|
|
25
|
+
* Matching the resolved path, rather than the bare specifier, is required by
|
|
26
|
+
* both loader APIs used here.
|
|
27
|
+
*/
|
|
28
|
+
declare const INK_ENTRY_PATTERN: RegExp;
|
|
29
|
+
/** The separately-built runtime imported by replacement module source. */
|
|
30
|
+
declare const INSTRUMENT_URL: string;
|
|
31
|
+
declare function shouldShim(urlOrPath: string): boolean;
|
|
32
|
+
declare function originalUrl(urlOrPath: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Forward the complete Ink namespace and shadow only `render`.
|
|
35
|
+
*
|
|
36
|
+
* The wrapper receives the already-marked original namespace, so the runtime
|
|
37
|
+
* never imports `ink` itself and cannot recurse through the loader hook.
|
|
38
|
+
*/
|
|
39
|
+
declare function buildShimSource(target: string, instrumentUrl?: string): string;
|
|
40
|
+
|
|
41
|
+
/** Truthful gate for Ink's live-region coordinates. */
|
|
42
|
+
interface GeometryGateOptions {
|
|
43
|
+
readonly alternateScreen: boolean;
|
|
44
|
+
readonly interactive?: boolean;
|
|
45
|
+
readonly stdoutIsTTY: boolean;
|
|
46
|
+
/** Injectable only so the default-interactivity branch is deterministic. */
|
|
47
|
+
readonly inCi?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reproduce Ink 7's `resolveInteractiveOption` and
|
|
51
|
+
* `resolveAlternateScreenOption`. Layout coordinates are terminal-absolute
|
|
52
|
+
* only if Ink actually entered the alternate screen on a TTY.
|
|
53
|
+
*/
|
|
54
|
+
declare function canPublishInkGeometry(options: GeometryGateOptions): boolean;
|
|
55
|
+
|
|
56
|
+
/** A committed Ink host tree to snapshot/commit/marker publication. */
|
|
57
|
+
|
|
58
|
+
/** What this probe truthfully offers at handshake time. */
|
|
59
|
+
declare function probeInfo(): ProbeInfo;
|
|
60
|
+
interface InkSessionOptions {
|
|
61
|
+
readonly channel: ProbeChannel;
|
|
62
|
+
readonly resolveRoot: () => InkDomElement | null;
|
|
63
|
+
readonly resolveExcluded?: () => InkDomElement | null;
|
|
64
|
+
readonly measureElement: MeasureElement;
|
|
65
|
+
readonly stdout: NodeJS.WriteStream;
|
|
66
|
+
readonly includeGeometry: boolean;
|
|
67
|
+
}
|
|
68
|
+
interface InkProbeSession {
|
|
69
|
+
readonly revision: number;
|
|
70
|
+
readonly frames: number;
|
|
71
|
+
notifyRender(): void;
|
|
72
|
+
/** Settle all captures queued at the time of the call. Never rejects. */
|
|
73
|
+
flush(): Promise<void>;
|
|
74
|
+
stop(): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pair each observed commit with its output bytes.
|
|
78
|
+
*
|
|
79
|
+
* Ink invokes `onRender` after layout and before writing. The tree is frozen
|
|
80
|
+
* synchronously in that callback; deferring observation would let a microtask
|
|
81
|
+
* or a throttled commit mutate the host objects before they were read. Only
|
|
82
|
+
* marker placement is deferred: after Ink returns and writes, stdout is
|
|
83
|
+
* drained and the authenticated marker is appended.
|
|
84
|
+
*/
|
|
85
|
+
declare function createInkSession(options: InkSessionOptions): InkProbeSession;
|
|
86
|
+
|
|
87
|
+
export { type GeometryGateOptions, INK_ENTRY_PATTERN, INSTRUMENT_URL, InkDomElement, type InkProbeSession, type InkSessionOptions, MeasureElement, ORIGINAL_MARKER, PROBE_ENTRIES, type ProbeCommand, ProbeRuntime, buildShimSource, canPublishInkGeometry, createInkSession, originalUrl, probeInfo, shouldShim, withProbe };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canPublishInkGeometry,
|
|
3
|
+
createInkSession,
|
|
4
|
+
observeInkTree,
|
|
5
|
+
probeInfo
|
|
6
|
+
} from "./chunk-Y5WYMWRU.js";
|
|
7
|
+
import {
|
|
8
|
+
INK_ENTRY_PATTERN,
|
|
9
|
+
INSTRUMENT_URL,
|
|
10
|
+
ORIGINAL_MARKER,
|
|
11
|
+
buildShimSource,
|
|
12
|
+
originalUrl,
|
|
13
|
+
shouldShim
|
|
14
|
+
} from "./chunk-IUFXTMZ7.js";
|
|
15
|
+
import {
|
|
16
|
+
isInstrumented
|
|
17
|
+
} from "./chunk-LO7YF74P.js";
|
|
18
|
+
|
|
19
|
+
// src/launch.ts
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
21
|
+
var PROBE_ENTRIES = {
|
|
22
|
+
bun: fileURLToPath(new URL("./bun-preload.js", import.meta.url)),
|
|
23
|
+
node: fileURLToPath(new URL("./node-hook.js", import.meta.url))
|
|
24
|
+
};
|
|
25
|
+
function withProbe(runtime, argv) {
|
|
26
|
+
if (argv.length === 0) throw new Error("withProbe needs an interpreter in argv");
|
|
27
|
+
const [interpreter, ...rest] = argv;
|
|
28
|
+
const flag = runtime === "bun" ? "--preload" : "--import";
|
|
29
|
+
return {
|
|
30
|
+
command: [
|
|
31
|
+
interpreter,
|
|
32
|
+
flag,
|
|
33
|
+
pathToFileURL(PROBE_ENTRIES[runtime]).href,
|
|
34
|
+
...rest
|
|
35
|
+
],
|
|
36
|
+
runtime
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
INK_ENTRY_PATTERN,
|
|
41
|
+
INSTRUMENT_URL,
|
|
42
|
+
ORIGINAL_MARKER,
|
|
43
|
+
PROBE_ENTRIES,
|
|
44
|
+
buildShimSource,
|
|
45
|
+
canPublishInkGeometry,
|
|
46
|
+
createInkSession,
|
|
47
|
+
isInstrumented,
|
|
48
|
+
observeInkTree,
|
|
49
|
+
originalUrl,
|
|
50
|
+
probeInfo,
|
|
51
|
+
shouldShim,
|
|
52
|
+
withProbe
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/launch.ts"],"sourcesContent":["/** Build an application command with the zero-config Ink preload attached. */\n\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport type { ProbeRuntime } from './runtime.js';\n\n/** Published preload paths; callers never have to guess package layout. */\nexport const PROBE_ENTRIES = {\n bun: fileURLToPath(new URL('./bun-preload.js', import.meta.url)),\n node: fileURLToPath(new URL('./node-hook.js', import.meta.url)),\n} as const;\n\nexport interface ProbeCommand {\n readonly command: readonly string[];\n readonly runtime: ProbeRuntime;\n}\n\n/** Prefix a normal Node or Bun command with the matching preload flag. */\nexport function withProbe(runtime: ProbeRuntime, argv: readonly string[]): ProbeCommand {\n if (argv.length === 0) throw new Error('withProbe needs an interpreter in argv');\n const [interpreter, ...rest] = argv as [string, ...string[]];\n const flag = runtime === 'bun' ? '--preload' : '--import';\n return {\n command: [\n interpreter,\n flag,\n pathToFileURL(PROBE_ENTRIES[runtime]).href,\n ...rest,\n ],\n runtime,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAEA,SAAS,eAAe,qBAAqB;AAItC,IAAM,gBAAgB;AAAA,EAC3B,KAAK,cAAc,IAAI,IAAI,oBAAoB,YAAY,GAAG,CAAC;AAAA,EAC/D,MAAM,cAAc,IAAI,IAAI,kBAAkB,YAAY,GAAG,CAAC;AAChE;AAQO,SAAS,UAAU,SAAuB,MAAuC;AACtF,MAAI,KAAK,WAAW,EAAG,OAAM,IAAI,MAAM,wCAAwC;AAC/E,QAAM,CAAC,aAAa,GAAG,IAAI,IAAI;AAC/B,QAAM,OAAO,YAAY,QAAQ,cAAc;AAC/C,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA,cAAc,cAAc,OAAO,CAAC,EAAE;AAAA,MACtC,GAAG;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { RenderOptions, Instance } from 'ink';
|
|
3
|
+
import { M as MeasureElement, E as EnvSource } from './observe-C853n-dp.js';
|
|
4
|
+
import '@termwright/protocol';
|
|
5
|
+
|
|
6
|
+
/** The render wrapper imported by the replacement Ink entry module. */
|
|
7
|
+
|
|
8
|
+
type InkRender = (node: ReactNode, options?: NodeJS.WriteStream | RenderOptions) => Instance;
|
|
9
|
+
/** Runtime Ink surface forwarded by the shim, kept structural to avoid cycles. */
|
|
10
|
+
interface InkModule {
|
|
11
|
+
readonly render: InkRender;
|
|
12
|
+
readonly Box: ComponentType<Record<string, unknown>>;
|
|
13
|
+
readonly measureElement: MeasureElement;
|
|
14
|
+
}
|
|
15
|
+
/** @internal Used only by the in-process component harness. */
|
|
16
|
+
interface InstrumentInkOptions {
|
|
17
|
+
readonly env?: EnvSource;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wrap the original `ink.render` while preserving every other Ink export.
|
|
21
|
+
* With no complete driver environment this function calls the original render
|
|
22
|
+
* directly, without even creating the hidden ref used by an active session.
|
|
23
|
+
*/
|
|
24
|
+
declare function wrapInkRender(ink: InkModule, options?: InstrumentInkOptions): InkRender;
|
|
25
|
+
|
|
26
|
+
export { type InkModule, type InstrumentInkOptions, wrapInkRender };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PACKAGE_VERSION,
|
|
3
|
+
canPublishInkGeometry,
|
|
4
|
+
createInkSession,
|
|
5
|
+
onInkAnnotationChange,
|
|
6
|
+
probeInfo
|
|
7
|
+
} from "./chunk-Y5WYMWRU.js";
|
|
8
|
+
import {
|
|
9
|
+
isInstrumented
|
|
10
|
+
} from "./chunk-LO7YF74P.js";
|
|
11
|
+
|
|
12
|
+
// src/instrument.ts
|
|
13
|
+
import { Stream } from "stream";
|
|
14
|
+
import { createElement, Fragment } from "react";
|
|
15
|
+
import { ENV_ENDPOINT, ENV_PROTOCOL, ENV_TOKEN, PROTOCOL_V2_ID } from "@termwright/protocol";
|
|
16
|
+
import { connectProbe } from "@termwright/probe-runtime";
|
|
17
|
+
var ADAPTER_NAME = "@termwright/probe-ink";
|
|
18
|
+
var ADAPTER_VERSION = PACKAGE_VERSION;
|
|
19
|
+
function wrapInkRender(ink, options = {}) {
|
|
20
|
+
const env = options.env ?? process.env;
|
|
21
|
+
const wrapped = (node, suppliedOptions) => {
|
|
22
|
+
if (!isInstrumented(env)) return ink.render(node, suppliedOptions);
|
|
23
|
+
try {
|
|
24
|
+
return instrumentedRender(ink, node, suppliedOptions, env);
|
|
25
|
+
} catch {
|
|
26
|
+
return ink.render(node, suppliedOptions);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(wrapped, "__termwright__", { value: true });
|
|
30
|
+
return wrapped;
|
|
31
|
+
}
|
|
32
|
+
function instrumentedRender(ink, node, suppliedOptions, env) {
|
|
33
|
+
const options = normalizeOptions(suppliedOptions);
|
|
34
|
+
const stdout = options.stdout ?? process.stdout;
|
|
35
|
+
const probeRef = { current: null };
|
|
36
|
+
const state = {
|
|
37
|
+
channel: null,
|
|
38
|
+
session: null
|
|
39
|
+
};
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const releaseAnnotations = onInkAnnotationChange(() => state.session?.notifyRender());
|
|
42
|
+
const wrap = (child) => createElement(
|
|
43
|
+
Fragment,
|
|
44
|
+
null,
|
|
45
|
+
createElement(ink.Box, { ref: probeRef, display: "none" }),
|
|
46
|
+
child
|
|
47
|
+
);
|
|
48
|
+
const userOnRender = options.onRender;
|
|
49
|
+
const instance = ink.render(wrap(node), {
|
|
50
|
+
...options,
|
|
51
|
+
onRender(metrics) {
|
|
52
|
+
try {
|
|
53
|
+
state.session?.notifyRender();
|
|
54
|
+
} catch {
|
|
55
|
+
state.session?.stop();
|
|
56
|
+
}
|
|
57
|
+
userOnRender?.(metrics);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
const includeGeometry = canPublishInkGeometry({
|
|
61
|
+
alternateScreen: options.alternateScreen === true,
|
|
62
|
+
...options.interactive === void 0 ? {} : { interactive: options.interactive },
|
|
63
|
+
stdoutIsTTY: stdout.isTTY === true
|
|
64
|
+
});
|
|
65
|
+
const baseCapabilities = includeGeometry ? ["tree", "bounds", "absolute-bounds", "states", "actions", "render-revisions"] : ["tree", "states", "actions", "render-revisions"];
|
|
66
|
+
const qualified = env[ENV_PROTOCOL] === PROTOCOL_V2_ID;
|
|
67
|
+
const capabilities = qualified ? [...baseCapabilities, "qualified-observations"] : baseCapabilities;
|
|
68
|
+
const connection = connectProbe({
|
|
69
|
+
endpoint: env[ENV_ENDPOINT],
|
|
70
|
+
token: env[ENV_TOKEN],
|
|
71
|
+
probe: probeInfo(),
|
|
72
|
+
capabilities,
|
|
73
|
+
adapterName: ADAPTER_NAME,
|
|
74
|
+
adapterVersion: ADAPTER_VERSION,
|
|
75
|
+
...qualified ? { protocol: PROTOCOL_V2_ID } : {}
|
|
76
|
+
}).then(async (channel) => {
|
|
77
|
+
if (channel === null || disposed) {
|
|
78
|
+
channel?.close();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
state.channel = channel;
|
|
82
|
+
state.session = createInkSession({
|
|
83
|
+
channel,
|
|
84
|
+
resolveRoot: () => probeRef.current?.parentNode ?? null,
|
|
85
|
+
resolveExcluded: () => probeRef.current,
|
|
86
|
+
measureElement: ink.measureElement,
|
|
87
|
+
stdout,
|
|
88
|
+
includeGeometry
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
await instance.waitUntilRenderFlush();
|
|
92
|
+
} catch {
|
|
93
|
+
state.session.stop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!disposed && state.session.frames === 0) state.session.notifyRender();
|
|
97
|
+
}).catch(() => void 0);
|
|
98
|
+
const stop = () => {
|
|
99
|
+
if (disposed) return;
|
|
100
|
+
disposed = true;
|
|
101
|
+
releaseAnnotations();
|
|
102
|
+
state.session?.stop();
|
|
103
|
+
state.channel?.close();
|
|
104
|
+
};
|
|
105
|
+
void instance.waitUntilExit().catch(() => void 0).then(async () => {
|
|
106
|
+
await connection;
|
|
107
|
+
await state.session?.flush();
|
|
108
|
+
stop();
|
|
109
|
+
}).catch(stop);
|
|
110
|
+
return {
|
|
111
|
+
...instance,
|
|
112
|
+
rerender(next) {
|
|
113
|
+
instance.rerender(wrap(next));
|
|
114
|
+
},
|
|
115
|
+
unmount(error) {
|
|
116
|
+
return instance.unmount(error);
|
|
117
|
+
},
|
|
118
|
+
cleanup() {
|
|
119
|
+
stop();
|
|
120
|
+
instance.cleanup();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function normalizeOptions(supplied) {
|
|
125
|
+
if (supplied === void 0) return {};
|
|
126
|
+
if (supplied instanceof Stream) {
|
|
127
|
+
return { stdout: supplied };
|
|
128
|
+
}
|
|
129
|
+
return supplied;
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
wrapInkRender
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=instrument.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/instrument.ts"],"sourcesContent":["/** The render wrapper imported by the replacement Ink entry module. */\n\nimport { Stream } from 'node:stream';\nimport { createElement, Fragment, type ComponentType, type ReactNode } from 'react';\nimport type { DOMElement, Instance, RenderOptions } from 'ink';\nimport type { AdapterCapability } from '@termwright/protocol';\nimport { ENV_ENDPOINT, ENV_PROTOCOL, ENV_TOKEN, PROTOCOL_V2_ID } from '@termwright/protocol';\nimport { connectProbe, type ProbeChannel } from '@termwright/probe-runtime';\nimport { canPublishInkGeometry } from './geometry.js';\nimport type { InkDomElement, MeasureElement } from './observe.js';\nimport { createInkSession, probeInfo, type InkProbeSession } from './session.js';\nimport { isInstrumented } from './runtime.js';\nimport type { EnvSource } from './runtime.js';\nimport { PACKAGE_VERSION } from './version.js';\nimport { onInkAnnotationChange } from './annotations.js';\n\nconst ADAPTER_NAME = '@termwright/probe-ink';\nconst ADAPTER_VERSION = PACKAGE_VERSION;\n\ntype InkRender = (\n node: ReactNode,\n options?: NodeJS.WriteStream | RenderOptions,\n) => Instance;\n\n/** Runtime Ink surface forwarded by the shim, kept structural to avoid cycles. */\nexport interface InkModule {\n readonly render: InkRender;\n readonly Box: ComponentType<Record<string, unknown>>;\n readonly measureElement: MeasureElement;\n}\n\n/** @internal Used only by the in-process component harness. */\nexport interface InstrumentInkOptions {\n readonly env?: EnvSource;\n}\n\n/**\n * Wrap the original `ink.render` while preserving every other Ink export.\n * With no complete driver environment this function calls the original render\n * directly, without even creating the hidden ref used by an active session.\n */\nexport function wrapInkRender(ink: InkModule, options: InstrumentInkOptions = {}): InkRender {\n const env = options.env ?? process.env;\n const wrapped: InkRender = (node, suppliedOptions) => {\n if (!isInstrumented(env)) return ink.render(node, suppliedOptions);\n\n try {\n return instrumentedRender(ink, node, suppliedOptions, env);\n } catch {\n // Setup failures are probe failures. The application still gets its\n // ordinary render rather than inheriting our exception.\n return ink.render(node, suppliedOptions);\n }\n };\n Object.defineProperty(wrapped, '__termwright__', { value: true });\n return wrapped;\n}\n\nfunction instrumentedRender(\n ink: InkModule,\n node: ReactNode,\n suppliedOptions: NodeJS.WriteStream | RenderOptions | undefined,\n env: EnvSource,\n): Instance {\n const options = normalizeOptions(suppliedOptions);\n const stdout = options.stdout ?? process.stdout;\n const probeRef: { current: DOMElement | null } = { current: null };\n const state: { channel: ProbeChannel | null; session: InkProbeSession | null } = {\n channel: null,\n session: null,\n };\n let disposed = false;\n const releaseAnnotations = onInkAnnotationChange(() => state.session?.notifyRender());\n\n const wrap = (child: ReactNode): ReactNode => createElement(\n Fragment,\n null,\n createElement(ink.Box, { ref: probeRef, display: 'none' }),\n child,\n );\n\n const userOnRender = options.onRender;\n const instance = ink.render(wrap(node), {\n ...options,\n onRender(metrics) {\n try {\n // Freeze the committed host tree before an application callback can\n // synchronously schedule or flush another update.\n state.session?.notifyRender();\n } catch {\n state.session?.stop();\n }\n userOnRender?.(metrics);\n },\n });\n\n const includeGeometry = canPublishInkGeometry({\n alternateScreen: options.alternateScreen === true,\n ...(options.interactive === undefined ? {} : { interactive: options.interactive }),\n stdoutIsTTY: stdout.isTTY === true,\n });\n const baseCapabilities: readonly AdapterCapability[] = includeGeometry\n ? ['tree', 'bounds', 'absolute-bounds', 'states', 'actions', 'render-revisions']\n : ['tree', 'states', 'actions', 'render-revisions'];\n const qualified = env[ENV_PROTOCOL] === PROTOCOL_V2_ID;\n const capabilities: readonly AdapterCapability[] = qualified\n ? [...baseCapabilities, 'qualified-observations']\n : baseCapabilities;\n\n const connection = connectProbe({\n endpoint: env[ENV_ENDPOINT] as string,\n token: env[ENV_TOKEN] as string,\n probe: probeInfo(),\n capabilities,\n adapterName: ADAPTER_NAME,\n adapterVersion: ADAPTER_VERSION,\n ...(qualified ? { protocol: PROTOCOL_V2_ID } : {}),\n })\n .then(async (channel) => {\n if (channel === null || disposed) {\n channel?.close();\n return;\n }\n state.channel = channel;\n state.session = createInkSession({\n channel,\n resolveRoot: () => (probeRef.current?.parentNode as InkDomElement | undefined) ?? null,\n resolveExcluded: () => probeRef.current as InkDomElement | null,\n measureElement: ink.measureElement,\n stdout,\n includeGeometry,\n });\n // The first commit may have beaten the handshake, but the live host tree\n // can already contain a throttled commit whose bytes are not on screen.\n // Flush Ink first. If that emits onRender, the newly-installed session\n // captures it there; otherwise the stable current tree is a safe catch-up.\n try {\n await instance.waitUntilRenderFlush();\n } catch {\n state.session.stop();\n return;\n }\n if (!disposed && state.session.frames === 0) state.session.notifyRender();\n })\n .catch(() => undefined);\n\n const stop = (): void => {\n if (disposed) return;\n disposed = true;\n releaseAnnotations();\n state.session?.stop();\n state.channel?.close();\n };\n\n // Natural `useApp().exit()` does not call our wrapped cleanup. Await the\n // attach attempt and the exact publication queue instead of guessing a\n // teardown delay; a slow stdout must not lose its final marker.\n void instance.waitUntilExit()\n .catch(() => undefined)\n .then(async () => {\n await connection;\n await state.session?.flush();\n stop();\n })\n .catch(stop);\n\n return {\n ...instance,\n rerender(next) {\n instance.rerender(wrap(next));\n },\n unmount(error?: unknown) {\n return instance.unmount(error as Parameters<Instance['unmount']>[0]);\n },\n cleanup() {\n stop();\n instance.cleanup();\n },\n };\n}\n\nfunction normalizeOptions(\n supplied: NodeJS.WriteStream | RenderOptions | undefined,\n): RenderOptions {\n if (supplied === undefined) return {};\n // Match Ink's own `getOptions` test exactly. A merely stream-shaped options\n // object must not gain different semantics only because the probe is active.\n if (supplied instanceof Stream) {\n return { stdout: supplied as NodeJS.WriteStream };\n }\n return supplied as RenderOptions;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Node entry: `node --import @termwright/probe-ink/node-hook app.js`. */
|
|
2
|
+
type LoadResult = {
|
|
3
|
+
format?: string | null;
|
|
4
|
+
source?: string | ArrayBufferView | undefined;
|
|
5
|
+
shortCircuit?: boolean;
|
|
6
|
+
};
|
|
7
|
+
/** Install the best loader API available on the current Node 22+ release. */
|
|
8
|
+
declare function installNodeHook(env?: NodeJS.ProcessEnv): 'sync' | 'thread' | null;
|
|
9
|
+
/** Off-thread loader export for early Node 22 releases. */
|
|
10
|
+
declare function loadHook(url: string, context: unknown, nextLoad: (url: string, context: unknown) => Promise<LoadResult>): Promise<LoadResult>;
|
|
11
|
+
|
|
12
|
+
declare const NODE_HOOK_PATH: string;
|
|
13
|
+
|
|
14
|
+
export { NODE_HOOK_PATH, installNodeHook, loadHook as load, loadHook };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildShimSource,
|
|
3
|
+
shouldShim
|
|
4
|
+
} from "./chunk-IUFXTMZ7.js";
|
|
5
|
+
import {
|
|
6
|
+
isInstrumented
|
|
7
|
+
} from "./chunk-LO7YF74P.js";
|
|
8
|
+
|
|
9
|
+
// src/node-hook.ts
|
|
10
|
+
import * as nodeModule from "module";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
function syncLoad(url, context, nextLoad) {
|
|
13
|
+
if (!shouldShim(url)) return nextLoad(url, context);
|
|
14
|
+
return { format: "module", shortCircuit: true, source: buildShimSource(url) };
|
|
15
|
+
}
|
|
16
|
+
function installNodeHook(env = process.env) {
|
|
17
|
+
if (!isInstrumented(env)) return null;
|
|
18
|
+
const registerHooks2 = nodeModule.registerHooks;
|
|
19
|
+
if (typeof registerHooks2 === "function") {
|
|
20
|
+
registerHooks2({ load: syncLoad });
|
|
21
|
+
return "sync";
|
|
22
|
+
}
|
|
23
|
+
nodeModule.register(new URL(import.meta.url), { parentURL: new URL(import.meta.url) });
|
|
24
|
+
return "thread";
|
|
25
|
+
}
|
|
26
|
+
async function loadHook(url, context, nextLoad) {
|
|
27
|
+
if (!shouldShim(url)) return nextLoad(url, context);
|
|
28
|
+
return { format: "module", shortCircuit: true, source: buildShimSource(url) };
|
|
29
|
+
}
|
|
30
|
+
var NODE_HOOK_PATH = fileURLToPath(import.meta.url);
|
|
31
|
+
installNodeHook();
|
|
32
|
+
export {
|
|
33
|
+
NODE_HOOK_PATH,
|
|
34
|
+
installNodeHook,
|
|
35
|
+
loadHook as load,
|
|
36
|
+
loadHook
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=node-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node-hook.ts"],"sourcesContent":["/** Node entry: `node --import @termwright/probe-ink/node-hook app.js`. */\n\nimport * as nodeModule from 'node:module';\nimport { fileURLToPath } from 'node:url';\nimport { buildShimSource, shouldShim } from './shim.js';\nimport { isInstrumented } from './runtime.js';\n\ntype LoadResult = {\n format?: string | null;\n source?: string | ArrayBufferView | undefined;\n shortCircuit?: boolean;\n};\ntype NextLoad = (url: string, context: unknown) => LoadResult;\n\nfunction syncLoad(url: string, context: unknown, nextLoad: NextLoad): LoadResult {\n if (!shouldShim(url)) return nextLoad(url, context);\n return { format: 'module', shortCircuit: true, source: buildShimSource(url) };\n}\n\n/** Install the best loader API available on the current Node 22+ release. */\nexport function installNodeHook(env: NodeJS.ProcessEnv = process.env): 'sync' | 'thread' | null {\n if (!isInstrumented(env)) return null;\n // A named ESM import of `registerHooks` makes early Node 22 fail while\n // instantiating this module, before a feature check can run. Namespace access\n // is the compatibility boundary: 22.9 simply yields `undefined`.\n const registerHooks = (nodeModule as typeof nodeModule & {\n readonly registerHooks?: (hooks: { load: never }) => unknown;\n }).registerHooks;\n if (typeof registerHooks === 'function') {\n registerHooks({ load: syncLoad as never });\n return 'sync';\n }\n nodeModule.register(new URL(import.meta.url), { parentURL: new URL(import.meta.url) });\n return 'thread';\n}\n\n/** Off-thread loader export for early Node 22 releases. */\nexport async function loadHook(\n url: string,\n context: unknown,\n nextLoad: (url: string, context: unknown) => Promise<LoadResult>,\n): Promise<LoadResult> {\n if (!shouldShim(url)) return nextLoad(url, context);\n return { format: 'module', shortCircuit: true, source: buildShimSource(url) };\n}\n\nexport { loadHook as load };\nexport const NODE_HOOK_PATH = fileURLToPath(import.meta.url);\n\ninstallNodeHook();\n"],"mappings":";;;;;;;;;AAEA,YAAY,gBAAgB;AAC5B,SAAS,qBAAqB;AAW9B,SAAS,SAAS,KAAa,SAAkB,UAAgC;AAC/E,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO,SAAS,KAAK,OAAO;AAClD,SAAO,EAAE,QAAQ,UAAU,cAAc,MAAM,QAAQ,gBAAgB,GAAG,EAAE;AAC9E;AAGO,SAAS,gBAAgB,MAAyB,QAAQ,KAA+B;AAC9F,MAAI,CAAC,eAAe,GAAG,EAAG,QAAO;AAIjC,QAAMA,iBAEH;AACH,MAAI,OAAOA,mBAAkB,YAAY;AACvC,IAAAA,eAAc,EAAE,MAAM,SAAkB,CAAC;AACzC,WAAO;AAAA,EACT;AACA,EAAW,oBAAS,IAAI,IAAI,YAAY,GAAG,GAAG,EAAE,WAAW,IAAI,IAAI,YAAY,GAAG,EAAE,CAAC;AACrF,SAAO;AACT;AAGA,eAAsB,SACpB,KACA,SACA,UACqB;AACrB,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO,SAAS,KAAK,OAAO;AAClD,SAAO,EAAE,QAAQ,UAAU,cAAc,MAAM,QAAQ,gBAAgB,GAAG,EAAE;AAC9E;AAGO,IAAM,iBAAiB,cAAc,YAAY,GAAG;AAE3D,gBAAgB;","names":["registerHooks"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ProbeFrame, ProtocolLimits } from '@termwright/protocol';
|
|
2
|
+
|
|
3
|
+
/** Runtime activation shared by the two preload entry points. */
|
|
4
|
+
/** Runtimes into which the Ink probe can be injected. */
|
|
5
|
+
type ProbeRuntime = 'bun' | 'node';
|
|
6
|
+
/** Read-only environment view, so activation is testable without mutation. */
|
|
7
|
+
type EnvSource = Readonly<Record<string, string | undefined>>;
|
|
8
|
+
/** Both secrets are required. A partial environment remains fully dormant. */
|
|
9
|
+
declare function isInstrumented(env: EnvSource): boolean;
|
|
10
|
+
|
|
11
|
+
/** Ink's retained host tree to framework-neutral Probe IR. */
|
|
12
|
+
|
|
13
|
+
/** Structural subset of Ink's DOM node. No runtime import from `ink`. */
|
|
14
|
+
interface InkDomElement {
|
|
15
|
+
readonly nodeName: 'ink-root' | 'ink-box' | 'ink-text' | 'ink-virtual-text';
|
|
16
|
+
readonly childNodes: readonly InkDomNode[];
|
|
17
|
+
readonly parentNode?: InkDomElement;
|
|
18
|
+
readonly style?: {
|
|
19
|
+
readonly display?: string;
|
|
20
|
+
};
|
|
21
|
+
readonly internal_static?: boolean;
|
|
22
|
+
readonly staticNode?: InkDomElement;
|
|
23
|
+
readonly internal_accessibility?: {
|
|
24
|
+
readonly role?: string;
|
|
25
|
+
readonly state?: {
|
|
26
|
+
readonly checked?: boolean;
|
|
27
|
+
readonly disabled?: boolean;
|
|
28
|
+
readonly expanded?: boolean;
|
|
29
|
+
readonly readonly?: boolean;
|
|
30
|
+
readonly selected?: boolean;
|
|
31
|
+
readonly busy?: boolean;
|
|
32
|
+
readonly multiline?: boolean;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
interface InkTextNode {
|
|
37
|
+
readonly nodeName: '#text';
|
|
38
|
+
readonly nodeValue: string;
|
|
39
|
+
readonly parentNode?: InkDomElement;
|
|
40
|
+
}
|
|
41
|
+
type InkDomNode = InkDomElement | InkTextNode;
|
|
42
|
+
/** Public Ink measurement function, kept injectable for tests and isolation. */
|
|
43
|
+
type MeasureElement = (node: InkDomElement) => {
|
|
44
|
+
readonly x: number;
|
|
45
|
+
readonly y: number;
|
|
46
|
+
readonly width: number;
|
|
47
|
+
readonly height: number;
|
|
48
|
+
};
|
|
49
|
+
interface ObserveInkOptions {
|
|
50
|
+
readonly frame: number;
|
|
51
|
+
readonly limits: ProtocolLimits;
|
|
52
|
+
/** The probe's own hidden Box. It is the sole injected node and is omitted. */
|
|
53
|
+
readonly excluded?: InkDomElement | null;
|
|
54
|
+
readonly measureElement?: MeasureElement;
|
|
55
|
+
/** Only true when live-region coordinates are proven viewport-absolute. */
|
|
56
|
+
readonly includeGeometry?: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface InkObservation {
|
|
59
|
+
readonly frame: ProbeFrame;
|
|
60
|
+
readonly truncated: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Observe every Ink host element, including plain unannotated layout boxes.
|
|
64
|
+
*
|
|
65
|
+
* Source component names do not survive Ink's reconciler. `frameworkType` is
|
|
66
|
+
* therefore deliberately one of Ink's four host kinds; inventing `Button` or
|
|
67
|
+
* a component stack here would be false provenance.
|
|
68
|
+
*/
|
|
69
|
+
declare function observeInkTree(root: InkDomElement, options: ObserveInkOptions): InkObservation;
|
|
70
|
+
|
|
71
|
+
export { type EnvSource as E, type InkDomElement as I, type MeasureElement as M, type ProbeRuntime as P, type InkDomNode as a, type InkObservation as b, isInstrumented as i, observeInkTree as o };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@termwright/probe-ink",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "zero-config Ink 7 probe: render interception and host-tree observation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Gorce-AI/termwright.git",
|
|
9
|
+
"directory": "packages/probe-ink"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./node-hook": {
|
|
21
|
+
"types": "./dist/node-hook.d.ts",
|
|
22
|
+
"import": "./dist/node-hook.js"
|
|
23
|
+
},
|
|
24
|
+
"./bun-preload": {
|
|
25
|
+
"types": "./dist/bun-preload.d.ts",
|
|
26
|
+
"import": "./dist/bun-preload.js"
|
|
27
|
+
},
|
|
28
|
+
"./internal/testing": {
|
|
29
|
+
"types": "./dist/instrument.d.ts",
|
|
30
|
+
"import": "./dist/instrument.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@termwright/protocol": "0.2.0",
|
|
38
|
+
"@termwright/probe-runtime": "0.2.0",
|
|
39
|
+
"@termwright/recognizers": "^0.2.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"ink": ">=7.1.0 <8",
|
|
43
|
+
"react": ">=19.2.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/react": "^19.2.18",
|
|
47
|
+
"ink": "^7.1.1",
|
|
48
|
+
"react": "^19.2.8",
|
|
49
|
+
"@termwright/ink": "0.2.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup src/index.ts src/node-hook.ts src/bun-preload.ts src/instrument.ts --format esm --dts --sourcemap --clean",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
}
|
|
56
|
+
}
|