@nwire/telemetry 0.13.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/dist/auto-install.d.ts +30 -0
- package/dist/auto-install.js +48 -0
- package/dist/file-telemetry-reporter.d.ts +42 -0
- package/dist/file-telemetry-reporter.js +128 -0
- package/dist/telemetry.d.ts +25 -0
- package/dist/telemetry.js +31 -0
- package/dist/topology-capture.d.ts +151 -0
- package/dist/topology-capture.js +145 -0
- package/dist/topology-emit.d.ts +54 -0
- package/dist/topology-emit.js +73 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
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.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev auto-install — persist telemetry runs without any user code.
|
|
3
|
+
*
|
|
4
|
+
* When an app boots inside a project working directory, we install the local
|
|
5
|
+
* file reporter by default so every run lands in `.nwire/telemetry/*.jsonl`
|
|
6
|
+
* and shows up in Studio history. This is userland-unaware: no plugin, no
|
|
7
|
+
* config — the composition root (`createApp`) calls this once.
|
|
8
|
+
*
|
|
9
|
+
* "Project cwd" is detected two ways, in order:
|
|
10
|
+
* 1. `NWIRE_CWD` env var set — the dev / Studio host exports this. Its value
|
|
11
|
+
* is the project root to write under.
|
|
12
|
+
* 2. A `.nwire` directory present in `process.cwd()` — a scanned project.
|
|
13
|
+
*
|
|
14
|
+
* It is deliberately gated OFF when neither holds (a bare unit test, a library
|
|
15
|
+
* import) and when running under Vitest (`VITEST` / `NODE_ENV=test`), so the
|
|
16
|
+
* test suite never writes stray run files. Returns the detach when installed,
|
|
17
|
+
* or `undefined` when it declined to install.
|
|
18
|
+
*/
|
|
19
|
+
import { type Runtime } from "@nwire/runtime";
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the project root to persist telemetry under, or `undefined` when the
|
|
22
|
+
* process is not running inside a project (so auto-install should decline).
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveProjectCwd(env?: NodeJS.ProcessEnv): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Install the local file reporter on `runtime` when a project cwd is detected.
|
|
27
|
+
* No-op (returns `undefined`) otherwise. Safe to call unconditionally from the
|
|
28
|
+
* composition root.
|
|
29
|
+
*/
|
|
30
|
+
export declare function autoInstallFileReporter(runtime: Runtime, env?: NodeJS.ProcessEnv): (() => void) | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev auto-install — persist telemetry runs without any user code.
|
|
3
|
+
*
|
|
4
|
+
* When an app boots inside a project working directory, we install the local
|
|
5
|
+
* file reporter by default so every run lands in `.nwire/telemetry/*.jsonl`
|
|
6
|
+
* and shows up in Studio history. This is userland-unaware: no plugin, no
|
|
7
|
+
* config — the composition root (`createApp`) calls this once.
|
|
8
|
+
*
|
|
9
|
+
* "Project cwd" is detected two ways, in order:
|
|
10
|
+
* 1. `NWIRE_CWD` env var set — the dev / Studio host exports this. Its value
|
|
11
|
+
* is the project root to write under.
|
|
12
|
+
* 2. A `.nwire` directory present in `process.cwd()` — a scanned project.
|
|
13
|
+
*
|
|
14
|
+
* It is deliberately gated OFF when neither holds (a bare unit test, a library
|
|
15
|
+
* import) and when running under Vitest (`VITEST` / `NODE_ENV=test`), so the
|
|
16
|
+
* test suite never writes stray run files. Returns the detach when installed,
|
|
17
|
+
* or `undefined` when it declined to install.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { installTelemetryReporter } from "@nwire/runtime";
|
|
22
|
+
import { fileTelemetryReporter } from "./file-telemetry-reporter.js";
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the project root to persist telemetry under, or `undefined` when the
|
|
25
|
+
* process is not running inside a project (so auto-install should decline).
|
|
26
|
+
*/
|
|
27
|
+
export function resolveProjectCwd(env = process.env) {
|
|
28
|
+
// Never auto-write during the test suite.
|
|
29
|
+
if (env.VITEST || env.NODE_ENV === "test")
|
|
30
|
+
return undefined;
|
|
31
|
+
if (env.NWIRE_CWD)
|
|
32
|
+
return env.NWIRE_CWD;
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
if (existsSync(join(cwd, ".nwire")))
|
|
35
|
+
return cwd;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Install the local file reporter on `runtime` when a project cwd is detected.
|
|
40
|
+
* No-op (returns `undefined`) otherwise. Safe to call unconditionally from the
|
|
41
|
+
* composition root.
|
|
42
|
+
*/
|
|
43
|
+
export function autoInstallFileReporter(runtime, env = process.env) {
|
|
44
|
+
const dir = resolveProjectCwd(env);
|
|
45
|
+
if (!dir)
|
|
46
|
+
return undefined;
|
|
47
|
+
return installTelemetryReporter(runtime, fileTelemetryReporter({ dir }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fileTelemetryReporter` — the local disk telemetry sink.
|
|
3
|
+
*
|
|
4
|
+
* Appends every record from `runtime.onTelemetry` (via
|
|
5
|
+
* `installTelemetryReporter`) as one JSON line to
|
|
6
|
+
* `<dir>/.nwire/telemetry/<runId>.jsonl`. One file per run; `runId` defaults
|
|
7
|
+
* to a sortable timestamp id minted at construction so successive runs sort
|
|
8
|
+
* lexically. On construction it prunes the telemetry directory so only the
|
|
9
|
+
* newest `maxRuns` run files survive — restarts don't accumulate forever.
|
|
10
|
+
*
|
|
11
|
+
* This is the "local" driver of the telemetry-sink lane: no ingestion server,
|
|
12
|
+
* no network — the dev/Studio host tails these JSONL files to render run
|
|
13
|
+
* history. The cloud driver (OTLP → Vector → Greptime) is
|
|
14
|
+
* `@nwire/telemetry-otel`.
|
|
15
|
+
*
|
|
16
|
+
* Writes are buffered through a Node write stream so a record push never blocks
|
|
17
|
+
* dispatch on a syscall; `flush()` drains the buffer, `close()` ends the stream.
|
|
18
|
+
*/
|
|
19
|
+
import type { TelemetryReporter } from "@nwire/runtime";
|
|
20
|
+
export interface FileTelemetryReporterOptions {
|
|
21
|
+
/** Project root. The reporter writes under `<dir>/.nwire/telemetry/`. */
|
|
22
|
+
readonly dir: string;
|
|
23
|
+
/**
|
|
24
|
+
* Run identifier — becomes the `<runId>.jsonl` filename. Defaults to a
|
|
25
|
+
* sortable timestamp id minted at construction (see {@link generateRunId}).
|
|
26
|
+
*/
|
|
27
|
+
readonly runId?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Keep only the newest N run files; older ones are pruned at construction.
|
|
30
|
+
* Default 20.
|
|
31
|
+
*/
|
|
32
|
+
readonly maxRuns?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sortable run id: an ISO-8601 timestamp with the colons / dot stripped so it
|
|
36
|
+
* is filesystem-safe and still lexically sorts by time, plus a short random
|
|
37
|
+
* suffix so two runs started in the same millisecond don't collide.
|
|
38
|
+
*
|
|
39
|
+
* 2026-06-18T09-41-12-307Z-a1b2c3
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateRunId(now?: Date): string;
|
|
42
|
+
export declare function fileTelemetryReporter(opts: FileTelemetryReporterOptions): TelemetryReporter;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fileTelemetryReporter` — the local disk telemetry sink.
|
|
3
|
+
*
|
|
4
|
+
* Appends every record from `runtime.onTelemetry` (via
|
|
5
|
+
* `installTelemetryReporter`) as one JSON line to
|
|
6
|
+
* `<dir>/.nwire/telemetry/<runId>.jsonl`. One file per run; `runId` defaults
|
|
7
|
+
* to a sortable timestamp id minted at construction so successive runs sort
|
|
8
|
+
* lexically. On construction it prunes the telemetry directory so only the
|
|
9
|
+
* newest `maxRuns` run files survive — restarts don't accumulate forever.
|
|
10
|
+
*
|
|
11
|
+
* This is the "local" driver of the telemetry-sink lane: no ingestion server,
|
|
12
|
+
* no network — the dev/Studio host tails these JSONL files to render run
|
|
13
|
+
* history. The cloud driver (OTLP → Vector → Greptime) is
|
|
14
|
+
* `@nwire/telemetry-otel`.
|
|
15
|
+
*
|
|
16
|
+
* Writes are buffered through a Node write stream so a record push never blocks
|
|
17
|
+
* dispatch on a syscall; `flush()` drains the buffer, `close()` ends the stream.
|
|
18
|
+
*/
|
|
19
|
+
import { appendFileSync, createWriteStream, mkdirSync, readdirSync, rmSync, } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
const TELEMETRY_SUBDIR = join(".nwire", "telemetry");
|
|
22
|
+
const DEFAULT_MAX_RUNS = 20;
|
|
23
|
+
/**
|
|
24
|
+
* Sortable run id: an ISO-8601 timestamp with the colons / dot stripped so it
|
|
25
|
+
* is filesystem-safe and still lexically sorts by time, plus a short random
|
|
26
|
+
* suffix so two runs started in the same millisecond don't collide.
|
|
27
|
+
*
|
|
28
|
+
* 2026-06-18T09-41-12-307Z-a1b2c3
|
|
29
|
+
*/
|
|
30
|
+
export function generateRunId(now = new Date()) {
|
|
31
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
32
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
33
|
+
return `${stamp}-${rand}`;
|
|
34
|
+
}
|
|
35
|
+
export function fileTelemetryReporter(opts) {
|
|
36
|
+
const runId = opts.runId ?? generateRunId();
|
|
37
|
+
const maxRuns = opts.maxRuns ?? DEFAULT_MAX_RUNS;
|
|
38
|
+
const telemetryDir = join(opts.dir, TELEMETRY_SUBDIR);
|
|
39
|
+
mkdirSync(telemetryDir, { recursive: true });
|
|
40
|
+
pruneRuns(telemetryDir, runId, maxRuns);
|
|
41
|
+
const filePath = join(telemetryDir, `${runId}.jsonl`);
|
|
42
|
+
// Touch the file synchronously so the run is visible the instant the reporter
|
|
43
|
+
// is constructed (the dev host watches the dir; the stream's own open is
|
|
44
|
+
// async and lazy). Append-mode so a re-attached runId keeps prior records.
|
|
45
|
+
appendFileSync(filePath, "");
|
|
46
|
+
let stream = createWriteStream(filePath, { flags: "a" });
|
|
47
|
+
// A write stream emits 'error' asynchronously (a vanished dir, a full disk).
|
|
48
|
+
// Without a listener Node turns it into an uncaught exception that would take
|
|
49
|
+
// the process down — telemetry must never do that. Swallow + log.
|
|
50
|
+
stream.on("error", (err) => {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(`[telemetry] file reporter "${runId}" stream error:`, err);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
name: `file:${runId}`,
|
|
56
|
+
report(record) {
|
|
57
|
+
if (!stream)
|
|
58
|
+
return;
|
|
59
|
+
// One JSON object per line. A record that can't serialize (a cycle, a
|
|
60
|
+
// BigInt) must not break the stream — drop it and note why.
|
|
61
|
+
let line;
|
|
62
|
+
try {
|
|
63
|
+
line = JSON.stringify(record);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
line = JSON.stringify({
|
|
67
|
+
kind: "telemetry.unserializable",
|
|
68
|
+
error: String(err),
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
stream.write(line + "\n");
|
|
73
|
+
},
|
|
74
|
+
flush() {
|
|
75
|
+
const s = stream;
|
|
76
|
+
if (!s)
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
// Resolve once the OS buffer accepts the next write — the drain barrier
|
|
79
|
+
// that guarantees everything queued so far has been handed to the kernel.
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const ok = s.write("");
|
|
82
|
+
if (ok)
|
|
83
|
+
resolve();
|
|
84
|
+
else
|
|
85
|
+
s.once("drain", () => resolve());
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
close() {
|
|
89
|
+
const s = stream;
|
|
90
|
+
stream = undefined;
|
|
91
|
+
if (!s)
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
s.end((err) => (err ? reject(err) : resolve()));
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Keep only the newest `maxRuns` `.jsonl` files in `telemetryDir`, counting the
|
|
101
|
+
* run we're about to create. Files sort lexically (the run id is timestamp-
|
|
102
|
+
* prefixed), so the lexically-smallest are the oldest. Best-effort — a missing
|
|
103
|
+
* dir or an unreadable entry never blocks startup.
|
|
104
|
+
*/
|
|
105
|
+
function pruneRuns(telemetryDir, currentRunId, maxRuns) {
|
|
106
|
+
if (maxRuns <= 0)
|
|
107
|
+
return;
|
|
108
|
+
let existing;
|
|
109
|
+
try {
|
|
110
|
+
existing = readdirSync(telemetryDir).filter((f) => f.endsWith(".jsonl"));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// The run we're about to open counts toward the cap, so leave room for it.
|
|
116
|
+
const keep = Math.max(0, maxRuns - 1);
|
|
117
|
+
const current = `${currentRunId}.jsonl`;
|
|
118
|
+
const others = existing.filter((f) => f !== current).sort();
|
|
119
|
+
const toDelete = others.slice(0, Math.max(0, others.length - keep));
|
|
120
|
+
for (const f of toDelete) {
|
|
121
|
+
try {
|
|
122
|
+
rmSync(join(telemetryDir, f));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* best-effort — a locked / vanished file is fine to skip */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/telemetry` — local telemetry reporters (sink terminals).
|
|
3
|
+
*
|
|
4
|
+
* The canonical telemetry stream (`runtime.onTelemetry`) is the SOURCE; a
|
|
5
|
+
* `TelemetryReporter` is the SINK terminal. This package ships the local
|
|
6
|
+
* disk driver — `fileTelemetryReporter` — which appends every record to a
|
|
7
|
+
* per-run JSONL file under `<dir>/.nwire/telemetry/`. The dev / Studio host
|
|
8
|
+
* tails those files to render run history with no ingestion server.
|
|
9
|
+
*
|
|
10
|
+
* Install a reporter with `installTelemetryReporter` from `@nwire/runtime`:
|
|
11
|
+
*
|
|
12
|
+
* import { installTelemetryReporter } from "@nwire/runtime";
|
|
13
|
+
* import { fileTelemetryReporter } from "@nwire/telemetry";
|
|
14
|
+
*
|
|
15
|
+
* const detach = installTelemetryReporter(
|
|
16
|
+
* app.runtime,
|
|
17
|
+
* fileTelemetryReporter({ dir: process.cwd() }),
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* Cloud / OTLP forwarding is a sibling driver — `@nwire/telemetry-otel`.
|
|
21
|
+
*/
|
|
22
|
+
export { fileTelemetryReporter, generateRunId, type FileTelemetryReporterOptions, } from "./file-telemetry-reporter.js";
|
|
23
|
+
export { autoInstallFileReporter, resolveProjectCwd } from "./auto-install.js";
|
|
24
|
+
export { installTopologyEmit, writeTopologyFile, TOPOLOGY_FILE_VERSION, type TopologyFile, } from "./topology-emit.js";
|
|
25
|
+
export { captureTopology, type Topology, type CapabilityInfo, type StageInfo, type PluginInfo, type BindingInfo, type HandlerInfo, type HookInfo, type ContributionInfo, type TriggerInfo, type AppInfo, type RouteInfo, } from "./topology-capture.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/telemetry` — local telemetry reporters (sink terminals).
|
|
3
|
+
*
|
|
4
|
+
* The canonical telemetry stream (`runtime.onTelemetry`) is the SOURCE; a
|
|
5
|
+
* `TelemetryReporter` is the SINK terminal. This package ships the local
|
|
6
|
+
* disk driver — `fileTelemetryReporter` — which appends every record to a
|
|
7
|
+
* per-run JSONL file under `<dir>/.nwire/telemetry/`. The dev / Studio host
|
|
8
|
+
* tails those files to render run history with no ingestion server.
|
|
9
|
+
*
|
|
10
|
+
* Install a reporter with `installTelemetryReporter` from `@nwire/runtime`:
|
|
11
|
+
*
|
|
12
|
+
* import { installTelemetryReporter } from "@nwire/runtime";
|
|
13
|
+
* import { fileTelemetryReporter } from "@nwire/telemetry";
|
|
14
|
+
*
|
|
15
|
+
* const detach = installTelemetryReporter(
|
|
16
|
+
* app.runtime,
|
|
17
|
+
* fileTelemetryReporter({ dir: process.cwd() }),
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* Cloud / OTLP forwarding is a sibling driver — `@nwire/telemetry-otel`.
|
|
21
|
+
*/
|
|
22
|
+
export { fileTelemetryReporter, generateRunId, } from "./file-telemetry-reporter.js";
|
|
23
|
+
// Dev auto-install — the composition root calls this so runs persist with no
|
|
24
|
+
// user code, gated to project cwds (off in unit tests / library imports).
|
|
25
|
+
export { autoInstallFileReporter, resolveProjectCwd } from "./auto-install.js";
|
|
26
|
+
// Topology self-emit — a running app writes its own runtime wiring to
|
|
27
|
+
// `.nwire/topology.json` so the static scanner can fold it in without booting.
|
|
28
|
+
export { installTopologyEmit, writeTopologyFile, TOPOLOGY_FILE_VERSION, } from "./topology-emit.js";
|
|
29
|
+
// Live-runtime topology capture — also re-exported by `@nwire/scan` for its
|
|
30
|
+
// legacy instance-based manifest path.
|
|
31
|
+
export { captureTopology, } from "./topology-capture.js";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TOPOLOGY layer — the runtime wiring a static AST pass can't see, because
|
|
3
|
+
* plugins register imperatively. Captured by a lean boot-introspection over a
|
|
4
|
+
* booted App, read from the runtime's own surface (`describeCapabilities`,
|
|
5
|
+
* `listSourceStages`/`listSinkStages`, `listHandlers`, `container.list`, the
|
|
6
|
+
* hooks registry, the app's plugin list).
|
|
7
|
+
*
|
|
8
|
+
* This lives in `@nwire/telemetry` (a runtime lib) — not the scanner — because
|
|
9
|
+
* a running app emits its own topology to `.nwire/topology.json` (see
|
|
10
|
+
* `topology-emit.ts`), and the static scanner (`@nwire/scan`) folds that file
|
|
11
|
+
* into the manifest. The scanner re-exports `captureTopology` for the legacy
|
|
12
|
+
* instance-based path. Introspecting a live runtime is runtime-domain work, so
|
|
13
|
+
* the lib owns it.
|
|
14
|
+
*
|
|
15
|
+
* This is the "show the internals exactly — how, what, why" half: which plugins
|
|
16
|
+
* are installed, what each contributes (capabilities + the kinds they apply to +
|
|
17
|
+
* the ctx keys they add), the ordered source/sink stage pipelines, the
|
|
18
|
+
* ctx-surface-per-kind, the registered handlers by kind, the DI bindings, the
|
|
19
|
+
* hooks, and the transport triggers.
|
|
20
|
+
*/
|
|
21
|
+
export interface CapabilityInfo {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** Handler kinds this capability's ctx applies to; undefined = universal. */
|
|
24
|
+
readonly kinds?: readonly string[];
|
|
25
|
+
/** ctx keys it contributes (probed). */
|
|
26
|
+
readonly ctxKeys: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface StageInfo {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly position: string;
|
|
31
|
+
readonly kind?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PluginInfo {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BindingInfo {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly kind?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface HandlerInfo {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
/** `action` / `query` / `handler` / … — read off the registered handler. */
|
|
43
|
+
readonly kind: string;
|
|
44
|
+
}
|
|
45
|
+
export interface HookInfo {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly id?: string;
|
|
48
|
+
readonly chain?: number;
|
|
49
|
+
readonly listeners?: number;
|
|
50
|
+
/** Where the hook was authored — `file:line:column`, captured at attach time. */
|
|
51
|
+
readonly source?: {
|
|
52
|
+
readonly file: string;
|
|
53
|
+
readonly line: number;
|
|
54
|
+
readonly column?: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What a single plugin contributed to the app — DI bindings, capabilities,
|
|
59
|
+
* source/sink stages, registered handlers, and hooks. Attributed at boot by the
|
|
60
|
+
* app lifecycle; powers the `contributes` graph edge on Studio's Plugins page.
|
|
61
|
+
*/
|
|
62
|
+
export interface ContributionInfo {
|
|
63
|
+
readonly plugin: string;
|
|
64
|
+
readonly bindings: readonly string[];
|
|
65
|
+
readonly capabilities: readonly string[];
|
|
66
|
+
readonly sourceStages: readonly string[];
|
|
67
|
+
readonly sinkStages: readonly string[];
|
|
68
|
+
readonly handlers: readonly string[];
|
|
69
|
+
readonly hooks: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
/** A transport trigger: a wired binding → the handler it dispatches. */
|
|
72
|
+
export interface TriggerInfo {
|
|
73
|
+
readonly binding: string;
|
|
74
|
+
readonly handler?: string;
|
|
75
|
+
}
|
|
76
|
+
/** An installed app (the BC). For a single captured app this is a one-element list. */
|
|
77
|
+
export interface AppInfo {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly description?: string;
|
|
80
|
+
readonly plugins: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
/** A transport route: method + path + the handler it mounts. */
|
|
83
|
+
export interface RouteInfo {
|
|
84
|
+
readonly method: string;
|
|
85
|
+
readonly path: string;
|
|
86
|
+
readonly handler?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface Topology {
|
|
89
|
+
readonly apps: readonly AppInfo[];
|
|
90
|
+
readonly plugins: readonly PluginInfo[];
|
|
91
|
+
readonly capabilities: readonly CapabilityInfo[];
|
|
92
|
+
/** kind → the ctx keys a handler of that kind receives (universal caps + kind-matched). */
|
|
93
|
+
readonly ctxByKind: Readonly<Record<string, readonly string[]>>;
|
|
94
|
+
readonly sourceStages: readonly StageInfo[];
|
|
95
|
+
readonly sinkStages: readonly StageInfo[];
|
|
96
|
+
readonly handlers: readonly HandlerInfo[];
|
|
97
|
+
readonly bindings: readonly BindingInfo[];
|
|
98
|
+
readonly hooks: readonly HookInfo[];
|
|
99
|
+
readonly triggers: readonly TriggerInfo[];
|
|
100
|
+
/** HTTP routes (method + path → handler) — the richer shape over `triggers`. */
|
|
101
|
+
readonly routes: readonly RouteInfo[];
|
|
102
|
+
/** Per-plugin contribution attribution — the `contributes` edge source. */
|
|
103
|
+
readonly contributions: readonly ContributionInfo[];
|
|
104
|
+
}
|
|
105
|
+
type Any = any;
|
|
106
|
+
interface IntrospectableApp {
|
|
107
|
+
readonly appName?: string;
|
|
108
|
+
readonly name?: string;
|
|
109
|
+
readonly description?: string;
|
|
110
|
+
readonly plugins?: ReadonlyArray<{
|
|
111
|
+
readonly name: string;
|
|
112
|
+
}>;
|
|
113
|
+
readonly pluginContributions?: () => ReadonlyArray<ContributionInfo>;
|
|
114
|
+
readonly container?: {
|
|
115
|
+
list?(): ReadonlyArray<{
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly kind?: string;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
readonly interface?: {
|
|
121
|
+
readonly wires?: ReadonlyArray<{
|
|
122
|
+
readonly binding?: Any;
|
|
123
|
+
readonly handler?: Any;
|
|
124
|
+
}>;
|
|
125
|
+
};
|
|
126
|
+
readonly runtime?: {
|
|
127
|
+
describeCapabilities?(): ReadonlyArray<{
|
|
128
|
+
name: string;
|
|
129
|
+
kinds?: readonly string[];
|
|
130
|
+
ctxKeys: readonly string[];
|
|
131
|
+
}>;
|
|
132
|
+
listSourceStages?(): ReadonlyArray<{
|
|
133
|
+
name: string;
|
|
134
|
+
position: string;
|
|
135
|
+
kind?: string;
|
|
136
|
+
}>;
|
|
137
|
+
listSinkStages?(): ReadonlyArray<{
|
|
138
|
+
name: string;
|
|
139
|
+
position: string;
|
|
140
|
+
kind?: string;
|
|
141
|
+
}>;
|
|
142
|
+
listHandlers?(): readonly string[];
|
|
143
|
+
getHandler?(name: string): Any;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Capture the topology from a booted App. Every read is guarded so a forge-less
|
|
148
|
+
* or transport-less app still produces a valid (sparser) topology.
|
|
149
|
+
*/
|
|
150
|
+
export declare function captureTopology(app: IntrospectableApp): Topology;
|
|
151
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TOPOLOGY layer — the runtime wiring a static AST pass can't see, because
|
|
3
|
+
* plugins register imperatively. Captured by a lean boot-introspection over a
|
|
4
|
+
* booted App, read from the runtime's own surface (`describeCapabilities`,
|
|
5
|
+
* `listSourceStages`/`listSinkStages`, `listHandlers`, `container.list`, the
|
|
6
|
+
* hooks registry, the app's plugin list).
|
|
7
|
+
*
|
|
8
|
+
* This lives in `@nwire/telemetry` (a runtime lib) — not the scanner — because
|
|
9
|
+
* a running app emits its own topology to `.nwire/topology.json` (see
|
|
10
|
+
* `topology-emit.ts`), and the static scanner (`@nwire/scan`) folds that file
|
|
11
|
+
* into the manifest. The scanner re-exports `captureTopology` for the legacy
|
|
12
|
+
* instance-based path. Introspecting a live runtime is runtime-domain work, so
|
|
13
|
+
* the lib owns it.
|
|
14
|
+
*
|
|
15
|
+
* This is the "show the internals exactly — how, what, why" half: which plugins
|
|
16
|
+
* are installed, what each contributes (capabilities + the kinds they apply to +
|
|
17
|
+
* the ctx keys they add), the ordered source/sink stage pipelines, the
|
|
18
|
+
* ctx-surface-per-kind, the registered handlers by kind, the DI bindings, the
|
|
19
|
+
* hooks, and the transport triggers.
|
|
20
|
+
*/
|
|
21
|
+
import { listHooks } from "@nwire/hooks";
|
|
22
|
+
/** Describe a wire binding compactly: `"POST /orders"`, a queue name, or its kind. */
|
|
23
|
+
function describeBinding(binding) {
|
|
24
|
+
if (!binding || typeof binding !== "object")
|
|
25
|
+
return "binding";
|
|
26
|
+
const verb = binding.verb ?? binding.method;
|
|
27
|
+
if (verb && binding.path)
|
|
28
|
+
return `${String(verb).toUpperCase()} ${binding.path}`;
|
|
29
|
+
return String(binding.name ?? binding.adapter ?? binding.$adapter ?? binding.kind ?? "binding");
|
|
30
|
+
}
|
|
31
|
+
/** Kind off a registered handler: `config.kind` → `$kind` → `"handler"`. */
|
|
32
|
+
function handlerKind(h) {
|
|
33
|
+
return h?.config?.kind ?? h?.$kind ?? "handler";
|
|
34
|
+
}
|
|
35
|
+
/** A wire binding's method + path, when it's an HTTP route (else undefined). */
|
|
36
|
+
function routeOf(binding) {
|
|
37
|
+
if (!binding || typeof binding !== "object")
|
|
38
|
+
return undefined;
|
|
39
|
+
const verb = binding.verb ?? binding.method;
|
|
40
|
+
if (verb && binding.path)
|
|
41
|
+
return { method: String(verb).toUpperCase(), path: String(binding.path) };
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Capture the topology from a booted App. Every read is guarded so a forge-less
|
|
46
|
+
* or transport-less app still produces a valid (sparser) topology.
|
|
47
|
+
*/
|
|
48
|
+
export function captureTopology(app) {
|
|
49
|
+
const rt = app.runtime ?? {};
|
|
50
|
+
const plugins = (app.plugins ?? []).map((p) => ({ name: p.name }));
|
|
51
|
+
const capabilities = (rt.describeCapabilities?.() ?? []).map((c) => ({
|
|
52
|
+
name: c.name,
|
|
53
|
+
kinds: c.kinds,
|
|
54
|
+
ctxKeys: c.ctxKeys,
|
|
55
|
+
}));
|
|
56
|
+
const sourceStages = (rt.listSourceStages?.() ?? []).map((s) => ({
|
|
57
|
+
name: s.name,
|
|
58
|
+
position: s.position,
|
|
59
|
+
kind: s.kind,
|
|
60
|
+
}));
|
|
61
|
+
const sinkStages = (rt.listSinkStages?.() ?? []).map((s) => ({
|
|
62
|
+
name: s.name,
|
|
63
|
+
position: s.position,
|
|
64
|
+
kind: s.kind,
|
|
65
|
+
}));
|
|
66
|
+
const handlerNames = rt.listHandlers?.() ?? [];
|
|
67
|
+
const handlers = handlerNames.map((name) => ({
|
|
68
|
+
name,
|
|
69
|
+
kind: handlerKind(rt.getHandler?.(name)),
|
|
70
|
+
}));
|
|
71
|
+
const bindings = (app.container?.list?.() ?? []).map((b) => ({
|
|
72
|
+
name: b.name,
|
|
73
|
+
kind: b.kind,
|
|
74
|
+
}));
|
|
75
|
+
// Hooks: from the process-wide hook registry (best-effort; tolerate absence).
|
|
76
|
+
let hooks = [];
|
|
77
|
+
try {
|
|
78
|
+
hooks = listHooks().map((h) => ({
|
|
79
|
+
name: h.name,
|
|
80
|
+
id: h.id,
|
|
81
|
+
chain: h.chain ?? h.stepCounts?.()?.chain,
|
|
82
|
+
listeners: h.listeners ?? h.stepCounts?.()?.listeners,
|
|
83
|
+
source: h.source,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* registry unavailable — leave empty */
|
|
88
|
+
}
|
|
89
|
+
const wires = app.interface?.wires ?? [];
|
|
90
|
+
const triggers = wires.map((w) => ({
|
|
91
|
+
binding: describeBinding(w.binding),
|
|
92
|
+
handler: w.handler?.name,
|
|
93
|
+
}));
|
|
94
|
+
// Apps (the BC) — a single captured app yields a one-element list; an
|
|
95
|
+
// app-composition merge would concatenate.
|
|
96
|
+
const apps = app.appName
|
|
97
|
+
? [{ name: app.appName, description: app.description, plugins: plugins.map((p) => p.name) }]
|
|
98
|
+
: [];
|
|
99
|
+
// Routes — the richer method+path+handler shape over `triggers`, for HTTP wires.
|
|
100
|
+
const routes = [];
|
|
101
|
+
for (const w of wires) {
|
|
102
|
+
const r = routeOf(w.binding);
|
|
103
|
+
if (r)
|
|
104
|
+
routes.push({ method: r.method, path: r.path, handler: w.handler?.name });
|
|
105
|
+
}
|
|
106
|
+
// ctx-per-kind: every kind a handler is registered under, plus the event
|
|
107
|
+
// listener kind, mapped to the union of ctx keys it receives (universal caps +
|
|
108
|
+
// caps whose `kinds` include it).
|
|
109
|
+
const kinds = new Set(handlers.map((h) => h.kind));
|
|
110
|
+
kinds.add("listener");
|
|
111
|
+
const ctxByKind = {};
|
|
112
|
+
for (const kind of kinds) {
|
|
113
|
+
const keys = new Set();
|
|
114
|
+
for (const cap of capabilities) {
|
|
115
|
+
if (cap.kinds === undefined || cap.kinds.includes(kind)) {
|
|
116
|
+
for (const k of cap.ctxKeys)
|
|
117
|
+
keys.add(k);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ctxByKind[kind] = [...keys];
|
|
121
|
+
}
|
|
122
|
+
const contributions = (app.pluginContributions?.() ?? []).map((c) => ({
|
|
123
|
+
plugin: c.plugin,
|
|
124
|
+
bindings: c.bindings,
|
|
125
|
+
capabilities: c.capabilities,
|
|
126
|
+
sourceStages: c.sourceStages,
|
|
127
|
+
sinkStages: c.sinkStages,
|
|
128
|
+
handlers: c.handlers,
|
|
129
|
+
hooks: c.hooks,
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
apps,
|
|
133
|
+
plugins,
|
|
134
|
+
capabilities,
|
|
135
|
+
ctxByKind,
|
|
136
|
+
sourceStages,
|
|
137
|
+
sinkStages,
|
|
138
|
+
handlers,
|
|
139
|
+
bindings,
|
|
140
|
+
hooks,
|
|
141
|
+
triggers,
|
|
142
|
+
routes,
|
|
143
|
+
contributions,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topology self-emit — a running app writes its own wiring to disk.
|
|
3
|
+
*
|
|
4
|
+
* The static scanner (`@nwire/scan`) sees source shapes (events/actions/…) but
|
|
5
|
+
* not the runtime topology, because plugins register imperatively at boot. The
|
|
6
|
+
* old fix booted the app from the scanner to read it; this replaces that with a
|
|
7
|
+
* self-emit, mirroring the telemetry auto-install: when an app boots inside a
|
|
8
|
+
* project working directory, it introspects its own runtime once it is ready and
|
|
9
|
+
* writes `<cwd>/.nwire/topology.json`. The scanner then folds that file into the
|
|
10
|
+
* manifest with no live instance — "scan the graph, not the runtime."
|
|
11
|
+
*
|
|
12
|
+
* Userland-unaware: no plugin, no config — the composition root (`createApp`)
|
|
13
|
+
* calls `installTopologyEmit` once. Gated by the SAME rule as the telemetry
|
|
14
|
+
* auto-install (`resolveProjectCwd`): `NWIRE_CWD` set, or a `.nwire` dir present,
|
|
15
|
+
* and OFF under Vitest / `NODE_ENV=test` so the suite never writes stray files.
|
|
16
|
+
*/
|
|
17
|
+
import { type Topology } from "./topology-capture.js";
|
|
18
|
+
/** The on-disk shape of `.nwire/topology.json`. */
|
|
19
|
+
export interface TopologyFile {
|
|
20
|
+
/** Bumped when the file shape changes, so a reader can detect a stale file. */
|
|
21
|
+
readonly version: 1;
|
|
22
|
+
/** ISO timestamp of the emit — the app's ready moment. */
|
|
23
|
+
readonly emittedAt: string;
|
|
24
|
+
/** The captured runtime topology. */
|
|
25
|
+
readonly topology: Topology;
|
|
26
|
+
}
|
|
27
|
+
/** Bumped when {@link TopologyFile} changes shape. */
|
|
28
|
+
export declare const TOPOLOGY_FILE_VERSION: 1;
|
|
29
|
+
/**
|
|
30
|
+
* Write the captured topology of a booted `app` to `<dir>/.nwire/topology.json`.
|
|
31
|
+
* Exported for tests + the scanner's instance path; the composition root uses
|
|
32
|
+
* {@link installTopologyEmit} instead, which gates + hooks lifecycle.
|
|
33
|
+
*/
|
|
34
|
+
export declare function writeTopologyFile(app: any, dir: string): string;
|
|
35
|
+
/** Minimal shape the emit needs off the App — its lifecycle hooks. */
|
|
36
|
+
interface EmittableApp {
|
|
37
|
+
readonly appHooks?: {
|
|
38
|
+
readonly AppReady?: {
|
|
39
|
+
on(fn: () => void | Promise<void>): unknown;
|
|
40
|
+
};
|
|
41
|
+
readonly AppShutdown?: {
|
|
42
|
+
on(fn: () => void | Promise<void>): unknown;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Install the topology self-emit on `app` when a project cwd is detected.
|
|
48
|
+
* No-op (returns `undefined`) otherwise. Safe to call unconditionally from the
|
|
49
|
+
* composition root. The topology is complete only after boot, so it captures on
|
|
50
|
+
* `AppReady`; the file is removed on `AppShutdown` so a dead app leaves no stale
|
|
51
|
+
* topology behind for the scanner to fold.
|
|
52
|
+
*/
|
|
53
|
+
export declare function installTopologyEmit(app: EmittableApp & Record<string, any>, env?: NodeJS.ProcessEnv): (() => void) | undefined;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topology self-emit — a running app writes its own wiring to disk.
|
|
3
|
+
*
|
|
4
|
+
* The static scanner (`@nwire/scan`) sees source shapes (events/actions/…) but
|
|
5
|
+
* not the runtime topology, because plugins register imperatively at boot. The
|
|
6
|
+
* old fix booted the app from the scanner to read it; this replaces that with a
|
|
7
|
+
* self-emit, mirroring the telemetry auto-install: when an app boots inside a
|
|
8
|
+
* project working directory, it introspects its own runtime once it is ready and
|
|
9
|
+
* writes `<cwd>/.nwire/topology.json`. The scanner then folds that file into the
|
|
10
|
+
* manifest with no live instance — "scan the graph, not the runtime."
|
|
11
|
+
*
|
|
12
|
+
* Userland-unaware: no plugin, no config — the composition root (`createApp`)
|
|
13
|
+
* calls `installTopologyEmit` once. Gated by the SAME rule as the telemetry
|
|
14
|
+
* auto-install (`resolveProjectCwd`): `NWIRE_CWD` set, or a `.nwire` dir present,
|
|
15
|
+
* and OFF under Vitest / `NODE_ENV=test` so the suite never writes stray files.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { resolveProjectCwd } from "./auto-install.js";
|
|
20
|
+
import { captureTopology } from "./topology-capture.js";
|
|
21
|
+
/** Bumped when {@link TopologyFile} changes shape. */
|
|
22
|
+
export const TOPOLOGY_FILE_VERSION = 1;
|
|
23
|
+
/** Resolve `<cwd>/.nwire/topology.json` for a project cwd. */
|
|
24
|
+
function topologyPath(cwd) {
|
|
25
|
+
return join(cwd, ".nwire", "topology.json");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Write the captured topology of a booted `app` to `<dir>/.nwire/topology.json`.
|
|
29
|
+
* Exported for tests + the scanner's instance path; the composition root uses
|
|
30
|
+
* {@link installTopologyEmit} instead, which gates + hooks lifecycle.
|
|
31
|
+
*/
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
export function writeTopologyFile(app, dir) {
|
|
34
|
+
const file = {
|
|
35
|
+
version: TOPOLOGY_FILE_VERSION,
|
|
36
|
+
emittedAt: new Date().toISOString(),
|
|
37
|
+
topology: captureTopology(app),
|
|
38
|
+
};
|
|
39
|
+
const path = topologyPath(dir);
|
|
40
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
41
|
+
writeFileSync(path, JSON.stringify(file, null, 2));
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Install the topology self-emit on `app` when a project cwd is detected.
|
|
46
|
+
* No-op (returns `undefined`) otherwise. Safe to call unconditionally from the
|
|
47
|
+
* composition root. The topology is complete only after boot, so it captures on
|
|
48
|
+
* `AppReady`; the file is removed on `AppShutdown` so a dead app leaves no stale
|
|
49
|
+
* topology behind for the scanner to fold.
|
|
50
|
+
*/
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
export function installTopologyEmit(app, env = process.env) {
|
|
53
|
+
const dir = resolveProjectCwd(env);
|
|
54
|
+
if (!dir)
|
|
55
|
+
return undefined;
|
|
56
|
+
let written;
|
|
57
|
+
app.appHooks?.AppReady?.on(() => {
|
|
58
|
+
written = writeTopologyFile(app, dir);
|
|
59
|
+
});
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
if (!written)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
rmSync(written, { force: true });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* best-effort — a missing file is fine */
|
|
68
|
+
}
|
|
69
|
+
written = undefined;
|
|
70
|
+
};
|
|
71
|
+
app.appHooks?.AppShutdown?.on(cleanup);
|
|
72
|
+
return cleanup;
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/telemetry",
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Nwire — local telemetry reporters. Writes the canonical telemetry stream to disk as JSONL run files (.nwire/telemetry/<runId>.jsonl) for Studio history and post-mortem review. Cloud/OTLP forwarding lives in @nwire/telemetry-otel.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jsonl",
|
|
7
|
+
"nwire",
|
|
8
|
+
"observability",
|
|
9
|
+
"reporter",
|
|
10
|
+
"telemetry"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/telemetry.js",
|
|
20
|
+
"types": "./dist/telemetry.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/telemetry.js",
|
|
24
|
+
"types": "./dist/telemetry.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@nwire/hooks": "0.13.0",
|
|
32
|
+
"@nwire/runtime": "0.13.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.19.9",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.0.18",
|
|
38
|
+
"@nwire/container": "0.13.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|