@reneturcios/greenlight 0.4.0 → 0.5.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/init.js ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * `greenlight init`: one-command agent setup.
3
+ *
4
+ * Before this, onboarding an agent meant cloning the repo and hand-copying
5
+ * MCP configs plus instruction snippets from integrations/ per tool. Init
6
+ * does that from the installed package: it copies (or JSON-merges) the MCP
7
+ * server config into the current project and installs the tool's instruction
8
+ * snippet, then prints the restart + verify steps.
9
+ *
10
+ * Safety rules: existing configs are merged (other servers are preserved),
11
+ * instruction appends are marker-guarded and idempotent, nothing is
12
+ * overwritten unless --force is given, and --dry-run only reports.
13
+ */
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { homedir } from "node:os";
17
+ import { fileURLToPath } from "node:url";
18
+ export const INIT_TOOLS = [
19
+ {
20
+ id: "claude-code",
21
+ label: "Claude Code",
22
+ configSrc: "claude-code/.mcp.json",
23
+ configDest: ".mcp.json",
24
+ serverMapKey: "mcpServers",
25
+ instructionsSrc: "claude-code/CLAUDE.snippet.md",
26
+ instructionsDest: "CLAUDE.md",
27
+ instructionsMode: "append-markers",
28
+ detectMarkers: [".mcp.json", "CLAUDE.md"],
29
+ restartNote: "Restart Claude Code, then ask it to list its MCP tools.",
30
+ },
31
+ {
32
+ id: "cursor",
33
+ label: "Cursor",
34
+ configSrc: "cursor/.cursor/mcp.json",
35
+ configDest: ".cursor/mcp.json",
36
+ serverMapKey: "mcpServers",
37
+ instructionsSrc: "cursor/greenlight.mdc",
38
+ instructionsDest: ".cursor/rules/greenlight.mdc",
39
+ instructionsMode: "copy",
40
+ detectMarkers: [".cursor"],
41
+ restartNote: "Restart Cursor, then ask the agent to list its MCP tools.",
42
+ },
43
+ {
44
+ id: "copilot",
45
+ label: "VS Code / Copilot",
46
+ configSrc: "copilot/.vscode/mcp.json",
47
+ configDest: ".vscode/mcp.json",
48
+ serverMapKey: "servers",
49
+ instructionsSrc: "copilot/copilot-instructions.snippet.md",
50
+ instructionsDest: ".github/copilot-instructions.md",
51
+ instructionsMode: "append-markers",
52
+ detectMarkers: [".vscode", ".github/copilot-instructions.md"],
53
+ restartNote: "Reload VS Code, then check the MCP server list.",
54
+ },
55
+ {
56
+ id: "windsurf",
57
+ label: "Windsurf",
58
+ configSrc: "windsurf/mcp_config.json",
59
+ configDest: ".codeium/windsurf/mcp_config.json",
60
+ homeDest: true,
61
+ serverMapKey: "mcpServers",
62
+ detectMarkers: [],
63
+ restartNote: "Restart Windsurf, then open Manage MCP Servers to confirm greenlight is listed.",
64
+ },
65
+ {
66
+ id: "cline",
67
+ label: "Cline",
68
+ configSrc: "cline/cline_mcp_settings.json",
69
+ configDest: ".cline/mcp_settings.json",
70
+ serverMapKey: "mcpServers",
71
+ detectMarkers: [".cline"],
72
+ restartNote: "Restart your editor, then check Cline's MCP Servers list.",
73
+ },
74
+ {
75
+ id: "generic",
76
+ label: "Any other MCP client",
77
+ configSrc: "mcp.json",
78
+ configDest: "greenlight.mcp.json",
79
+ serverMapKey: null,
80
+ detectMarkers: [],
81
+ restartNote: "Point your MCP client at greenlight.mcp.json (command npx, args -y @reneturcios/greenlight mcp).",
82
+ },
83
+ ];
84
+ /** Markers guarding an appended instruction block; presence means "already installed". */
85
+ export const INIT_BEGIN_MARKER = "<!-- greenlight:init:begin -->";
86
+ export const INIT_END_MARKER = "<!-- greenlight:init:end -->";
87
+ /** The integrations pack ships inside the npm package next to dist/. */
88
+ export function defaultPackDir() {
89
+ return fileURLToPath(new URL("../integrations/", import.meta.url));
90
+ }
91
+ export function findTool(id) {
92
+ return INIT_TOOLS.find((t) => t.id === id.toLowerCase());
93
+ }
94
+ /** Split --agent values on commas, lowercase, dedupe, drop empties. */
95
+ export function parseAgentList(values) {
96
+ const out = [];
97
+ for (const v of values) {
98
+ for (const part of v.split(",")) {
99
+ const id = part.trim().toLowerCase();
100
+ if (id && !out.includes(id))
101
+ out.push(id);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /** Tool ids whose marker files exist in the project (a hint, never auto-applied). */
107
+ export function detectTools(projectDir) {
108
+ return INIT_TOOLS.filter((t) => t.detectMarkers.length > 0 && t.detectMarkers.some((m) => existsSync(join(projectDir, m)))).map((t) => t.id);
109
+ }
110
+ function sameJson(a, b) {
111
+ return JSON.stringify(a) === JSON.stringify(b);
112
+ }
113
+ function writeFileEnsured(path, content, dryRun) {
114
+ if (dryRun)
115
+ return;
116
+ mkdirSync(dirname(path), { recursive: true });
117
+ writeFileSync(path, content, "utf8");
118
+ }
119
+ /**
120
+ * Install one tool's MCP config. Existing JSON configs are merged (the
121
+ * greenlight server is added, everything else preserved). Returns the file
122
+ * result; throws only when the destination is unparseable and --force was
123
+ * not given, since silently dropping a broken config would hide user data.
124
+ */
125
+ function installConfig(tool, destPath, packContent, force, dryRun) {
126
+ if (tool.serverMapKey === null) {
127
+ if (!existsSync(destPath)) {
128
+ writeFileEnsured(destPath, packContent, dryRun);
129
+ return { path: destPath, action: "created", detail: "config copied" };
130
+ }
131
+ const existing = readFileSync(destPath, "utf8");
132
+ if (existing === packContent) {
133
+ return { path: destPath, action: "skipped", detail: "already set up" };
134
+ }
135
+ if (!force) {
136
+ return {
137
+ path: destPath,
138
+ action: "skipped",
139
+ detail: "exists with different content; re-run with --force to overwrite",
140
+ };
141
+ }
142
+ writeFileEnsured(destPath, packContent, dryRun);
143
+ return { path: destPath, action: "overwritten", detail: "overwritten with --force" };
144
+ }
145
+ const packDoc = JSON.parse(packContent);
146
+ const packEntry = packDoc[tool.serverMapKey]?.greenlight;
147
+ if (!existsSync(destPath)) {
148
+ writeFileEnsured(destPath, packContent.endsWith("\n") ? packContent : packContent + "\n", dryRun);
149
+ return { path: destPath, action: "created", detail: "config copied" };
150
+ }
151
+ let destDoc;
152
+ try {
153
+ destDoc = JSON.parse(readFileSync(destPath, "utf8"));
154
+ }
155
+ catch {
156
+ if (!force) {
157
+ throw new Error(`${destPath} exists but is not valid JSON; fix it or re-run with --force to overwrite it`);
158
+ }
159
+ writeFileEnsured(destPath, packContent.endsWith("\n") ? packContent : packContent + "\n", dryRun);
160
+ return { path: destPath, action: "overwritten", detail: "invalid JSON overwritten with --force" };
161
+ }
162
+ if (typeof destDoc !== "object" || destDoc === null || Array.isArray(destDoc)) {
163
+ if (!force) {
164
+ throw new Error(`${destPath} exists but is not a JSON object; fix it or re-run with --force to overwrite it`);
165
+ }
166
+ writeFileEnsured(destPath, packContent.endsWith("\n") ? packContent : packContent + "\n", dryRun);
167
+ return { path: destPath, action: "overwritten", detail: "non-object JSON overwritten with --force" };
168
+ }
169
+ if (typeof destDoc[tool.serverMapKey] !== "object" || destDoc[tool.serverMapKey] === null) {
170
+ destDoc[tool.serverMapKey] = {};
171
+ }
172
+ const map = destDoc[tool.serverMapKey];
173
+ if ("greenlight" in map && sameJson(map.greenlight, packEntry)) {
174
+ return { path: destPath, action: "skipped", detail: "greenlight server already configured" };
175
+ }
176
+ const hadEntry = "greenlight" in map;
177
+ map.greenlight = packEntry;
178
+ writeFileEnsured(destPath, JSON.stringify(destDoc, null, 2) + "\n", dryRun);
179
+ return {
180
+ path: destPath,
181
+ action: hadEntry ? "updated" : "merged",
182
+ detail: hadEntry ? "greenlight server entry refreshed, other servers kept" : "greenlight server added, other servers kept",
183
+ };
184
+ }
185
+ /** Install one tool's instruction snippet (append-with-markers or whole copy). */
186
+ function installInstructions(tool, destPath, snippet, force, dryRun) {
187
+ if (tool.instructionsMode === "copy") {
188
+ if (!existsSync(destPath)) {
189
+ writeFileEnsured(destPath, snippet, dryRun);
190
+ return { path: destPath, action: "created", detail: "rule file copied" };
191
+ }
192
+ if (readFileSync(destPath, "utf8") === snippet) {
193
+ return { path: destPath, action: "skipped", detail: "already set up" };
194
+ }
195
+ if (!force) {
196
+ return {
197
+ path: destPath,
198
+ action: "skipped",
199
+ detail: "exists with different content; re-run with --force to overwrite",
200
+ };
201
+ }
202
+ writeFileEnsured(destPath, snippet, dryRun);
203
+ return { path: destPath, action: "overwritten", detail: "overwritten with --force" };
204
+ }
205
+ // append-markers: idempotent by marker presence; never touches user text.
206
+ if (existsSync(destPath) && readFileSync(destPath, "utf8").includes(INIT_BEGIN_MARKER)) {
207
+ return { path: destPath, action: "skipped", detail: "instructions already installed" };
208
+ }
209
+ const block = `${INIT_BEGIN_MARKER}\n${snippet.trim()}\n${INIT_END_MARKER}\n`;
210
+ if (!existsSync(destPath)) {
211
+ writeFileEnsured(destPath, block, dryRun);
212
+ return { path: destPath, action: "created", detail: "instructions installed" };
213
+ }
214
+ const existing = readFileSync(destPath, "utf8");
215
+ const joined = existing + (existing.endsWith("\n") ? "\n" : "\n\n") + block;
216
+ writeFileEnsured(destPath, joined, dryRun);
217
+ return { path: destPath, action: "updated", detail: "instructions appended, existing content kept" };
218
+ }
219
+ /**
220
+ * Set up the given tools. Throws on unknown tool ids or a missing pack so
221
+ * the CLI can show the tool list instead of half-writing.
222
+ */
223
+ export function initTools(toolIds, opts = {}) {
224
+ const projectDir = opts.projectDir ?? process.cwd();
225
+ const homeDir = opts.homeDir ?? homedir();
226
+ const packDir = opts.packDir ?? defaultPackDir();
227
+ const force = opts.force ?? false;
228
+ const includeInstructions = opts.includeInstructions ?? true;
229
+ const dryRun = opts.dryRun ?? false;
230
+ const tools = toolIds.map((id) => {
231
+ const tool = findTool(id);
232
+ if (!tool) {
233
+ throw new Error(`Unknown agent "${id}". Run 'greenlight init --list' to see the supported tools.`);
234
+ }
235
+ return tool;
236
+ });
237
+ // Read every pack file up front so a missing pack fails before writing.
238
+ const packFiles = new Map();
239
+ const readPack = (rel) => {
240
+ const cached = packFiles.get(rel);
241
+ if (cached !== undefined)
242
+ return cached;
243
+ const abs = join(packDir, rel);
244
+ if (!existsSync(abs)) {
245
+ throw new Error(`Pack file missing: ${rel} (looked in ${packDir})`);
246
+ }
247
+ const content = readFileSync(abs, "utf8");
248
+ packFiles.set(rel, content);
249
+ return content;
250
+ };
251
+ for (const tool of tools) {
252
+ readPack(tool.configSrc);
253
+ if (includeInstructions && tool.instructionsSrc)
254
+ readPack(tool.instructionsSrc);
255
+ }
256
+ return tools.map((tool) => {
257
+ const notes = [];
258
+ const files = [];
259
+ const base = tool.homeDest ? homeDir : projectDir;
260
+ const configDest = join(base, tool.configDest);
261
+ files.push(installConfig(tool, configDest, readPack(tool.configSrc), force, dryRun));
262
+ if (includeInstructions && tool.instructionsSrc && tool.instructionsDest) {
263
+ const snippetDest = join(projectDir, tool.instructionsDest);
264
+ files.push(installInstructions(tool, snippetDest, readPack(tool.instructionsSrc), force, dryRun));
265
+ }
266
+ else if (tool.instructionsSrc && !includeInstructions) {
267
+ notes.push("instructions skipped (--no-instructions)");
268
+ }
269
+ notes.push(tool.restartNote);
270
+ return { id: tool.id, label: tool.label, files, notes };
271
+ });
272
+ }
273
+ /** Human-readable report of an init run (file actions plus next steps). */
274
+ export function formatInitHuman(results, dryRun) {
275
+ const verb = {
276
+ created: "create",
277
+ merged: "merge into",
278
+ updated: "update",
279
+ overwritten: "overwrite",
280
+ skipped: "skip",
281
+ };
282
+ const lines = [];
283
+ if (dryRun)
284
+ lines.push("Dry run: nothing was written. Re-run without --dry-run to apply.");
285
+ for (const r of results) {
286
+ lines.push(`${r.label}:`);
287
+ for (const f of r.files) {
288
+ const action = dryRun ? `would ${verb[f.action]}` : f.action;
289
+ lines.push(` ${action}: ${f.path} (${f.detail})`);
290
+ }
291
+ for (const n of r.notes)
292
+ lines.push(` next: ${n}`);
293
+ }
294
+ lines.push("Verify: ask your agent to list its MCP tools; greenlight should appear with 12 tools.");
295
+ lines.push("Tip: set GITHUB_TOKEN (classic PAT, no scopes needed for public repos) for the 5000 req/hr limit.");
296
+ return lines.join("\n");
297
+ }
298
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Verified-comment mode.
3
+ *
4
+ * Some repos take no outside PRs at all. The proven examples are
5
+ * modelcontextprotocol/inspector ("Pull requests against this repo are
6
+ * opened by the repo maintainers only") and modelcontextprotocol/servers
7
+ * ("We accept issues, not pull requests"): outside PRs are closed on
8
+ * sight, and the contribution that lands is a detailed, verified issue
9
+ * comment instead — a reproduced problem, a located root cause, a
10
+ * fix sketch, and honest notes on what was actually run.
11
+ *
12
+ * This module assembles that comment from contributor-supplied inputs.
13
+ * It never posts anything (there is no post path), and it never invents
14
+ * facts: any section the contributor did not fill renders as an explicit
15
+ * gap marker ("not yet run", "not yet isolated", "not recorded") so a
16
+ * maintainer can see exactly what is verified and what is not. A
17
+ * plausible-looking filled-in section would be a lie; a labeled gap is
18
+ * the product.
19
+ */
20
+ import type { Gate } from "./types.js";
21
+ export interface VerifiedCommentFields {
22
+ /** One-line claim of the problem (usually the issue title restated). */
23
+ claim: string;
24
+ /** Environment: repo, ref/commit, date, platform. */
25
+ environment: string;
26
+ /** Reproduction steps, in order, exact commands. */
27
+ reproduction: string[];
28
+ /** What was observed when the repro ran. */
29
+ observed: string;
30
+ /** What should have happened instead. */
31
+ expected: string;
32
+ /** Root cause: file paths and line refs when known. */
33
+ rootCause: string;
34
+ /** Proposed fix sketch, not a code dump. */
35
+ fixSketch: string;
36
+ /** What was actually run (commands, counts). */
37
+ ran: string;
38
+ /** Who ran it. */
39
+ verifiedBy: string;
40
+ }
41
+ export type VerifiedCommentInput = Partial<VerifiedCommentFields>;
42
+ export interface BuiltComment {
43
+ markdown: string;
44
+ /** Names of sections that rendered as gap markers. */
45
+ gaps: string[];
46
+ }
47
+ /**
48
+ * Assemble the verified comment. Everything comes from the contributor's
49
+ * inputs (or the --from-file parse); nothing is fetched and nothing is
50
+ * invented. The greenlight version in the footer is a real fact about the
51
+ * tool that assembled the comment, not a fact about the bug.
52
+ */
53
+ export declare function buildVerifiedComment(issueUrl: string, fields: VerifiedCommentInput, greenlightVersion: string): BuiltComment;
54
+ /**
55
+ * True when the repo class is maintainer-only PRs: outside PRs are
56
+ * refused on sight, so the useful contribution is a verified comment.
57
+ * That is Greenlight's `collaborators_only` gate (inspector and servers
58
+ * both land there: their policy text is matched by the gate parser).
59
+ */
60
+ export declare function suggestsVerifiedComment(gates: Gate[]): boolean;
61
+ /** The suggestion line check_issue prints for maintainer-only repos. */
62
+ export declare function verifiedCommentSuggestion(issueUrl: string): string;
63
+ export interface ParsedNoteFile {
64
+ fields: VerifiedCommentInput;
65
+ /** Sections that did not map to a known field, kept verbatim. */
66
+ additionalNotes: string;
67
+ }
68
+ /**
69
+ * Parse a markdown notes file into comment fields. Lines under a heading
70
+ * map to that section; unmapped headings are preserved under
71
+ * "Additional notes" instead of being dropped, so contributor content is
72
+ * never silently lost. Unheaded text at the top of the file becomes the
73
+ * claim.
74
+ */
75
+ export declare function parseNoteFile(text: string): ParsedNoteFile;
@@ -0,0 +1,149 @@
1
+ const GAP = (label) => `*${label}: not recorded*`;
2
+ function section(title, body, gaps, gapName) {
3
+ if (body.trim().length === 0)
4
+ gaps.push(gapName);
5
+ return `## ${title}\n\n${body.trim().length > 0 ? body.trim() : GAP("not yet filled in")}\n`;
6
+ }
7
+ /**
8
+ * Assemble the verified comment. Everything comes from the contributor's
9
+ * inputs (or the --from-file parse); nothing is fetched and nothing is
10
+ * invented. The greenlight version in the footer is a real fact about the
11
+ * tool that assembled the comment, not a fact about the bug.
12
+ */
13
+ export function buildVerifiedComment(issueUrl, fields, greenlightVersion) {
14
+ const gaps = [];
15
+ const f = (k) => fields[k] instanceof Array
16
+ ? fields[k].join("\n")
17
+ : (fields[k] ?? "").trim();
18
+ const parts = [];
19
+ parts.push(`<!-- greenlight-verified-comment v1 -->`);
20
+ parts.push(`# Verified finding for ${issueUrl}`);
21
+ parts.push(``);
22
+ parts.push(section("Claim of the problem", f("claim"), gaps, "claim"));
23
+ parts.push(section("Environment", f("environment"), gaps, "environment"));
24
+ const repro = fields.reproduction ?? [];
25
+ const reproBody = repro.length > 0
26
+ ? repro.map((s, i) => `${i + 1}. ${s.trim()}`).join("\n")
27
+ : `*Repro: not yet run*`;
28
+ if (repro.length === 0)
29
+ gaps.push("reproduction");
30
+ parts.push(`## Reproduction\n\n${reproBody}\n`);
31
+ parts.push(section("Observed vs expected", [f("observed"), f("expected")].some((s) => s.length > 0)
32
+ ? `**Observed:** ${f("observed") || "*not recorded*"}\n\n**Expected:** ${f("expected") || "*not recorded*"}`
33
+ : "", gaps, "observed/expected"));
34
+ parts.push(section("Root cause", f("rootCause"), gaps, "root-cause").replace("not yet filled in", "not yet isolated"));
35
+ parts.push(section("Proposed fix", f("fixSketch"), gaps, "fix-sketch").replace("not yet filled in", "not sketched yet"));
36
+ const ran = f("ran");
37
+ if (ran.length === 0)
38
+ gaps.push("verification");
39
+ const by = f("verifiedBy");
40
+ parts.push(`## Verification\n\n` +
41
+ `What was actually run: ${ran.length > 0 ? ran : "*not yet run*"}\n\n` +
42
+ `Verified by: ${by.length > 0 ? by : "*contributor not stated*"}\n\n` +
43
+ `---\n` +
44
+ `*Comment drafted with Greenlight v${greenlightVersion} (verified-comment mode). ` +
45
+ `Assembled locally from the contributor's notes; nothing here was posted automatically.*\n`);
46
+ return { markdown: parts.join("\n"), gaps };
47
+ }
48
+ /**
49
+ * True when the repo class is maintainer-only PRs: outside PRs are
50
+ * refused on sight, so the useful contribution is a verified comment.
51
+ * That is Greenlight's `collaborators_only` gate (inspector and servers
52
+ * both land there: their policy text is matched by the gate parser).
53
+ */
54
+ export function suggestsVerifiedComment(gates) {
55
+ return gates.some((g) => g.detected && g.kind === "collaborators_only");
56
+ }
57
+ /** The suggestion line check_issue prints for maintainer-only repos. */
58
+ export function verifiedCommentSuggestion(issueUrl) {
59
+ return `This repo takes verified comments instead of PRs: draft one with greenlight verify-comment ${issueUrl}`;
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // --from-file parsing: headings map onto sections, everything else is kept.
63
+ // ---------------------------------------------------------------------------
64
+ const HEADING_MAP = [
65
+ { match: /^(claim|problem|summary|title)$/i, field: "claim" },
66
+ { match: /^(environment|env|setup|context)$/i, field: "environment" },
67
+ { match: /^(repro|reproduction|reproduction steps|steps|how to reproduce)$/i, field: "reproduction" },
68
+ { match: /^(observed)$/i, field: "observed" },
69
+ { match: /^(expected)$/i, field: "expected" },
70
+ { match: /^(observed vs expected|observed\/expected|behavior)$/i, field: "observed" },
71
+ { match: /^(root cause|cause|analysis)$/i, field: "rootCause" },
72
+ { match: /^(fix|proposed fix|fix sketch|proposal|solution)$/i, field: "fixSketch" },
73
+ { match: /^(ran|verification|what was run|testing|test results)$/i, field: "ran" },
74
+ { match: /^(by|verified by|author|contributor)$/i, field: "verifiedBy" },
75
+ ];
76
+ /**
77
+ * Parse a markdown notes file into comment fields. Lines under a heading
78
+ * map to that section; unmapped headings are preserved under
79
+ * "Additional notes" instead of being dropped, so contributor content is
80
+ * never silently lost. Unheaded text at the top of the file becomes the
81
+ * claim.
82
+ */
83
+ export function parseNoteFile(text) {
84
+ const fields = {};
85
+ const extras = [];
86
+ const lines = text.split("\n");
87
+ let current = {
88
+ field: null,
89
+ extraTitle: null,
90
+ };
91
+ const buffers = new Map();
92
+ const push = (key, line) => {
93
+ const b = buffers.get(key) ?? [];
94
+ b.push(line);
95
+ buffers.set(key, b);
96
+ };
97
+ const flushExtra = () => {
98
+ if (current.extraTitle) {
99
+ const b = buffers.get(`extra:${current.extraTitle}`) ?? [];
100
+ if (b.join("\n").trim().length > 0) {
101
+ extras.push(`### ${current.extraTitle}\n\n${b.join("\n").trim()}`);
102
+ }
103
+ current.extraTitle = null;
104
+ }
105
+ };
106
+ for (const line of lines) {
107
+ const m = /^(#{1,3})\s+(.+?)\s*$/.exec(line);
108
+ if (m) {
109
+ flushExtra();
110
+ const title = m[2].trim();
111
+ const hit = HEADING_MAP.find((h) => h.match.test(title));
112
+ current = hit
113
+ ? { field: hit.field, extraTitle: null }
114
+ : { field: null, extraTitle: title };
115
+ if (!hit)
116
+ buffers.set(`extra:${title}`, []);
117
+ continue;
118
+ }
119
+ if (current.field) {
120
+ push(current.field, line);
121
+ }
122
+ else if (current.extraTitle) {
123
+ push(`extra:${current.extraTitle}`, line);
124
+ }
125
+ else {
126
+ // Unheaded leading text: treat as the claim of the problem.
127
+ push("claim", line);
128
+ }
129
+ }
130
+ flushExtra();
131
+ for (const [k, b] of buffers) {
132
+ if (k.startsWith("extra:"))
133
+ continue;
134
+ const value = b.join("\n").trim();
135
+ if (value.length === 0)
136
+ continue;
137
+ if (k === "reproduction") {
138
+ fields.reproduction = value
139
+ .split("\n")
140
+ .map((s) => s.replace(/^\s*(?:\d+[.)]|[-*])\s+/, "").trim())
141
+ .filter((s) => s.length > 0);
142
+ }
143
+ else {
144
+ fields[k] = value;
145
+ }
146
+ }
147
+ return { fields, additionalNotes: extras.join("\n\n") };
148
+ }
149
+ //# sourceMappingURL=verify-comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-comment.js","sourceRoot":"","sources":["../src/verify-comment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Test-output parsers for `greenlight verify`.
3
+ *
4
+ * The delta (base vs head) is only honest if it is derived from parsed
5
+ * test identities, never from raw log lines. Each parser turns one
6
+ * runner's output into the same shape: named passing tests and named
7
+ * failing tests. Runners slot in by implementing TestRunnerParser; the
8
+ * pipeline picks the first parser whose canParse matches the test
9
+ * command.
10
+ */
11
+ export interface ParsedTests {
12
+ passed: string[];
13
+ failed: string[];
14
+ }
15
+ export interface TestRunnerParser {
16
+ /** Human name, e.g. "node:test". */
17
+ name: string;
18
+ /** True when this parser claims the given test command. */
19
+ canParse(command: string): boolean;
20
+ /** Parse one full run's stdout+stderr. */
21
+ parse(output: string): ParsedTests;
22
+ }
23
+ export declare const RUNNER_PARSERS: TestRunnerParser[];
24
+ /** Pick the parser for a test command, or null when none claims it. */
25
+ export declare function parserForCommand(command: string): TestRunnerParser | null;
@@ -0,0 +1,56 @@
1
+ function dedupe(xs) {
2
+ return [...new Set(xs)];
3
+ }
4
+ const nodeTest = {
5
+ name: "node:test",
6
+ canParse: (cmd) => /(^|\s)(node\s+--test|tsx?\s+--test)/.test(cmd) || /\bvitest\b/.test(cmd),
7
+ parse(output) {
8
+ const passed = [];
9
+ const failed = [];
10
+ for (const line of output.split("\n")) {
11
+ // node --test: "✔ name (1.23ms)" / "✖ name (1.23ms)"; leading spaces ok.
12
+ // The summary headers "✖ failing tests:" / "✔ passing tests:" are not
13
+ // tests (the live greenlight-verify run on 2026-10-16 caught "failing
14
+ // tests:" being counted as a failure).
15
+ const ok = /^\s*✔\s+(.+?)(?:\s+\(\d[\d.,]*m?s\))?\s*$/.exec(line);
16
+ if (ok && !/^(passing|failing) tests:?$/i.test(ok[1].trim())) {
17
+ passed.push(ok[1].trim());
18
+ continue;
19
+ }
20
+ const bad = /^\s*✖\s+(.+?)(?:\s+\(\d[\d.,]*m?s\))?\s*$/.exec(line);
21
+ if (bad && !/^(passing|failing) tests:?$/i.test(bad[1].trim())) {
22
+ failed.push(bad[1].trim());
23
+ continue;
24
+ }
25
+ // vitest run: "✓ src/foo.test.ts (3 tests)" is file-level; real
26
+ // per-test lines come from node --test above. Skip to avoid mixing
27
+ // granularities.
28
+ }
29
+ return { passed: dedupe(passed), failed: dedupe(failed) };
30
+ },
31
+ };
32
+ const pytest = {
33
+ name: "pytest",
34
+ canParse: (cmd) => /(^|\s)pytest(\s|$)/.test(cmd),
35
+ parse(output) {
36
+ const passed = [];
37
+ const failed = [];
38
+ for (const line of output.split("\n")) {
39
+ // -v output: "tests/x.py::TestY::test_z PASSED" / FAILED / ERROR
40
+ const m = /^(\S+::\S+)\s+(PASSED|FAILED|ERROR)\b/.exec(line.trim());
41
+ if (!m)
42
+ continue;
43
+ if (m[2] === "PASSED")
44
+ passed.push(m[1]);
45
+ else
46
+ failed.push(m[1]);
47
+ }
48
+ return { passed: dedupe(passed), failed: dedupe(failed) };
49
+ },
50
+ };
51
+ export const RUNNER_PARSERS = [nodeTest, pytest];
52
+ /** Pick the parser for a test command, or null when none claims it. */
53
+ export function parserForCommand(command) {
54
+ return RUNNER_PARSERS.find((p) => p.canParse(command)) ?? null;
55
+ }
56
+ //# sourceMappingURL=verify-parsers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-parsers.js","sourceRoot":"","sources":["../src/verify-parsers.ts"],"names":[],"mappings":"AAwBA,SAAS,MAAM,CAAC,EAAY;IAC1B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,QAAQ,GAAqB;IACjC,IAAI,EAAE,WAAW;IACjB,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,qCAAqC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC;IAC5F,KAAK,CAAC,MAAM;QACV,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,yEAAyE;YACzE,sEAAsE;YACtE,sEAAsE;YACtE,uCAAuC;YACvC,MAAM,EAAE,GAAG,2CAA2C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClE,IAAI,EAAE,IAAI,CAAC,8BAA8B,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC7D,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC1B,SAAS;YACX,CAAC;YACD,MAAM,GAAG,GAAG,2CAA2C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnE,IAAI,GAAG,IAAI,CAAC,8BAA8B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC/D,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC3B,SAAS;YACX,CAAC;YACD,gEAAgE;YAChE,mEAAmE;YACnE,iBAAiB;QACnB,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IAC5D,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,GAAqB;IAC/B,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC;IACjD,KAAK,CAAC,MAAM;QACV,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,iEAAiE;YACjE,MAAM,CAAC,GAAG,uCAAuC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YACpE,IAAI,CAAC,CAAC;gBAAE,SAAS;YACjB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;gBACpC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IAC5D,CAAC;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAuB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAErE,uEAAuE;AACvE,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC;AACjE,CAAC"}