@wenrwa/marketplace-cli 0.4.6 → 0.5.0-lws.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/dist/__tests__/bridge-agent-launch.test.d.ts +1 -0
  2. package/dist/__tests__/bridge-agent-launch.test.js +47 -0
  3. package/dist/__tests__/bridge-browser-driver.test.js +65 -0
  4. package/dist/__tests__/bridge-browser-relay.test.js +70 -0
  5. package/dist/__tests__/bridge-cdp-transport-race.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-cdp-transport-race.test.js +79 -0
  7. package/dist/__tests__/bridge-dev-server-detect.test.d.ts +1 -0
  8. package/dist/__tests__/bridge-dev-server-detect.test.js +94 -0
  9. package/dist/__tests__/bridge-fail-fast.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-fail-fast.test.js +597 -0
  11. package/dist/__tests__/bridge-gitignore-matcher.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-gitignore-matcher.test.js +55 -0
  13. package/dist/__tests__/bridge-local-context-no-self-echo.test.d.ts +1 -0
  14. package/dist/__tests__/bridge-local-context-no-self-echo.test.js +106 -0
  15. package/dist/__tests__/bridge-local-context-writer.test.d.ts +1 -0
  16. package/dist/__tests__/bridge-local-context-writer.test.js +120 -0
  17. package/dist/__tests__/bridge-local-tree-service.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-local-tree-service.test.js +121 -0
  19. package/dist/__tests__/bridge-publish-preflight.test.d.ts +1 -0
  20. package/dist/__tests__/bridge-publish-preflight.test.js +194 -0
  21. package/dist/__tests__/bridge-relay-client.test.js +356 -2
  22. package/dist/__tests__/bridge-server-dev-server-frame.test.d.ts +1 -0
  23. package/dist/__tests__/bridge-server-dev-server-frame.test.js +111 -0
  24. package/dist/__tests__/bridge-server-local-context.test.d.ts +1 -0
  25. package/dist/__tests__/bridge-server-local-context.test.js +158 -0
  26. package/dist/__tests__/bridge-server-local-publish.test.d.ts +1 -0
  27. package/dist/__tests__/bridge-server-local-publish.test.js +102 -0
  28. package/dist/__tests__/bridge-server-publish-preflight.test.d.ts +1 -0
  29. package/dist/__tests__/bridge-server-publish-preflight.test.js +133 -0
  30. package/dist/__tests__/bridge-server-verify-origin.test.d.ts +1 -0
  31. package/dist/__tests__/bridge-server-verify-origin.test.js +107 -0
  32. package/dist/__tests__/bridge-server.test.js +301 -2
  33. package/dist/__tests__/bridge-session-manager.test.js +270 -23
  34. package/dist/__tests__/bridge-tree-scanner.test.d.ts +1 -0
  35. package/dist/__tests__/bridge-tree-scanner.test.js +174 -0
  36. package/dist/__tests__/bridge-tree-watcher.test.d.ts +1 -0
  37. package/dist/__tests__/bridge-tree-watcher.test.js +111 -0
  38. package/dist/__tests__/bridge-workspace-multirepo.test.js +109 -0
  39. package/dist/__tests__/localPublish.test.d.ts +1 -0
  40. package/dist/__tests__/localPublish.test.js +256 -0
  41. package/dist/__tests__/originVerification.test.d.ts +1 -0
  42. package/dist/__tests__/originVerification.test.js +160 -0
  43. package/dist/__tests__/term-snapshot.test.d.ts +1 -0
  44. package/dist/__tests__/term-snapshot.test.js +80 -0
  45. package/dist/__tests__/writeGitignore.test.d.ts +1 -0
  46. package/dist/__tests__/writeGitignore.test.js +59 -0
  47. package/dist/bridge/agentLaunch.d.ts +31 -0
  48. package/dist/bridge/agentLaunch.js +18 -0
  49. package/dist/bridge/browserDriver.d.ts +25 -0
  50. package/dist/bridge/browserDriver.js +70 -3
  51. package/dist/bridge/devServerDetect.d.ts +41 -0
  52. package/dist/bridge/devServerDetect.js +123 -0
  53. package/dist/bridge/gitignoreMatcher.d.ts +19 -0
  54. package/dist/bridge/gitignoreMatcher.js +0 -0
  55. package/dist/bridge/localContextWriter.d.ts +30 -0
  56. package/dist/bridge/localContextWriter.js +80 -0
  57. package/dist/bridge/localPublish.d.ts +15 -0
  58. package/dist/bridge/localPublish.js +82 -0
  59. package/dist/bridge/localTreeService.d.ts +38 -0
  60. package/dist/bridge/localTreeService.js +51 -0
  61. package/dist/bridge/originVerification.d.ts +17 -0
  62. package/dist/bridge/originVerification.js +55 -0
  63. package/dist/bridge/publishPreflight.d.ts +7 -0
  64. package/dist/bridge/publishPreflight.js +152 -0
  65. package/dist/bridge/realPty.js +6 -2
  66. package/dist/bridge/relayClient.d.ts +55 -0
  67. package/dist/bridge/relayClient.js +232 -33
  68. package/dist/bridge/repoCheckout.js +46 -10
  69. package/dist/bridge/server.d.ts +103 -0
  70. package/dist/bridge/server.js +299 -21
  71. package/dist/bridge/sessionManager.d.ts +110 -6
  72. package/dist/bridge/sessionManager.js +202 -13
  73. package/dist/bridge/termSnapshot.d.ts +36 -0
  74. package/dist/bridge/termSnapshot.js +66 -0
  75. package/dist/bridge/treeScanner.d.ts +25 -0
  76. package/dist/bridge/treeScanner.js +125 -0
  77. package/dist/bridge/treeWatcher.d.ts +17 -0
  78. package/dist/bridge/treeWatcher.js +116 -0
  79. package/dist/bridge/writeGitignore.d.ts +15 -0
  80. package/dist/bridge/writeGitignore.js +73 -0
  81. package/dist/commands/bridge.d.ts +13 -0
  82. package/dist/commands/bridge.js +187 -1
  83. package/dist/index.js +0 -0
  84. package/package.json +3 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const agentLaunch_1 = require("../bridge/agentLaunch");
4
+ /**
5
+ * TDD — lws-06: pure decision of what the bridge spawns for an agent-scoped
6
+ * PTY (ADR-0035 D4). `claude`, never $SHELL — autonomous maps to
7
+ * --dangerously-skip-permissions ONLY when the agent opts in; default OFF.
8
+ *
9
+ * lws-08 (PRD D9) — every agent-scoped launch also carries a minimal env
10
+ * pointer at `.hiveop/context.md` (relative to the spawn cwd, the project
11
+ * root). This is the ONLY mechanism used to point a spawned `claude` at the
12
+ * manifest: no args, no stdin injection, no CLAUDE.md edit. `claude` itself
13
+ * doesn't read this var natively today — it exists so the context file's
14
+ * location is a documented, stable contract other tooling (a future MCP
15
+ * resource, a slash command, a system-prompt append) can rely on instead of
16
+ * hardcoding the path, and so a human/agent inspecting `env` in the PTY can
17
+ * discover it. Deliberately NOT wired into --append-system-prompt or similar
18
+ * — keeping this additive-only avoids coupling the launch decision to
19
+ * `claude` CLI flag churn.
20
+ */
21
+ describe('resolveAgentLaunch (lws-06)', () => {
22
+ it('defaults to a bare `claude` invocation (autonomous OFF)', () => {
23
+ expect((0, agentLaunch_1.resolveAgentLaunch)({ autonomous: false })).toEqual({
24
+ command: 'claude',
25
+ args: [],
26
+ env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
27
+ });
28
+ });
29
+ it('an agent-scoped launch with autonomous unset also defaults OFF', () => {
30
+ expect((0, agentLaunch_1.resolveAgentLaunch)({})).toEqual({
31
+ command: 'claude',
32
+ args: [],
33
+ env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
34
+ });
35
+ });
36
+ it('autonomous:true maps to --dangerously-skip-permissions', () => {
37
+ expect((0, agentLaunch_1.resolveAgentLaunch)({ autonomous: true })).toEqual({
38
+ command: 'claude',
39
+ args: ['--dangerously-skip-permissions'],
40
+ env: { [agentLaunch_1.HIVEOP_CONTEXT_ENV_VAR]: agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE },
41
+ });
42
+ });
43
+ it('the context env var always points at the relative .hiveop/context.md path', () => {
44
+ expect(agentLaunch_1.HIVEOP_CONTEXT_ENV_VALUE).toBe('.hiveop/context.md');
45
+ expect((0, agentLaunch_1.resolveAgentLaunch)({}).env).toEqual({ HIVEOP_CONTEXT_FILE: '.hiveop/context.md' });
46
+ });
47
+ });
@@ -210,6 +210,71 @@ describe('createBrowserDriver (cbs-02)', () => {
210
210
  expect(calls[0].params).toEqual({ text: 'hunter2' });
211
211
  expect(calls[0].sessionId).toBe('sess-1');
212
212
  });
213
+ // Live bug fix: Page.startScreencast only emits Page.screencastFrame on
214
+ // an actual repaint, so an idle/already-loaded page (or a pane that
215
+ // reattaches to one) gets nothing new. refresh() is the forcing
216
+ // function: re-emit whatever we last delivered, then restart the
217
+ // screencast so Chromium produces a genuinely fresh frame.
218
+ describe('refresh()', () => {
219
+ it('re-emits the cached last-delivered frame synchronously', async () => {
220
+ const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
221
+ const frames = [];
222
+ driver.onFrame((f) => frames.push(f));
223
+ await driver.start('https://example.com');
224
+ endpoint.emitEvent('Page.screencastFrame', { data: 'first', sessionId: 1, metadata: {} }, 'sess-1');
225
+ expect(frames.map((f) => f.data)).toEqual(['first']);
226
+ void driver.refresh();
227
+ // The cached frame is re-emitted synchronously, before any CDP
228
+ // round-trip for the forced-fresh frame completes.
229
+ expect(frames.map((f) => f.data)).toEqual(['first', 'first']);
230
+ });
231
+ it('restarts the screencast (stopScreencast then startScreencast, same params) on the attached session', async () => {
232
+ const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
233
+ await driver.start('https://example.com');
234
+ endpoint.sent.length = 0;
235
+ await driver.refresh();
236
+ const methods = endpoint.sent.map((m) => m.method);
237
+ expect(methods).toEqual(['Page.stopScreencast', 'Page.startScreencast']);
238
+ const stop = endpoint.sentCalls('Page.stopScreencast')[0];
239
+ expect(stop.sessionId).toBe('sess-1');
240
+ expect(stop.params).toEqual({});
241
+ const start = endpoint.sentCalls('Page.startScreencast')[0];
242
+ expect(start.sessionId).toBe('sess-1');
243
+ expect(start.params).toEqual({ format: 'jpeg', quality: 60, maxWidth: 1280, maxHeight: 800 });
244
+ });
245
+ it('a restarted screencast delivers the forced-fresh frame through onFrame once Chromium emits it', async () => {
246
+ const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
247
+ const frames = [];
248
+ driver.onFrame((f) => frames.push(f));
249
+ await driver.start('https://example.com');
250
+ endpoint.emitEvent('Page.screencastFrame', { data: 'stale', sessionId: 1, metadata: {} }, 'sess-1');
251
+ await driver.refresh();
252
+ endpoint.emitEvent('Page.screencastFrame', { data: 'fresh', sessionId: 2, metadata: {} }, 'sess-1');
253
+ expect(frames.map((f) => f.data)).toEqual(['stale', 'stale', 'fresh']);
254
+ });
255
+ it('called before start() resolves is a safe no-op (nothing cached, no session to restart)', async () => {
256
+ const { driver } = setup({ existingPageTargetId: 't1' });
257
+ await expect(driver.refresh()).resolves.toBeUndefined();
258
+ });
259
+ it('un-wedges a pacer stuck on a previous consumer\'s unacked frame, so the next pushed frame delivers immediately', async () => {
260
+ const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
261
+ const frames = [];
262
+ driver.onFrame((f) => frames.push(f));
263
+ await driver.start('https://example.com');
264
+ // One frame lands and is delivered; nobody ever acks it (the pane
265
+ // that received it is gone) and — matching the real idle-page bug —
266
+ // nothing further is pushed, so nothing is queued behind it either.
267
+ // The pacer is left permanently "awaitingAck" with no live consumer.
268
+ endpoint.emitEvent('Page.screencastFrame', { data: 'stale', sessionId: 1, metadata: {} }, 'sess-1');
269
+ expect(frames.map((f) => f.data)).toEqual(['stale']);
270
+ await driver.refresh();
271
+ // A newly (re)attached pane must see the next captured frame land
272
+ // immediately — WITHOUT anyone having called ackFrame() — instead of
273
+ // it silently queuing behind a phantom ack that will never come.
274
+ endpoint.emitEvent('Page.screencastFrame', { data: 'new-attach-frame', sessionId: 2, metadata: {} }, 'sess-1');
275
+ expect(frames[frames.length - 1].data).toBe('new-attach-frame');
276
+ });
277
+ });
213
278
  it('navigate(url) after start sends Page.navigate again on the attached target session', async () => {
214
279
  const { driver, endpoint } = setup({ existingPageTargetId: 't1' });
215
280
  await driver.start('https://example.com');
@@ -31,6 +31,7 @@ const makeFactory = () => ({ spawn: (o) => new FakePty(o) });
31
31
  class FakeBrowserDriver {
32
32
  startCalls = [];
33
33
  navigateCalls = [];
34
+ refreshCalls = 0;
34
35
  inputCalls = [];
35
36
  stopCalls = 0;
36
37
  sleepCalls = [];
@@ -43,6 +44,9 @@ class FakeBrowserDriver {
43
44
  async navigate(url) {
44
45
  this.navigateCalls.push(url);
45
46
  }
47
+ async refresh() {
48
+ this.refreshCalls += 1;
49
+ }
46
50
  input(event) {
47
51
  this.inputCalls.push(event);
48
52
  }
@@ -208,6 +212,72 @@ describe('relay client — browser-open/browser-input (cbs-02)', () => {
208
212
  expect(drivers).toHaveLength(1);
209
213
  expect(drivers[0].startCalls).toEqual(['https://example.com']);
210
214
  expect(drivers[0].navigateCalls).toEqual(['https://example.com/other']);
215
+ expect(drivers[0].refreshCalls).toBe(0);
216
+ handle.close();
217
+ await closeRelay(relay.wss);
218
+ });
219
+ // Live bug fix: a pane that closes and reopens the Browser view for the
220
+ // SAME session/url is a reattach, not a navigation — navigate()-ing an
221
+ // already-loaded page is a correct-but-useless no-visible-effect call
222
+ // (see browserDriver's refresh() doc comment for why). refresh() is what
223
+ // actually gets the returning pane a frame.
224
+ it('browser-open on an already-open session with the SAME url calls refresh(), not navigate()', async () => {
225
+ const relay = await startFakeRelay();
226
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
227
+ const drivers = [];
228
+ const handle = (0, relayClient_1.startRelayClient)({
229
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
230
+ relayToken: 'tok',
231
+ sessions,
232
+ createBrowserDriver: () => {
233
+ const d = new FakeBrowserDriver();
234
+ drivers.push(d);
235
+ return d;
236
+ },
237
+ });
238
+ const ws = await relay.nextConnection();
239
+ const q = frameQueue(ws);
240
+ await q.next(); // auth
241
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
242
+ await new Promise((r) => setTimeout(r, 20));
243
+ // Reattach: same sessionId, same url.
244
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
245
+ await new Promise((r) => setTimeout(r, 20));
246
+ expect(drivers).toHaveLength(1);
247
+ expect(drivers[0].startCalls).toEqual(['https://example.com']);
248
+ expect(drivers[0].navigateCalls).toEqual([]);
249
+ expect(drivers[0].refreshCalls).toBe(1);
250
+ handle.close();
251
+ await closeRelay(relay.wss);
252
+ });
253
+ // A url change after a same-url reattach must still navigate — refresh()
254
+ // is only for the no-op-navigation case.
255
+ it('browser-open with a DIFFERENT url after a same-url reattach still navigates', async () => {
256
+ const relay = await startFakeRelay();
257
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
258
+ const drivers = [];
259
+ const handle = (0, relayClient_1.startRelayClient)({
260
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
261
+ relayToken: 'tok',
262
+ sessions,
263
+ createBrowserDriver: () => {
264
+ const d = new FakeBrowserDriver();
265
+ drivers.push(d);
266
+ return d;
267
+ },
268
+ });
269
+ const ws = await relay.nextConnection();
270
+ const q = frameQueue(ws);
271
+ await q.next(); // auth
272
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' }));
273
+ await new Promise((r) => setTimeout(r, 20));
274
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com' })); // reattach
275
+ await new Promise((r) => setTimeout(r, 20));
276
+ ws.send(JSON.stringify({ t: 'browser-open', sessionId: 'browser:c1', url: 'https://example.com/other' })); // real nav
277
+ await new Promise((r) => setTimeout(r, 20));
278
+ expect(drivers).toHaveLength(1);
279
+ expect(drivers[0].refreshCalls).toBe(1);
280
+ expect(drivers[0].navigateCalls).toEqual(['https://example.com/other']);
211
281
  handle.close();
212
282
  await closeRelay(relay.wss);
213
283
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const browserDriver_1 = require("../bridge/browserDriver");
4
+ /**
5
+ * Regression test for the first-light hang: the driver sends its first CDP
6
+ * command (Target.getTargets) the moment the transport is constructed, while
7
+ * the DevTools socket is still CONNECTING. The original transport dropped
8
+ * anything sent before OPEN, so that command never reached Chromium and
9
+ * start() hung forever with no error. Frames never flowed.
10
+ *
11
+ * The real `ws` path is otherwise untested by design (mocks are injected at
12
+ * wsFactory), which is exactly why the bug survived to production — so this
13
+ * test drives a fake WebSocket through the real transport instead.
14
+ */
15
+ class FakeSocket {
16
+ url;
17
+ static CONNECTING = 0;
18
+ static OPEN = 1;
19
+ static CLOSING = 2;
20
+ static CLOSED = 3;
21
+ readyState = FakeSocket.CONNECTING;
22
+ sent = [];
23
+ handlers = new Map();
24
+ constructor(url) {
25
+ this.url = url;
26
+ }
27
+ send(d) { this.sent.push(d); }
28
+ close() { this.readyState = FakeSocket.CLOSED; }
29
+ on(event, cb) {
30
+ const set = this.handlers.get(event) ?? [];
31
+ set.push(cb);
32
+ this.handlers.set(event, set);
33
+ }
34
+ /** simulate the socket finishing its handshake */
35
+ fireOpen() {
36
+ this.readyState = FakeSocket.OPEN;
37
+ for (const cb of this.handlers.get('open') ?? [])
38
+ cb();
39
+ }
40
+ }
41
+ // The transport reads OPEN/CONNECTING off the constructor it is given.
42
+ function makeTransport() {
43
+ let created;
44
+ class Tracked extends FakeSocket {
45
+ constructor(url) { super(url); created = this; }
46
+ }
47
+ const transport = (0, browserDriver_1.createDefaultTransport)('ws://127.0.0.1:1/devtools/browser/x', Tracked);
48
+ return { transport, socket: created };
49
+ }
50
+ describe('CDP default transport — connect race', () => {
51
+ it('buffers a command sent while CONNECTING and flushes it on open', () => {
52
+ const { transport, socket } = makeTransport();
53
+ transport.send('{"id":1,"method":"Target.getTargets"}');
54
+ expect(socket.sent).toEqual([]); // still connecting — must NOT be lost
55
+ socket.fireOpen();
56
+ expect(socket.sent).toEqual(['{"id":1,"method":"Target.getTargets"}']);
57
+ });
58
+ it('preserves order across the open boundary', () => {
59
+ const { transport, socket } = makeTransport();
60
+ transport.send('a');
61
+ transport.send('b');
62
+ socket.fireOpen();
63
+ transport.send('c');
64
+ expect(socket.sent).toEqual(['a', 'b', 'c']);
65
+ });
66
+ it('sends straight through once open', () => {
67
+ const { transport, socket } = makeTransport();
68
+ socket.fireOpen();
69
+ transport.send('x');
70
+ expect(socket.sent).toEqual(['x']);
71
+ });
72
+ it('drops (without throwing) once the socket is closed', () => {
73
+ const { transport, socket } = makeTransport();
74
+ socket.fireOpen();
75
+ socket.close();
76
+ expect(() => transport.send('late')).not.toThrow();
77
+ expect(socket.sent).toEqual([]);
78
+ });
79
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * TDD — lws-08 (PRD D7 nice-to-have): pure dev-server-banner matcher over
5
+ * agent PTY output. Table-driven per common framework so the false-positive
6
+ * surface stays small (a bare "listening on" fallback would false-trigger on
7
+ * ordinary chatter); every framework line must ALSO resolve to a
8
+ * localhost/127.0.0.1/0.0.0.0/[::1] URL, since that is the whole point
9
+ * (offer a Browser pane at a port on THIS machine).
10
+ */
11
+ const devServerDetect_1 = require("../bridge/devServerDetect");
12
+ describe('matchDevServerLine — per-framework banner table', () => {
13
+ it('Next.js — "- Local: http://localhost:3000"', () => {
14
+ expect((0, devServerDetect_1.matchDevServerLine)(' - Local: http://localhost:3000')).toEqual({
15
+ framework: 'Next.js', port: 3000, url: 'http://localhost:3000',
16
+ });
17
+ });
18
+ it('Vite — "➜ Local: http://localhost:5173/"', () => {
19
+ expect((0, devServerDetect_1.matchDevServerLine)(' ➜ Local: http://localhost:5173/')).toEqual({
20
+ framework: 'Vite', port: 5173, url: 'http://localhost:5173/',
21
+ });
22
+ });
23
+ it('Create React App — "Local: http://localhost:3000"', () => {
24
+ expect((0, devServerDetect_1.matchDevServerLine)('You can now view app in the browser.\n Local: http://localhost:3000')).toEqual({
25
+ framework: 'CRA', port: 3000, url: 'http://localhost:3000',
26
+ });
27
+ });
28
+ it('Rails/Puma — "Listening on http://127.0.0.1:3000"', () => {
29
+ expect((0, devServerDetect_1.matchDevServerLine)('* Listening on http://127.0.0.1:3000')).toEqual({
30
+ framework: 'Rails', port: 3000, url: 'http://127.0.0.1:3000',
31
+ });
32
+ });
33
+ it('Django — "Starting development server at http://127.0.0.1:8000/"', () => {
34
+ expect((0, devServerDetect_1.matchDevServerLine)('Starting development server at http://127.0.0.1:8000/')).toEqual({
35
+ framework: 'Django', port: 8000, url: 'http://127.0.0.1:8000/',
36
+ });
37
+ });
38
+ it('generic fallback — "Server ready on http://localhost:4000"', () => {
39
+ expect((0, devServerDetect_1.matchDevServerLine)('Server ready on http://localhost:4000')).toEqual({
40
+ framework: 'generic', port: 4000, url: 'http://localhost:4000',
41
+ });
42
+ });
43
+ it('generic fallback — "App listening on 0.0.0.0:8080"', () => {
44
+ expect((0, devServerDetect_1.matchDevServerLine)('App listening on 0.0.0.0:8080')).toEqual({
45
+ framework: 'generic', port: 8080, url: 'http://0.0.0.0:8080',
46
+ });
47
+ });
48
+ it('strips ANSI color codes before matching', () => {
49
+ const line = '\x1b[32m - Local:\x1b[39m \x1b[1mhttp://localhost:3000\x1b[22m';
50
+ expect((0, devServerDetect_1.matchDevServerLine)(line)).toEqual({ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' });
51
+ });
52
+ it('a non-localhost URL never matches (the point is offering a pane on THIS machine)', () => {
53
+ expect((0, devServerDetect_1.matchDevServerLine)('- Local: http://example.com:3000')).toBeNull();
54
+ });
55
+ it('no false positives on ordinary agent chatter', () => {
56
+ expect((0, devServerDetect_1.matchDevServerLine)('Running tests...')).toBeNull();
57
+ expect((0, devServerDetect_1.matchDevServerLine)('$ npm install')).toBeNull();
58
+ expect((0, devServerDetect_1.matchDevServerLine)('Reading file src/index.ts')).toBeNull();
59
+ expect((0, devServerDetect_1.matchDevServerLine)('I will now start the server for you.')).toBeNull();
60
+ expect((0, devServerDetect_1.matchDevServerLine)('See http://localhost:3000 for more info in the README')).toBeNull();
61
+ expect((0, devServerDetect_1.matchDevServerLine)('')).toBeNull();
62
+ });
63
+ });
64
+ describe('createDevServerWatcher — feed(chunk), dedupe/debounce per session', () => {
65
+ it('emits a match once a full banner line arrives', () => {
66
+ const w = (0, devServerDetect_1.createDevServerWatcher)();
67
+ const matches = w.feed('some output\n- Local: http://localhost:3000\nmore output\n');
68
+ expect(matches).toEqual([{ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' }]);
69
+ });
70
+ it('dedupes — the same port is never emitted twice for one watcher instance', () => {
71
+ const w = (0, devServerDetect_1.createDevServerWatcher)();
72
+ expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(1);
73
+ expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(0);
74
+ // Even framed differently (Vite banner, same port) — still a dupe on port.
75
+ expect(w.feed('➜ Local: http://localhost:3000/\n')).toHaveLength(0);
76
+ });
77
+ it('emits separately for genuinely different ports', () => {
78
+ const w = (0, devServerDetect_1.createDevServerWatcher)();
79
+ expect(w.feed('- Local: http://localhost:3000\n')).toHaveLength(1);
80
+ expect(w.feed('- Local: http://localhost:5173\n')).toEqual([
81
+ { framework: 'Next.js', port: 5173, url: 'http://localhost:5173' },
82
+ ]);
83
+ });
84
+ it('handles a banner split across two chunks (buffers the incomplete line)', () => {
85
+ const w = (0, devServerDetect_1.createDevServerWatcher)();
86
+ expect(w.feed('- Local: http://localhost:')).toHaveLength(0);
87
+ expect(w.feed('3000\n')).toEqual([{ framework: 'Next.js', port: 3000, url: 'http://localhost:3000' }]);
88
+ });
89
+ it('no false positives from a burst of ordinary PTY output', () => {
90
+ const w = (0, devServerDetect_1.createDevServerWatcher)();
91
+ const chunk = ['$ npm install', 'added 200 packages', '$ npm run build', 'Compiled successfully.'].join('\n');
92
+ expect(w.feed(chunk)).toHaveLength(0);
93
+ });
94
+ });
@@ -0,0 +1 @@
1
+ export {};