@nwire/cli 0.7.1 → 0.8.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.
Files changed (84) hide show
  1. package/README.md +62 -20
  2. package/dist/__tests__/bench.test.d.ts +2 -0
  3. package/dist/__tests__/bench.test.d.ts.map +1 -0
  4. package/dist/__tests__/bench.test.js +94 -0
  5. package/dist/__tests__/bench.test.js.map +1 -0
  6. package/dist/__tests__/replay.test.d.ts +2 -0
  7. package/dist/__tests__/replay.test.d.ts.map +1 -0
  8. package/dist/__tests__/replay.test.js +202 -0
  9. package/dist/__tests__/replay.test.js.map +1 -0
  10. package/dist/__tests__/trace.test.d.ts +2 -0
  11. package/dist/__tests__/trace.test.d.ts.map +1 -0
  12. package/dist/__tests__/trace.test.js +136 -0
  13. package/dist/__tests__/trace.test.js.map +1 -0
  14. package/dist/__tests__/watch.test.d.ts +2 -0
  15. package/dist/__tests__/watch.test.d.ts.map +1 -0
  16. package/dist/__tests__/watch.test.js +109 -0
  17. package/dist/__tests__/watch.test.js.map +1 -0
  18. package/dist/cache-runner.js +62 -14
  19. package/dist/cache-runner.js.map +1 -1
  20. package/dist/cli.js +8 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/bench.d.ts +103 -0
  23. package/dist/commands/bench.d.ts.map +1 -0
  24. package/dist/commands/bench.js +203 -0
  25. package/dist/commands/bench.js.map +1 -0
  26. package/dist/commands/dev.d.ts.map +1 -1
  27. package/dist/commands/dev.js +31 -20
  28. package/dist/commands/dev.js.map +1 -1
  29. package/dist/commands/doctor.d.ts.map +1 -1
  30. package/dist/commands/doctor.js +29 -5
  31. package/dist/commands/doctor.js.map +1 -1
  32. package/dist/commands/infra.d.ts.map +1 -1
  33. package/dist/commands/infra.js +7 -0
  34. package/dist/commands/infra.js.map +1 -1
  35. package/dist/commands/logs.d.ts.map +1 -1
  36. package/dist/commands/logs.js +64 -11
  37. package/dist/commands/logs.js.map +1 -1
  38. package/dist/commands/replay.d.ts +45 -0
  39. package/dist/commands/replay.d.ts.map +1 -0
  40. package/dist/commands/replay.js +159 -0
  41. package/dist/commands/replay.js.map +1 -0
  42. package/dist/commands/run.d.ts.map +1 -1
  43. package/dist/commands/run.js +14 -4
  44. package/dist/commands/run.js.map +1 -1
  45. package/dist/commands/studio.d.ts.map +1 -1
  46. package/dist/commands/studio.js +7 -1
  47. package/dist/commands/studio.js.map +1 -1
  48. package/dist/commands/test.d.ts.map +1 -1
  49. package/dist/commands/test.js +3 -0
  50. package/dist/commands/test.js.map +1 -1
  51. package/dist/commands/trace.d.ts +40 -0
  52. package/dist/commands/trace.d.ts.map +1 -0
  53. package/dist/commands/trace.js +195 -0
  54. package/dist/commands/trace.js.map +1 -0
  55. package/dist/commands/watch.d.ts +33 -0
  56. package/dist/commands/watch.d.ts.map +1 -0
  57. package/dist/commands/watch.js +152 -0
  58. package/dist/commands/watch.js.map +1 -0
  59. package/dist/lib/dev-entry.d.ts +39 -0
  60. package/dist/lib/dev-entry.d.ts.map +1 -0
  61. package/dist/lib/dev-entry.js +102 -0
  62. package/dist/lib/dev-entry.js.map +1 -0
  63. package/dist/lib/exec.d.ts +7 -0
  64. package/dist/lib/exec.d.ts.map +1 -1
  65. package/dist/lib/exec.js +19 -1
  66. package/dist/lib/exec.js.map +1 -1
  67. package/dist/lib/run-task.d.ts.map +1 -1
  68. package/dist/lib/run-task.js +6 -1
  69. package/dist/lib/run-task.js.map +1 -1
  70. package/dist/lib/sse.d.ts +24 -0
  71. package/dist/lib/sse.d.ts.map +1 -0
  72. package/dist/lib/sse.js +63 -0
  73. package/dist/lib/sse.js.map +1 -0
  74. package/dist/lib/wire-discovery.d.ts +9 -0
  75. package/dist/lib/wire-discovery.d.ts.map +1 -0
  76. package/dist/lib/wire-discovery.js +82 -0
  77. package/dist/lib/wire-discovery.js.map +1 -0
  78. package/dist/load-config.d.ts +3 -3
  79. package/dist/load-config.d.ts.map +1 -1
  80. package/dist/load-config.js +15 -3
  81. package/dist/load-config.js.map +1 -1
  82. package/dist/ls-runner.js +2 -2
  83. package/dist/ls-runner.js.map +1 -1
  84. package/package.json +5 -3
package/README.md CHANGED
@@ -1,36 +1,78 @@
1
1
  # @nwire/cli
2
2
 
3
- > The `nwire` umbrella CLI — `dev`, `run`, `cache`, `studio`, `call`, `please`, `ls`, `infra`, …
3
+ > The `nwire` umbrella CLI — `dev`, `run`, `cache`, `studio`, `please`, `ls`, `trace`, `watch`, `replay`, `infra`, …
4
4
 
5
- ## What it does
6
-
7
- Built on citty: one root command per file under `commands/`. Long-lived processes (`dev`, `run`, `studio`) go through `@nwire/kernel`; build/quality passthroughs (`fmt`, `lint`, `check`) shell out. Reads `nwire.config.ts` (or falls back to convention) to locate the consumer's apps registry and `.nwire/` cache.
8
-
9
- ## Install
5
+ Built on citty: one root command per file under `commands/`. Long-lived
6
+ processes (`dev`, `run`, `studio`) go through `@nwire/kernel`;
7
+ build/quality passthroughs (`fmt`, `lint`, `check`, `test`) shell out.
8
+ Reads `nwire.config.ts` (or falls back to convention) to locate the
9
+ consumer's apps registry and the `.nwire/` cache.
10
10
 
11
11
  ```bash
12
12
  pnpm add -D @nwire/cli
13
13
  ```
14
14
 
15
- After install the `nwire` binary is on your `node_modules/.bin`. With `pnpm dlx` (or aliased globally) you can run anywhere.
15
+ After install the `nwire` binary is on `node_modules/.bin`.
16
16
 
17
- ## Quick start
17
+ ## Commands
18
18
 
19
19
  ```bash
20
- nwire dev # boot every app in the registry, hot-reload
21
- nwire run learnflow-api # boot one wire
22
- nwire cache # rebuild.nwire/*.json from source
23
- nwire studio # open Studio against the cache + live runtime
24
- nwire please <action> # dispatch an action from the command line
25
- nwire ls # list actions / events / actors per app
26
- nwire infra up # docker-compose up (postgres/redis/mongo/mailhog/logto/minio)
20
+ # Run / develop
21
+ nwire dev # boot every app in the registry, hot-reload
22
+ nwire run <wire> # boot one wire
23
+ nwire ps # list running processes
24
+ nwire logs <wire> # tail logs
25
+
26
+ # Static + dynamic introspection
27
+ nwire cache # rebuild .nwire/*.json from source
28
+ nwire ls # list actions / events / actors per app
29
+ nwire studio # open Studio against the cache + live runtime
30
+
31
+ # Live observability (hooks + events)
32
+ nwire trace [--correlation <id>] [--limit N]
33
+ # tail event stream from a running wire; with --correlation
34
+ # renders the causation tree.
35
+ nwire watch <hook> [--limit N]
36
+ # tail hook-step telemetry. Supports wildcards:
37
+ # `nwire watch plugin.boot:*`
38
+ nwire replay --file <recording.json> [--hook <name>]
39
+ # replay a recorded hook run against the live wire,
40
+ # or print an offline trace.
41
+
42
+ # Operator
43
+ nwire please <action> [--<arg> <value> ...]
44
+ # dispatch an action / query / defineCommand
45
+
46
+ # Quality + build
47
+ nwire fmt / nwire lint / nwire check / nwire test / nwire build
48
+
49
+ # Misc
50
+ nwire infra up # docker-compose up (postgres/redis/mongo/mailhog/logto/minio)
51
+ nwire doctor # environment + config sanity check
27
52
  ```
28
53
 
29
- ## API surface
54
+ Running `nwire` with no args prints a greeting + the command list.
55
+
56
+ ## Surface
57
+
58
+ | Export | Role |
59
+ | ------------------------------- | ------------------------------------------------------------ |
60
+ | `nwire` binary | One subcommand per file under `src/commands/`. |
61
+ | `loadConfig(cwd)` | Programmatic config loader. |
62
+ | `resolveAppsEntry(cwd, config)` | Locate the apps registry from convention or config. |
63
+
64
+ Adding a command: write `src/commands/<name>.ts` with citty's
65
+ `defineCommand`, register it in `src/cli.ts`. To make it reachable
66
+ from Studio + MCP, register a kernel router handler too.
67
+
68
+ ## Related
30
69
 
31
- - `nwire` CLI binary (one subcommand per file).
32
- - `loadConfig(cwd)` / `resolveAppsEntry(cwd, config)` programmatic entry resolution.
70
+ - `@nwire/kernel` — host for long-lived processes; the CLI delegates `dev`/`run`/`studio` to it.
71
+ - `@nwire/scan` `nwire cache` writes `.nwire/*.json` via this package.
72
+ - `@nwire/please` — `nwire please` delegates here.
73
+ - `@nwire/mcp` — same kernel router exposed to AI clients over stdio.
74
+ - `@nwire/studio` — `nwire studio` boots the Vite dev server for it.
33
75
 
34
- ## When to use
76
+ ## Status
35
77
 
36
- Every Nwire project after `pnpm install`. Fits every level.
78
+ v0.x command surface is additive. `trace` / `watch` / `replay` operate on the runtime's `/__nwire/*` HTTP+SSE endpoints; offline modes fall back to `.nwire/` cache where possible.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bench.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bench.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Unit tests for `nwire bench`.
3
+ *
4
+ * The command shells out to a real `k6` binary at runtime, so we don't
5
+ * exercise the full execution path here. We do test the pieces that
6
+ * deserve assertions:
7
+ *
8
+ * - scenario path resolution (walks up from CWD and finds the right file)
9
+ * - the k6 argv builder (env flags + script tail)
10
+ * - the command meta + arg shape
11
+ */
12
+ import { describe, it, expect, afterEach, beforeEach } from "vitest";
13
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { benchCommand, buildK6Args, resolveScenarioPath, } from "../commands/bench.js";
17
+ let target;
18
+ beforeEach(() => {
19
+ target = mkdtempSync(join(tmpdir(), "bench-test-"));
20
+ });
21
+ afterEach(() => {
22
+ if (target)
23
+ rmSync(target, { recursive: true, force: true });
24
+ });
25
+ describe("nwire bench — command meta", () => {
26
+ it("registers a `bench` subcommand with the expected flags", () => {
27
+ const meta = benchCommand.meta;
28
+ expect(meta.name).toBe("bench");
29
+ expect(benchCommand.args).toMatchObject({
30
+ scenario: expect.any(Object),
31
+ vus: expect.any(Object),
32
+ duration: expect.any(Object),
33
+ ramp: expect.any(Object),
34
+ "base-url": expect.any(Object),
35
+ out: expect.any(Object),
36
+ });
37
+ });
38
+ });
39
+ describe("resolveScenarioPath", () => {
40
+ it("returns null when no scripts/k6 folder exists anywhere in the walk", () => {
41
+ expect(resolveScenarioPath("action", target, [target])).toBeNull();
42
+ });
43
+ it("finds a script when scripts/k6/<file> exists in CWD", () => {
44
+ mkdirSync(join(target, "scripts", "k6"), { recursive: true });
45
+ writeFileSync(join(target, "scripts", "k6", "action-dispatch.js"), "// stub\n");
46
+ const found = resolveScenarioPath("action", target, [target]);
47
+ expect(found).toBe(join(target, "scripts", "k6", "action-dispatch.js"));
48
+ });
49
+ it("walks up from a root to find the framework's scripts/k6", () => {
50
+ mkdirSync(join(target, "scripts", "k6"), { recursive: true });
51
+ writeFileSync(join(target, "scripts", "k6", "mixed-workload.js"), "// stub\n");
52
+ mkdirSync(join(target, "nested", "deeper"), { recursive: true });
53
+ const found = resolveScenarioPath("mixed", target, [join(target, "nested", "deeper")]);
54
+ expect(found).toBe(join(target, "scripts", "k6", "mixed-workload.js"));
55
+ });
56
+ });
57
+ describe("buildK6Args", () => {
58
+ it("emits run + env vars + script path", () => {
59
+ const argv = buildK6Args({
60
+ script: "/path/to/action-dispatch.js",
61
+ args: {
62
+ scenario: "action",
63
+ vus: 50,
64
+ duration: "30s",
65
+ ramp: "10s",
66
+ baseUrl: "http://localhost:3030",
67
+ },
68
+ });
69
+ expect(argv[0]).toBe("run");
70
+ expect(argv).toContain("-e");
71
+ expect(argv).toContain("VUS=50");
72
+ expect(argv).toContain("DURATION=30s");
73
+ expect(argv).toContain("RAMP=10s");
74
+ expect(argv).toContain("BASE_URL=http://localhost:3030");
75
+ expect(argv[argv.length - 1]).toBe("/path/to/action-dispatch.js");
76
+ });
77
+ it("inserts --summary-export when a summaryOut path is provided", () => {
78
+ const argv = buildK6Args({
79
+ script: "/x.js",
80
+ args: {
81
+ scenario: "read",
82
+ vus: 1,
83
+ duration: "1s",
84
+ ramp: "1s",
85
+ baseUrl: "http://localhost:1",
86
+ },
87
+ summaryOut: "/tmp/read.json",
88
+ });
89
+ expect(argv).toContain("--summary-export");
90
+ const i = argv.indexOf("--summary-export");
91
+ expect(argv[i + 1]).toBe("/tmp/read.json");
92
+ });
93
+ });
94
+ //# sourceMappingURL=bench.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bench.test.js","sourceRoot":"","sources":["../../src/__tests__/bench.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=replay.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/replay.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Unit tests for `nwire replay`.
3
+ *
4
+ * Same shape as `trace.test.ts`: we don't drive the full citty command
5
+ * end-to-end (it calls `process.exit`), we exercise the composable parts:
6
+ *
7
+ * • command meta + arg shape
8
+ * • loadRecording() against real fixture files
9
+ * • postReplay() happy path against a stubbed wire
10
+ * • postReplay() returns null when wire answers 404 / 501 (graceful fallback)
11
+ * • renderOfflineDiff() output (offline fallback)
12
+ */
13
+ import { describe, it, expect, afterEach } from "vitest";
14
+ import { createServer } from "node:http";
15
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { replayCommand, loadRecording, postReplay, renderOfflineDiff, renderReplayResult, } from "../commands/replay.js";
19
+ let server;
20
+ let tmp;
21
+ afterEach(async () => {
22
+ if (server) {
23
+ await new Promise((res) => server.close(() => res()));
24
+ server = undefined;
25
+ }
26
+ if (tmp) {
27
+ rmSync(tmp, { recursive: true, force: true });
28
+ tmp = undefined;
29
+ }
30
+ });
31
+ function fixtureRecording() {
32
+ return {
33
+ hookId: "h_test",
34
+ hookName: "submission.submit",
35
+ runId: "r_1",
36
+ outcome: "completed",
37
+ steps: [
38
+ {
39
+ hookName: "submission.submit",
40
+ hookId: "h_test",
41
+ runId: "r_1",
42
+ stepId: 0,
43
+ stepKind: "chain",
44
+ stepName: "validate",
45
+ phase: "start",
46
+ ts: 0,
47
+ },
48
+ {
49
+ hookName: "submission.submit",
50
+ hookId: "h_test",
51
+ runId: "r_1",
52
+ stepId: 0,
53
+ stepKind: "chain",
54
+ stepName: "validate",
55
+ phase: "end",
56
+ ts: 1,
57
+ durationMs: 1,
58
+ },
59
+ ],
60
+ ctxIn: { studentId: "s1" },
61
+ ctxOut: { studentId: "s1", validated: true },
62
+ capturedAt: "2026-05-29T10:00:00.000Z",
63
+ };
64
+ }
65
+ function writeFixture(rec) {
66
+ tmp = mkdtempSync(join(tmpdir(), "replay-test-"));
67
+ const path = join(tmp, "rec.json");
68
+ writeFileSync(path, JSON.stringify(rec));
69
+ return path;
70
+ }
71
+ describe("nwire replay — command meta", () => {
72
+ it("registers a `replay` subcommand with a positional file + optional --hook", () => {
73
+ expect(replayCommand.meta).toBeDefined();
74
+ const meta = replayCommand.meta;
75
+ expect(meta.name).toBe("replay");
76
+ const args = replayCommand.args;
77
+ expect(args.file).toMatchObject({ type: "positional", required: true });
78
+ expect(args.hook).toMatchObject({ type: "string" });
79
+ });
80
+ });
81
+ describe("loadRecording", () => {
82
+ it("loads + parses a recording JSON file", () => {
83
+ const rec = fixtureRecording();
84
+ const path = writeFixture(rec);
85
+ const out = loadRecording(path);
86
+ expect(out.hookName).toBe("submission.submit");
87
+ expect(out.steps).toHaveLength(2);
88
+ });
89
+ it("rejects a file whose shape doesn't look like a Recording", () => {
90
+ tmp = mkdtempSync(join(tmpdir(), "replay-test-"));
91
+ const path = join(tmp, "bad.json");
92
+ writeFileSync(path, JSON.stringify({ hello: "world" }));
93
+ expect(() => loadRecording(path)).toThrow(/not a Recording/);
94
+ });
95
+ });
96
+ describe("postReplay — happy path against a stubbed wire", () => {
97
+ it("POSTs the recording and parses the wire response", async () => {
98
+ const rec = fixtureRecording();
99
+ server = createServer((req, res) => {
100
+ if (req.method === "POST" && req.url === "/_nwire/hooks/replay") {
101
+ const chunks = [];
102
+ req.on("data", (c) => chunks.push(c));
103
+ req.on("end", () => {
104
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
105
+ expect(body.recording.hookName).toBe("submission.submit");
106
+ res.writeHead(200, { "content-type": "application/json" });
107
+ res.end(JSON.stringify({
108
+ matches: true,
109
+ drift: [],
110
+ recorded: body.recording,
111
+ replayed: { stepCount: 2, outcome: "completed" },
112
+ }));
113
+ });
114
+ return;
115
+ }
116
+ res.writeHead(404);
117
+ res.end();
118
+ });
119
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
120
+ const port = server.address().port;
121
+ const result = await postReplay({
122
+ url: `http://127.0.0.1:${port}`,
123
+ recording: rec,
124
+ });
125
+ expect(result).not.toBeNull();
126
+ expect(result.matches).toBe(true);
127
+ expect(result.replayed.stepCount).toBe(2);
128
+ });
129
+ });
130
+ describe("postReplay — graceful fallback when endpoint is missing", () => {
131
+ it("returns null on 404 (no /_nwire/hooks/replay mount)", async () => {
132
+ server = createServer((_req, res) => {
133
+ res.writeHead(404);
134
+ res.end();
135
+ });
136
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
137
+ const port = server.address().port;
138
+ const result = await postReplay({
139
+ url: `http://127.0.0.1:${port}`,
140
+ recording: fixtureRecording(),
141
+ });
142
+ expect(result).toBeNull();
143
+ });
144
+ it("returns null on 501 Not Implemented", async () => {
145
+ server = createServer((_req, res) => {
146
+ res.writeHead(501);
147
+ res.end();
148
+ });
149
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
150
+ const port = server.address().port;
151
+ const result = await postReplay({
152
+ url: `http://127.0.0.1:${port}`,
153
+ recording: fixtureRecording(),
154
+ });
155
+ expect(result).toBeNull();
156
+ });
157
+ it("throws on other non-OK statuses (5xx that isn't 501)", async () => {
158
+ server = createServer((_req, res) => {
159
+ res.writeHead(500);
160
+ res.end("boom");
161
+ });
162
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
163
+ const port = server.address().port;
164
+ await expect(postReplay({ url: `http://127.0.0.1:${port}`, recording: fixtureRecording() })).rejects.toThrow(/500/);
165
+ });
166
+ });
167
+ describe("renderOfflineDiff", () => {
168
+ it("includes the offline-only banner + the step trace", () => {
169
+ const out = renderOfflineDiff(fixtureRecording());
170
+ expect(out).toContain("/_nwire/hooks/replay");
171
+ expect(out).toContain("offline-only");
172
+ expect(out).toContain("submission.submit");
173
+ expect(out).toContain("validate");
174
+ expect(out).toContain("outcome=completed");
175
+ });
176
+ });
177
+ describe("renderReplayResult", () => {
178
+ it("prints the match banner + step alignment", () => {
179
+ const rec = fixtureRecording();
180
+ const out = renderReplayResult({
181
+ matches: true,
182
+ drift: [],
183
+ recorded: rec,
184
+ replayed: { stepCount: rec.steps.length, outcome: "completed" },
185
+ });
186
+ expect(out).toContain("matches");
187
+ expect(out).toContain("validate");
188
+ });
189
+ it("surfaces drift entries when matches=false", () => {
190
+ const rec = fixtureRecording();
191
+ const out = renderReplayResult({
192
+ matches: false,
193
+ drift: ["step[1]: recorded=chain/validate/end replayed=chain/validate/error"],
194
+ recorded: rec,
195
+ replayed: { stepCount: 2, outcome: "failed" },
196
+ });
197
+ expect(out).toContain("drift");
198
+ expect(out).toContain("validate/error");
199
+ expect(out).toContain("outcome=failed");
200
+ });
201
+ });
202
+ //# sourceMappingURL=replay.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.test.js","sourceRoot":"","sources":["../../src/__tests__/replay.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EACL,aAAa,EACb,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,IAAI,MAA0B,CAAC;AAC/B,IAAI,GAAuB,CAAC;AAE5B,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,MAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC7D,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,GAAG,GAAG,SAAS,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,SAAS,gBAAgB;IACvB,OAAO;QACL,MAAM,EAAK,QAAQ;QACnB,QAAQ,EAAG,mBAAmB;QAC9B,KAAK,EAAM,KAAK;QAChB,OAAO,EAAI,WAAW;QACtB,KAAK,EAAE;YACL;gBACE,QAAQ,EAAE,mBAAmB;gBAC7B,MAAM,EAAI,QAAQ;gBAClB,KAAK,EAAK,KAAK;gBACf,MAAM,EAAI,CAAC;gBACX,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,UAAU;gBACpB,KAAK,EAAK,OAAO;gBACjB,EAAE,EAAQ,CAAC;aACZ;YACD;gBACE,QAAQ,EAAE,mBAAmB;gBAC7B,MAAM,EAAI,QAAQ;gBAClB,KAAK,EAAK,KAAK;gBACf,MAAM,EAAI,CAAC;gBACX,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,UAAU;gBACpB,KAAK,EAAK,KAAK;gBACf,EAAE,EAAQ,CAAC;gBACX,UAAU,EAAE,CAAC;aACd;SACF;QACD,KAAK,EAAO,EAAE,SAAS,EAAE,IAAI,EAAE;QAC/B,MAAM,EAAM,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;QAChD,UAAU,EAAE,0BAA0B;KACvC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,GAAc;IAClC,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IACnC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,aAAa,CAAC,IAAyB,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,IAAI,GAAI,aAAkF,CAAC,IAAK,CAAC;QACvG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,GAAG,GAAI,gBAAgB,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAI,aAAa,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACnC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACxD,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC9D,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;QAE/B,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACjC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,sBAAsB,EAAE,CAAC;gBAChE,MAAM,MAAM,GAAa,EAAE,CAAC;gBAC5B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC9C,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;oBACjB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;oBAChE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;oBAC1D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;oBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;wBACrB,OAAO,EAAG,IAAI;wBACd,KAAK,EAAK,EAAE;wBACZ,QAAQ,EAAE,IAAI,CAAC,SAAS;wBACxB,QAAQ,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE;qBACjD,CAAC,CAAC,CAAC;gBACN,CAAC,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;QAEpD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC;YAC9B,GAAG,EAAQ,oBAAoB,IAAI,EAAE;YACrC,SAAS,EAAE,GAAG;SACf,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,MAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACvE,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YAClC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;QACpD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC;YAC9B,GAAG,EAAQ,oBAAoB,IAAI,EAAE;YACrC,SAAS,EAAE,gBAAgB,EAAE;SAC9B,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YAClC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;QACpD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC;YAC9B,GAAG,EAAQ,oBAAoB,IAAI,EAAE;YACrC,SAAS,EAAE,gBAAgB,EAAE;SAC9B,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YAClC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAC;QACpD,MAAM,MAAM,CACV,UAAU,CAAC,EAAE,GAAG,EAAE,oBAAoB,IAAI,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAC/E,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,GAAG,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;QAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,kBAAkB,CAAC;YAC7B,OAAO,EAAG,IAAI;YACd,KAAK,EAAK,EAAE;YACZ,QAAQ,EAAE,GAAG;YACb,QAAQ,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE;SAChE,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,kBAAkB,CAAC;YAC7B,OAAO,EAAG,KAAK;YACf,KAAK,EAAK,CAAC,oEAAoE,CAAC;YAChF,QAAQ,EAAE,GAAG;YACb,QAAQ,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE;SAC9C,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=trace.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/trace.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Unit tests for `nwire trace`.
3
+ *
4
+ * Long-running stream behaviour is intentionally NOT exercised end-to-end
5
+ * (timing-flakey). We test the small composable pieces: command meta,
6
+ * wire discovery against a real loopback HTTP server, the SSE parser,
7
+ * and the line/tree formatters.
8
+ */
9
+ import { describe, it, expect, afterEach } from "vitest";
10
+ import { createServer } from "node:http";
11
+ import { traceCommand, formatSummaryLine, renderTree, shortId, formatTimestamp } from "../commands/trace.js";
12
+ import { SseParser } from "../lib/sse.js";
13
+ import { findWireUrl } from "../lib/wire-discovery.js";
14
+ let server;
15
+ afterEach(async () => {
16
+ if (server) {
17
+ await new Promise((res) => server.close(() => res()));
18
+ server = undefined;
19
+ }
20
+ delete process.env.NWIRE_INSPECT_URL;
21
+ });
22
+ describe("nwire trace — command meta", () => {
23
+ it("registers a `trace` subcommand with optional --correlation and --limit", () => {
24
+ expect(traceCommand.meta).toBeDefined();
25
+ const meta = traceCommand.meta;
26
+ expect(meta.name).toBe("trace");
27
+ expect(traceCommand.args).toMatchObject({
28
+ correlation: expect.any(Object),
29
+ limit: expect.any(Object),
30
+ });
31
+ });
32
+ });
33
+ describe("findWireUrl — port-probe discovery", () => {
34
+ it("returns the URL of a stubbed HTTP server that answers /_nwire/events/recent", async () => {
35
+ server = createServer((req, res) => {
36
+ if (req.url?.startsWith("/_nwire/events/recent")) {
37
+ res.writeHead(200, { "content-type": "application/json" });
38
+ res.end("[]");
39
+ return;
40
+ }
41
+ res.writeHead(404);
42
+ res.end();
43
+ });
44
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
45
+ const port = server.address().port;
46
+ process.env.NWIRE_INSPECT_URL = `http://127.0.0.1:${port}`;
47
+ const url = await findWireUrl();
48
+ expect(url).toBe(`http://127.0.0.1:${port}`);
49
+ });
50
+ it("strips a trailing slash from NWIRE_INSPECT_URL", async () => {
51
+ process.env.NWIRE_INSPECT_URL = "http://127.0.0.1:9999/";
52
+ expect(await findWireUrl()).toBe("http://127.0.0.1:9999");
53
+ });
54
+ });
55
+ describe("SseParser — frame extraction", () => {
56
+ it("parses a single complete frame", () => {
57
+ const parser = new SseParser();
58
+ const out = parser.feed(`data: {"a":1}\n\n`);
59
+ expect(out).toEqual([{ a: 1 }]);
60
+ });
61
+ it("parses multiple frames in one feed call", () => {
62
+ const parser = new SseParser();
63
+ const out = parser.feed(`data: {"a":1}\n\ndata: {"b":2}\n\ndata: {"c":3}\n\n`);
64
+ expect(out).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]);
65
+ });
66
+ it("buffers a partial frame across two feed calls", () => {
67
+ const parser = new SseParser();
68
+ expect(parser.feed(`data: {"a":`)).toEqual([]);
69
+ expect(parser.feed(`1}\n\n`)).toEqual([{ a: 1 }]);
70
+ });
71
+ it("ignores SSE comment / heartbeat frames", () => {
72
+ const parser = new SseParser();
73
+ const out = parser.feed(`: heartbeat\n\ndata: {"k":"v"}\n\n`);
74
+ expect(out).toEqual([{ k: "v" }]);
75
+ });
76
+ it("joins multi-line data: payloads with newline", () => {
77
+ const parser = new SseParser();
78
+ const out = parser.feed(`data: {"a":\ndata: 1}\n\n`);
79
+ expect(out).toEqual([{ a: 1 }]);
80
+ });
81
+ it("tolerates \\r\\n line endings", () => {
82
+ const parser = new SseParser();
83
+ const out = parser.feed(`data: {"a":1}\r\n\r\n`);
84
+ expect(out).toEqual([{ a: 1 }]);
85
+ });
86
+ it("drops frames whose JSON is malformed", () => {
87
+ const parser = new SseParser();
88
+ expect(parser.feed(`data: {not-json}\n\n`)).toEqual([]);
89
+ });
90
+ });
91
+ describe("trace — formatters", () => {
92
+ it("formatTimestamp pads to HH:MM:SS.mmm", () => {
93
+ const ts = formatTimestamp("2026-05-29T10:05:03.040Z");
94
+ expect(ts).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3}$/);
95
+ });
96
+ it("shortId returns 8 chars (strips known prefixes)", () => {
97
+ expect(shortId("evt_abc12345678")).toBe("12345678");
98
+ expect(shortId(undefined)).toBe("--------");
99
+ });
100
+ it("formatSummaryLine includes the event name", () => {
101
+ const line = formatSummaryLine({
102
+ name: "Submission.Submitted",
103
+ envelope: {
104
+ messageId: "m_abcdefgh",
105
+ causationId: "c_zyxwvuts",
106
+ correlationId: "k_corrcorr",
107
+ occurredAt: "2026-05-29T10:00:00.000Z",
108
+ },
109
+ });
110
+ // strip ANSI for readability — picocolors emits codes only when stdout is a TTY,
111
+ // so under vitest most colours are dropped already.
112
+ expect(line).toContain("Submission.Submitted");
113
+ expect(line).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
114
+ });
115
+ it("renderTree nests children under their causation parent", () => {
116
+ const tree = renderTree([
117
+ { name: "Root", envelope: { messageId: "ROOT" } },
118
+ { name: "ChildA", envelope: { messageId: "A", causationId: "ROOT" } },
119
+ { name: "ChildB", envelope: { messageId: "B", causationId: "ROOT" } },
120
+ { name: "GrandA", envelope: { messageId: "GA", causationId: "A" } },
121
+ ]);
122
+ expect(tree).toContain("Root");
123
+ expect(tree).toContain("ChildA");
124
+ expect(tree).toContain("ChildB");
125
+ expect(tree).toContain("GrandA");
126
+ // ChildA appears before GrandA in the output (parent before child)
127
+ expect(tree.indexOf("ChildA")).toBeLessThan(tree.indexOf("GrandA"));
128
+ });
129
+ it("renderTree treats orphans as additional roots", () => {
130
+ const tree = renderTree([
131
+ { name: "Orphan", envelope: { messageId: "X", causationId: "missing-parent" } },
132
+ ]);
133
+ expect(tree).toContain("Orphan");
134
+ });
135
+ });
136
+ //# sourceMappingURL=trace.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.test.js","sourceRoot":"","sources":["../../src/__tests__/trace.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=watch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/watch.test.ts"],"names":[],"mappings":""}