@hover-dev/core 0.10.0 → 0.12.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/dist/plugin-api.d.ts +34 -0
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +125 -1
- package/dist/specs/listSpecs.d.ts +40 -0
- package/dist/specs/listSpecs.d.ts.map +1 -0
- package/dist/specs/listSpecs.js +114 -0
- package/package.json +1 -1
package/dist/plugin-api.d.ts
CHANGED
|
@@ -149,8 +149,42 @@ export interface HoverPluginManifest {
|
|
|
149
149
|
* inside their server-side entry. If absent, the plugin contributes
|
|
150
150
|
* no widget code (server-side-only plugin). */
|
|
151
151
|
widgetEntry?: string;
|
|
152
|
+
/** v0.12 — plugin-contributed save handlers. The widget Save dropdown
|
|
153
|
+
* picks up these entries via the host API (`host.registerSaveEntry`)
|
|
154
|
+
* and the service routes incoming `save:<type>` WS messages to the
|
|
155
|
+
* plugin's handler. Each plugin owns its own write semantics — the
|
|
156
|
+
* service does NOT touch the payload, it just delivers it. Letting
|
|
157
|
+
* plugins write entirely different artefacts (security regression
|
|
158
|
+
* specs, performance reports, …) without forcing them into core's
|
|
159
|
+
* SkillStep[] shape. */
|
|
160
|
+
saveHandlers?: HoverPluginSaveHandler[];
|
|
152
161
|
hooks?: HoverHooks;
|
|
153
162
|
}
|
|
163
|
+
export interface HoverPluginSaveHandler {
|
|
164
|
+
/** WS message type the widget sends — the service uses this verbatim
|
|
165
|
+
* in its router. Convention: `save:<plugin>:<kind>`. Example:
|
|
166
|
+
* `'save:security:spec'`. Must be unique across all loaded plugins. */
|
|
167
|
+
type: string;
|
|
168
|
+
/** UI label shown in the widget's Save dropdown. Example: "Security spec". */
|
|
169
|
+
label: string;
|
|
170
|
+
/** Optional short hint shown under the label. Example: "Playwright
|
|
171
|
+
* regression spec for the IDOR / authz probes the agent recorded." */
|
|
172
|
+
description?: string;
|
|
173
|
+
/** Modes in which this Save entry is offered. Defaults to the
|
|
174
|
+
* plugin's own mode (or `['*']` if the plugin has no mode). */
|
|
175
|
+
activeInModes?: string[];
|
|
176
|
+
/** Server-side handler. Receives the raw payload the widget sent
|
|
177
|
+
* alongside `devRoot`. Returns the on-disk path + slug for the
|
|
178
|
+
* service to echo back as `<type>:saved`. Throw to signal failure;
|
|
179
|
+
* service surfaces the error message to the widget. */
|
|
180
|
+
handle(ctx: {
|
|
181
|
+
devRoot: string;
|
|
182
|
+
payload: unknown;
|
|
183
|
+
}): Promise<{
|
|
184
|
+
path: string;
|
|
185
|
+
slug: string;
|
|
186
|
+
}>;
|
|
187
|
+
}
|
|
154
188
|
/**
|
|
155
189
|
* Branded factory that wraps a plugin manifest factory. The wrapper
|
|
156
190
|
* - asserts `apiVersion` matches this core's version at construction time
|
package/dist/plugin-api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA6EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;6EAGyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiDD,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqyB/E"}
|
package/dist/service.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* { type: 'skill-saved', payload: { name, path } }
|
|
14
14
|
* { type: 'skill-exists', payload: { slug, existingPath } }
|
|
15
15
|
* { type: 'skills-list', payload: { skills: SkillSummary[] } }
|
|
16
|
+
* { type: 'specs-list', payload: { specs: SpecSummary[] } }
|
|
16
17
|
* { type: 'spec-saved', payload: { name, path } }
|
|
17
18
|
* { type: 'spec-exists', payload: { slug, existingPath } }
|
|
18
19
|
* { type: 'case-csv-saved', payload: { name, path } }
|
|
@@ -20,7 +21,12 @@
|
|
|
20
21
|
* { type: 'error', payload: { message } }
|
|
21
22
|
*
|
|
22
23
|
* client → server
|
|
23
|
-
* { type: 'command', payload: { text, sessionId
|
|
24
|
+
* { type: 'command', payload: { text, sessionId?, reRecord?: { slug } } }
|
|
25
|
+
* // when reRecord.slug is set, the
|
|
26
|
+
* // service collects tool_use events
|
|
27
|
+
* // into a step list and on a clean
|
|
28
|
+
* // session_end overwrites
|
|
29
|
+
* // __vibe_tests__/<slug>.spec.ts
|
|
24
30
|
* { type: 'cancel' }
|
|
25
31
|
* { type: 'check-cdp', payload: { pageUrl } } // "is this widget in the debug Chrome?"
|
|
26
32
|
* { type: 'launch-chrome', payload: { pageUrl } } // start debug Chrome, navigate to pageUrl
|
|
@@ -29,6 +35,7 @@
|
|
|
29
35
|
* { type: 'save-spec', payload: { name, description, steps, assertions?, overwrite? } }
|
|
30
36
|
* { type: 'save-case-csv', payload: { name, description, steps, assertions?, jiraProjectKey?, labels?, overwrite? } }
|
|
31
37
|
* { type: 'list-skills' }
|
|
38
|
+
* { type: 'list-specs' } // ask for every spec under __vibe_tests__/, with parsed JSDoc headers
|
|
32
39
|
* { type: 'list-agents' } // ask for the full agent registry + install status
|
|
33
40
|
* { type: 'switch-agent', payload: { agentId } } // set the service's current agent; broadcasts to all connections
|
|
34
41
|
*
|
|
@@ -48,6 +55,7 @@ import { getAgent } from './agents/registry.js';
|
|
|
48
55
|
import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
|
|
49
56
|
import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
|
|
50
57
|
import { listSkills } from './skills/writeSkill.js';
|
|
58
|
+
import { listSpecs } from './specs/listSpecs.js';
|
|
51
59
|
import { send, sendIfOpen } from './service/types.js';
|
|
52
60
|
import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
|
|
53
61
|
import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
|
|
@@ -525,6 +533,15 @@ export async function startService(opts) {
|
|
|
525
533
|
send(ws, { type: 'skills-list', payload: { skills } });
|
|
526
534
|
return;
|
|
527
535
|
}
|
|
536
|
+
if (msg.type === 'list-specs') {
|
|
537
|
+
// Widget asks for every spec under <devRoot>/__vibe_tests__/ so it
|
|
538
|
+
// can render the Specs tab in the Saved-sessions overlay. Each
|
|
539
|
+
// summary carries `originalPrompt` (parsed from the JSDoc header)
|
|
540
|
+
// so the Re-record button can resubmit it as a normal command.
|
|
541
|
+
const specs = await listSpecs(devRoot);
|
|
542
|
+
send(ws, { type: 'specs-list', payload: { specs } });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
528
545
|
if (msg.type === 'save-spec') {
|
|
529
546
|
await handleSaveArtifact(ws, msg, devRoot, SPEC_CONFIG);
|
|
530
547
|
return;
|
|
@@ -533,6 +550,39 @@ export async function startService(opts) {
|
|
|
533
550
|
await handleSaveArtifact(ws, msg, devRoot, CASE_CSV_CONFIG);
|
|
534
551
|
return;
|
|
535
552
|
}
|
|
553
|
+
// v0.12 — plugin-contributed save handlers. Lookup is O(plugins),
|
|
554
|
+
// which is fine because there's at most a handful of plugins ever
|
|
555
|
+
// loaded. Each plugin's manifest declares `saveHandlers[].type`
|
|
556
|
+
// as the WS message type the widget sends; we match exactly.
|
|
557
|
+
if (typeof msg.type === 'string' && msg.type.startsWith('save:')) {
|
|
558
|
+
for (const p of plugins) {
|
|
559
|
+
const handler = p.saveHandlers?.find((h) => h.type === msg.type);
|
|
560
|
+
if (!handler)
|
|
561
|
+
continue;
|
|
562
|
+
try {
|
|
563
|
+
const result = await handler.handle({ devRoot, payload: msg.payload });
|
|
564
|
+
send(ws, {
|
|
565
|
+
type: `${msg.type}:saved`,
|
|
566
|
+
payload: { name: result.slug, path: result.path },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
571
|
+
send(ws, {
|
|
572
|
+
type: 'error',
|
|
573
|
+
payload: { message: `${msg.type}: ${m}` },
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// No plugin matched — surface as a normal error rather than
|
|
579
|
+
// silently swallowing.
|
|
580
|
+
send(ws, {
|
|
581
|
+
type: 'error',
|
|
582
|
+
payload: { message: `no plugin registered for save type "${msg.type}"` },
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
536
586
|
if (msg.type === 'check-cdp') {
|
|
537
587
|
await handleCheckCdp(ws, msg, cdpUrl, effectiveLaunchExtras());
|
|
538
588
|
return;
|
|
@@ -551,6 +601,15 @@ export async function startService(opts) {
|
|
|
551
601
|
const resumeSessionId = typeof msg.payload?.sessionId === 'string' && msg.payload.sessionId.length > 0
|
|
552
602
|
? msg.payload.sessionId
|
|
553
603
|
: undefined;
|
|
604
|
+
// Re-record mode: when the client (widget Specs tab or hover CLI)
|
|
605
|
+
// passes `reRecord: { slug }`, we collect tool_use events server-side
|
|
606
|
+
// into a SkillStep[] and, on session_end with no error, overwrite the
|
|
607
|
+
// existing __vibe_tests__/<slug>.spec.ts. This is the same flow the
|
|
608
|
+
// widget uses for "Save as Spec", but the spec already exists and is
|
|
609
|
+
// being regenerated for the current UI.
|
|
610
|
+
const reRecordSlug = msg.payload && typeof msg.payload === 'object' && 'reRecord' in msg.payload
|
|
611
|
+
? msg.payload.reRecord?.slug
|
|
612
|
+
: undefined;
|
|
554
613
|
if (typeof text !== 'string' || !text.trim())
|
|
555
614
|
return;
|
|
556
615
|
if (busy) {
|
|
@@ -563,6 +622,16 @@ export async function startService(opts) {
|
|
|
563
622
|
busy = true;
|
|
564
623
|
cancelled = false;
|
|
565
624
|
inflight = new AbortController();
|
|
625
|
+
// Re-record step collector — populated as tool_use events stream by,
|
|
626
|
+
// consumed at session_end to overwrite the original spec. Empty unless
|
|
627
|
+
// reRecordSlug is set on this command. We seed with a synthetic
|
|
628
|
+
// `user` step so writeSpec's JSDoc Original-prompt: line carries the
|
|
629
|
+
// text the agent was actually given (which is the prompt we read out
|
|
630
|
+
// of the existing spec — the same one we're regenerating).
|
|
631
|
+
const reRecordSteps = [];
|
|
632
|
+
if (reRecordSlug) {
|
|
633
|
+
reRecordSteps.push({ kind: 'user', text });
|
|
634
|
+
}
|
|
566
635
|
try {
|
|
567
636
|
// Build the MCP config first — it's pure local file IO and lets
|
|
568
637
|
// us assert plugin-contributed servers landed in the config even
|
|
@@ -684,6 +753,61 @@ export async function startService(opts) {
|
|
|
684
753
|
if (cancelled || ws.readyState !== WebSocket.OPEN)
|
|
685
754
|
return;
|
|
686
755
|
send(ws, { type: 'event', payload: ev });
|
|
756
|
+
// Re-record collection. Mirror what widget client.js does on the
|
|
757
|
+
// way past tool_use events: accumulate into a SkillStep[] so we
|
|
758
|
+
// can write a fresh spec when the session ends. We do this only
|
|
759
|
+
// when this command was launched in re-record mode; ordinary
|
|
760
|
+
// commands don't need server-side step retention (widget owns
|
|
761
|
+
// that for normal saves).
|
|
762
|
+
if (reRecordSlug && ev.kind === 'tool_use') {
|
|
763
|
+
reRecordSteps.push({
|
|
764
|
+
kind: 'step',
|
|
765
|
+
tool: ev.tool,
|
|
766
|
+
input: ev.input,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (reRecordSlug && ev.kind === 'session_end') {
|
|
770
|
+
// Cancelled or errored runs: don't overwrite — the existing
|
|
771
|
+
// spec is still valid. Tell the client what happened.
|
|
772
|
+
if (ev.isError) {
|
|
773
|
+
sendIfOpen(ws, {
|
|
774
|
+
type: 'error',
|
|
775
|
+
payload: {
|
|
776
|
+
message: `Re-record failed: ${ev.summary ?? 'agent reported an error'}. ` +
|
|
777
|
+
`Original spec left unchanged.`,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
// Snapshot the agent's final summary into a synthetic `done`
|
|
783
|
+
// step so writeSpec's `Outcome:` header reflects the new run.
|
|
784
|
+
if (ev.summary) {
|
|
785
|
+
reRecordSteps.push({ kind: 'done', summary: ev.summary });
|
|
786
|
+
}
|
|
787
|
+
// Overwrite. writeSpec uses the slug to name the file; we
|
|
788
|
+
// pass the original slug verbatim so the path is stable.
|
|
789
|
+
try {
|
|
790
|
+
const { writeSpec } = await import('./specs/writeSpec.js');
|
|
791
|
+
const result = await writeSpec({
|
|
792
|
+
devRoot,
|
|
793
|
+
name: reRecordSlug,
|
|
794
|
+
steps: reRecordSteps,
|
|
795
|
+
overwrite: true,
|
|
796
|
+
});
|
|
797
|
+
sendIfOpen(ws, {
|
|
798
|
+
type: 'spec-saved',
|
|
799
|
+
payload: { name: reRecordSlug, path: result.path },
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
catch (e) {
|
|
803
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
804
|
+
sendIfOpen(ws, {
|
|
805
|
+
type: 'error',
|
|
806
|
+
payload: { message: `Re-record could not write spec: ${m}` },
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
687
811
|
}
|
|
688
812
|
}
|
|
689
813
|
catch (err) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface SpecSummary {
|
|
2
|
+
/** Path-relative slug, e.g. `login-and-counter`. Identifies the spec. */
|
|
3
|
+
slug: string;
|
|
4
|
+
/** Absolute path to the .spec.ts file. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** `Original prompt:` parsed from the JSDoc header. `null` for
|
|
7
|
+
* hand-authored specs that have no header — they list but can't be
|
|
8
|
+
* re-recorded automatically. */
|
|
9
|
+
originalPrompt: string | null;
|
|
10
|
+
/** First line of `Outcome:` from the JSDoc header, if present. */
|
|
11
|
+
outcome: string | null;
|
|
12
|
+
/** Number of `Steps:` lines parsed (informational only). */
|
|
13
|
+
stepCount: number;
|
|
14
|
+
/** File mtime in ms — used to show "saved 2 hours ago" in the UI. */
|
|
15
|
+
mtimeMs: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SpecHeader {
|
|
18
|
+
/** Raw text of `Original prompt:` line, or null when absent. */
|
|
19
|
+
originalPrompt: string | null;
|
|
20
|
+
/** First line of `Outcome:`. */
|
|
21
|
+
outcome: string | null;
|
|
22
|
+
/** Step lines from the `Steps:` block, in order. */
|
|
23
|
+
steps: string[];
|
|
24
|
+
/** Lines from the `Expected:` block, in order. */
|
|
25
|
+
expected: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
|
|
29
|
+
* - Specs without any JSDoc (returns all-null).
|
|
30
|
+
* - Hand-edited specs where users reordered or trimmed sections.
|
|
31
|
+
* - Long prompts that wrap across lines (we take only the first line).
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseSpecHeader(source: string): SpecHeader;
|
|
34
|
+
/**
|
|
35
|
+
* List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
|
|
36
|
+
* parsed header. Returns newest-first by mtime so the widget overlay shows
|
|
37
|
+
* recently-saved specs at the top.
|
|
38
|
+
*/
|
|
39
|
+
export declare function listSpecs(devRoot: string): Promise<SpecSummary[]>;
|
|
40
|
+
//# sourceMappingURL=listSpecs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listSpecs.d.ts","sourceRoot":"","sources":["../../src/specs/listSpecs.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb;;qCAEiC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kEAAkE;IAClE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gCAAgC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAsB1D;AA4BD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAkCvE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List + parse Hover-generated Playwright specs under `<devRoot>/__vibe_tests__/`.
|
|
3
|
+
*
|
|
4
|
+
* Used by:
|
|
5
|
+
* - The widget's "Specs" overlay tab (server pushes a SpecSummary[] list).
|
|
6
|
+
* - The CLI's `hover re-record <spec>` subcommand (parses one spec for its
|
|
7
|
+
* `Original prompt:` JSDoc header).
|
|
8
|
+
*
|
|
9
|
+
* Hand-authored specs (no Hover JSDoc header) are listed but reported with
|
|
10
|
+
* `originalPrompt: null` — the UI / CLI surfaces that "this spec can't be
|
|
11
|
+
* re-recorded automatically; the natural-language intent isn't recorded."
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the listSkills shape so widget UI can use the same row renderer.
|
|
14
|
+
*/
|
|
15
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { stat } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/**
|
|
19
|
+
* Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
|
|
20
|
+
* - Specs without any JSDoc (returns all-null).
|
|
21
|
+
* - Hand-edited specs where users reordered or trimmed sections.
|
|
22
|
+
* - Long prompts that wrap across lines (we take only the first line).
|
|
23
|
+
*/
|
|
24
|
+
export function parseSpecHeader(source) {
|
|
25
|
+
// JSDoc block right after the @playwright/test import (or at file top).
|
|
26
|
+
// We don't require it to be the very first JSDoc — there could be a
|
|
27
|
+
// banner comment from a linter. We DO require it to appear before the
|
|
28
|
+
// first `test(` / `test.describe(` so that long file footers can't
|
|
29
|
+
// confuse the parser.
|
|
30
|
+
const beforeFirstTest = source.split(/^\s*(?:test|test\.describe)\s*\(/m)[0] ?? source;
|
|
31
|
+
const blockMatch = beforeFirstTest.match(/\/\*\*([\s\S]*?)\*\//);
|
|
32
|
+
if (!blockMatch) {
|
|
33
|
+
return { originalPrompt: null, outcome: null, steps: [], expected: [] };
|
|
34
|
+
}
|
|
35
|
+
const block = blockMatch[1];
|
|
36
|
+
const originalPrompt = extractScalar(block, /^\s*\*\s*Original prompt:\s*(.+?)\s*$/m);
|
|
37
|
+
const outcome = extractScalar(block, /^\s*\*\s*Outcome:\s*(.+?)\s*$/m);
|
|
38
|
+
return {
|
|
39
|
+
originalPrompt,
|
|
40
|
+
outcome,
|
|
41
|
+
steps: extractList(block, /^\s*\*\s*Steps:\s*$/m),
|
|
42
|
+
expected: extractList(block, /^\s*\*\s*Expected:\s*$/m),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function extractScalar(block, re) {
|
|
46
|
+
const m = block.match(re);
|
|
47
|
+
return m ? m[1].trim() : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extract a JSDoc list-style block. Given a header regex matching "Steps:"
|
|
51
|
+
* or "Expected:", read subsequent ` * <indented line>` lines until the next
|
|
52
|
+
* top-level marker (blank ` *` line or another `Section:` header).
|
|
53
|
+
*/
|
|
54
|
+
function extractList(block, headerRe) {
|
|
55
|
+
const match = block.match(headerRe);
|
|
56
|
+
if (!match)
|
|
57
|
+
return [];
|
|
58
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
59
|
+
const tail = block.slice(start);
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const raw of tail.split('\n')) {
|
|
62
|
+
// Stop at a blank JSDoc line (` *` only) or another `Section:` header.
|
|
63
|
+
if (/^\s*\*\s*$/.test(raw))
|
|
64
|
+
break;
|
|
65
|
+
if (/^\s*\*\s*\w[\w ]*:\s*$/.test(raw) || /^\s*\*\s*\w[\w ]*:\s/.test(raw))
|
|
66
|
+
break;
|
|
67
|
+
const m = raw.match(/^\s*\*\s*(?:[•\-\*\d.]\s*)*(.+?)\s*$/);
|
|
68
|
+
if (m && m[1])
|
|
69
|
+
lines.push(m[1]);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
|
|
75
|
+
* parsed header. Returns newest-first by mtime so the widget overlay shows
|
|
76
|
+
* recently-saved specs at the top.
|
|
77
|
+
*/
|
|
78
|
+
export async function listSpecs(devRoot) {
|
|
79
|
+
const root = join(devRoot, '__vibe_tests__');
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(root);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const summaries = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.endsWith('.spec.ts'))
|
|
90
|
+
continue;
|
|
91
|
+
const path = join(root, entry);
|
|
92
|
+
let content;
|
|
93
|
+
let mtimeMs = 0;
|
|
94
|
+
try {
|
|
95
|
+
content = await readFile(path, 'utf-8');
|
|
96
|
+
const st = await stat(path);
|
|
97
|
+
mtimeMs = st.mtimeMs;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const header = parseSpecHeader(content);
|
|
103
|
+
summaries.push({
|
|
104
|
+
slug: entry.replace(/\.spec\.ts$/, ''),
|
|
105
|
+
path,
|
|
106
|
+
originalPrompt: header.originalPrompt,
|
|
107
|
+
outcome: header.outcome,
|
|
108
|
+
stepCount: header.steps.length,
|
|
109
|
+
mtimeMs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
summaries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
113
|
+
return summaries;
|
|
114
|
+
}
|