@robhowley/pi-structured-return 0.1.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/extensions/structured-return/examples/.pi/machine-readable.json +18 -0
  4. package/extensions/structured-return/examples/.pi/parsers/foo-cli.ts +25 -0
  5. package/extensions/structured-return/src/.tmp/vitest-report.xml +55 -0
  6. package/extensions/structured-return/src/config/project-config.ts +10 -0
  7. package/extensions/structured-return/src/config/registry.ts +89 -0
  8. package/extensions/structured-return/src/index.test.ts +98 -0
  9. package/extensions/structured-return/src/index.ts +161 -0
  10. package/extensions/structured-return/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  11. package/extensions/structured-return/src/parsers/eslint-json.test.ts +60 -0
  12. package/extensions/structured-return/src/parsers/eslint-json.ts +33 -0
  13. package/extensions/structured-return/src/parsers/junit-xml.test.ts +169 -0
  14. package/extensions/structured-return/src/parsers/junit-xml.ts +97 -0
  15. package/extensions/structured-return/src/parsers/minitest-text.test.ts +110 -0
  16. package/extensions/structured-return/src/parsers/minitest-text.ts +99 -0
  17. package/extensions/structured-return/src/parsers/pytest-json-report.test.ts +76 -0
  18. package/extensions/structured-return/src/parsers/pytest-json-report.ts +41 -0
  19. package/extensions/structured-return/src/parsers/rspec-json.test.ts +126 -0
  20. package/extensions/structured-return/src/parsers/rspec-json.ts +76 -0
  21. package/extensions/structured-return/src/parsers/ruff-json.test.ts +57 -0
  22. package/extensions/structured-return/src/parsers/ruff-json.ts +37 -0
  23. package/extensions/structured-return/src/parsers/tail-fallback.test.ts +41 -0
  24. package/extensions/structured-return/src/parsers/tail-fallback.ts +20 -0
  25. package/extensions/structured-return/src/parsers/vitest-json.test.ts +118 -0
  26. package/extensions/structured-return/src/parsers/vitest-json.ts +60 -0
  27. package/extensions/structured-return/src/storage/log-store.ts +23 -0
  28. package/extensions/structured-return/src/types.ts +55 -0
  29. package/extensions/structured-return/src/ui/widget.ts +3 -0
  30. package/extensions/structured-return/tsconfig.json +11 -0
  31. package/extensions/structured-return/vitest.config.ts +9 -0
  32. package/package.json +62 -0
  33. package/skills/structured-return/SKILL.md +67 -0
@@ -0,0 +1,126 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, it, expect } from "vitest";
5
+ import parser from "./rspec-json";
6
+ import type { RunContext } from "../types";
7
+
8
+ function makeCtx(stdout: string, cwd = "/project"): RunContext {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rspec-test-"));
10
+ const stdoutPath = path.join(dir, "stdout");
11
+ fs.writeFileSync(stdoutPath, stdout);
12
+ return {
13
+ command: "bundle exec rspec --format json",
14
+ argv: ["bundle", "exec", "rspec", "--format", "json"],
15
+ cwd,
16
+ artifactPaths: [],
17
+ stdoutPath,
18
+ stderrPath: path.join(dir, "stderr"),
19
+ logPath: path.join(dir, "log"),
20
+ };
21
+ }
22
+
23
+ const passing = (id: string, file: string, line: number): object => ({
24
+ id,
25
+ full_description: id,
26
+ status: "passed",
27
+ file_path: file,
28
+ line_number: line,
29
+ });
30
+
31
+ const failing = (id: string, file: string, line: number, message: string): object => ({
32
+ id,
33
+ full_description: id,
34
+ status: "failed",
35
+ file_path: file,
36
+ line_number: line,
37
+ exception: {
38
+ class: "RSpec::Expectations::ExpectationNotMetError",
39
+ message,
40
+ backtrace: [`${file}:${line}:in 'block (2 levels) in <top (required)>'`],
41
+ },
42
+ });
43
+
44
+ function report(examples: object[], failureCount: number, pendingCount = 0): string {
45
+ return JSON.stringify({
46
+ examples,
47
+ summary: {
48
+ example_count: examples.length,
49
+ failure_count: failureCount,
50
+ pending_count: pendingCount,
51
+ },
52
+ });
53
+ }
54
+
55
+ describe("rspec-json parser", () => {
56
+ it("mix of passed and failed → status fail, correct counts, failures listed", async () => {
57
+ const cwd = "/project";
58
+ const stdout = report(
59
+ [
60
+ passing("Foo does something", "./spec/foo_spec.rb", 5),
61
+ failing("Foo blows up", "./spec/foo_spec.rb", 10, "expected: 2\n got: 1"),
62
+ passing("Bar works", "./spec/bar_spec.rb", 3),
63
+ failing("Bar also blows up", "./spec/bar_spec.rb", 8, "expected true\n got false"),
64
+ ],
65
+ 2
66
+ );
67
+ const result = await parser.parse(makeCtx(stdout, cwd));
68
+ expect(result.status).toBe("fail");
69
+ expect(result.summary).toBe("2 failed, 2 passed");
70
+ expect(result.failures).toHaveLength(2);
71
+ expect(result.failures![0].file).toBe("spec/foo_spec.rb");
72
+ expect(result.failures![1].file).toBe("spec/bar_spec.rb");
73
+ });
74
+
75
+ it("all passing → status pass, summary reflects passed count", async () => {
76
+ const stdout = report(
77
+ [passing("Foo works", "./spec/foo_spec.rb", 5), passing("Bar works", "./spec/bar_spec.rb", 3)],
78
+ 0
79
+ );
80
+ const result = await parser.parse(makeCtx(stdout));
81
+ expect(result.status).toBe("pass");
82
+ expect(result.summary).toBe("2 passed");
83
+ expect(result.failures).toHaveLength(0);
84
+ });
85
+
86
+ it("failure message → expected/got pair joined, stops at blank line", async () => {
87
+ const stdout = report(
88
+ [failing("Foo blows up", "./spec/foo_spec.rb", 10, "\nexpected: 2\n got: 1\n\n(compared using ==)\n")],
89
+ 1
90
+ );
91
+ const result = await parser.parse(makeCtx(stdout));
92
+ expect(result.failures![0].message).toBe("expected: 2 / got: 1");
93
+ });
94
+
95
+ it("file_path with ./ prefix → stripped in relative path output", async () => {
96
+ const stdout = report([failing("Foo blows up", "./spec/foo_spec.rb", 10, "error")], 1);
97
+ const result = await parser.parse(makeCtx(stdout, "/project"));
98
+ expect(result.failures![0].file).toBe("spec/foo_spec.rb");
99
+ expect(result.failures![0].file).not.toMatch(/^\.\//);
100
+ });
101
+
102
+ it("line number preserved on failure", async () => {
103
+ const stdout = report([failing("Foo blows up", "./spec/foo_spec.rb", 42, "error")], 1);
104
+ const result = await parser.parse(makeCtx(stdout));
105
+ expect(result.failures![0].line).toBe(42);
106
+ });
107
+
108
+ it("pending tests excluded from passed count", async () => {
109
+ const stdout = report(
110
+ [
111
+ passing("Foo works", "./spec/foo_spec.rb", 5),
112
+ { ...passing("Pending thing", "./spec/foo_spec.rb", 20), status: "pending" },
113
+ ],
114
+ 0,
115
+ 1
116
+ );
117
+ const result = await parser.parse(makeCtx(stdout));
118
+ expect(result.status).toBe("pass");
119
+ expect(result.summary).toBe("1 passed");
120
+ });
121
+
122
+ it("empty stdout → status error, no crash", async () => {
123
+ const result = await parser.parse(makeCtx(""));
124
+ expect(result.status).toBe("error");
125
+ });
126
+ });
@@ -0,0 +1,76 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ParserModule } from "../types";
4
+
5
+ interface RSpecException {
6
+ class: string;
7
+ message: string;
8
+ backtrace: string[];
9
+ }
10
+
11
+ interface RSpecExample {
12
+ id: string;
13
+ full_description: string;
14
+ status: "passed" | "failed" | "pending";
15
+ file_path: string;
16
+ line_number: number;
17
+ exception?: RSpecException;
18
+ }
19
+
20
+ interface RSpecReport {
21
+ examples: RSpecExample[];
22
+ summary: {
23
+ example_count: number;
24
+ failure_count: number;
25
+ pending_count: number;
26
+ };
27
+ }
28
+
29
+ /** Take all non-empty lines before the first blank line — captures expected/got pairs intact. */
30
+ function firstParagraph(message: string): string {
31
+ const lines = message.trim().split("\n");
32
+ const paragraph: string[] = [];
33
+ for (const line of lines) {
34
+ if (line.trim() === "") break;
35
+ paragraph.push(line.trim());
36
+ }
37
+ return paragraph.join(" / ");
38
+ }
39
+
40
+ const parser: ParserModule = {
41
+ id: "rspec-json",
42
+ async parse(ctx) {
43
+ const stdout = fs.readFileSync(ctx.stdoutPath, "utf8").trim();
44
+ const report = stdout ? (JSON.parse(stdout) as RSpecReport) : null;
45
+ if (!report) {
46
+ return {
47
+ tool: "rspec",
48
+ status: "error",
49
+ summary: "no output",
50
+ logPath: ctx.logPath,
51
+ };
52
+ }
53
+
54
+ const failures = report.examples
55
+ .filter((e) => e.status === "failed")
56
+ .map((e) => ({
57
+ id: e.id,
58
+ file: path.relative(ctx.cwd, path.resolve(ctx.cwd, e.file_path)),
59
+ line: e.line_number,
60
+ message: e.exception ? firstParagraph(e.exception.message) : e.full_description,
61
+ }));
62
+
63
+ const failed = report.summary.failure_count;
64
+ const passed = report.summary.example_count - failed - report.summary.pending_count;
65
+
66
+ return {
67
+ tool: "rspec",
68
+ status: failed > 0 ? "fail" : "pass",
69
+ summary: failed > 0 ? `${failed} failed, ${passed} passed` : `${passed} passed`,
70
+ failures,
71
+ logPath: ctx.logPath,
72
+ };
73
+ },
74
+ };
75
+
76
+ export default parser;
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, it, expect } from "vitest";
5
+ import parser from "./ruff-json";
6
+ import type { RunContext } from "../types";
7
+
8
+ function makeCtx(stdout: string, cwd = "/project"): RunContext {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ruff-test-"));
10
+ const stdoutPath = path.join(dir, "stdout");
11
+ fs.writeFileSync(stdoutPath, stdout);
12
+ return {
13
+ command: "ruff check . --output-format=json",
14
+ argv: ["ruff", "check", ".", "--output-format=json"],
15
+ cwd,
16
+ artifactPaths: [],
17
+ stdoutPath,
18
+ stderrPath: path.join(dir, "stderr"),
19
+ logPath: path.join(dir, "log"),
20
+ };
21
+ }
22
+
23
+ describe("ruff-json parser", () => {
24
+ it("multiple errors across multiple files → correct relative paths, rule code mapped to rule, status fail", async () => {
25
+ const cwd = "/project";
26
+ const stdout = JSON.stringify([
27
+ { filename: "/project/src/foo.py", code: "F401", message: "`os` imported but unused", location: { row: 1 } },
28
+ { filename: "/project/src/foo.py", code: "E741", message: "Ambiguous variable name: `l`", location: { row: 5 } },
29
+ {
30
+ filename: "/project/src/bar.py",
31
+ code: "F841",
32
+ message: "Local variable `x` is assigned to but never used",
33
+ location: { row: 3 },
34
+ },
35
+ ]);
36
+ const result = await parser.parse(makeCtx(stdout, cwd));
37
+ expect(result.status).toBe("fail");
38
+ expect(result.failures).toHaveLength(3);
39
+ expect(result.failures![0].file).toBe("src/foo.py");
40
+ expect(result.failures![2].file).toBe("src/bar.py");
41
+ expect(result.failures![0].file).not.toContain("/project");
42
+ expect(result.failures![0].rule).toBe("F401");
43
+ expect(result.failures![1].rule).toBe("E741");
44
+ });
45
+
46
+ it("no errors → empty failures, status pass", async () => {
47
+ const result = await parser.parse(makeCtx(JSON.stringify([])));
48
+ expect(result.status).toBe("pass");
49
+ expect(result.failures).toHaveLength(0);
50
+ });
51
+
52
+ it("empty stdout → no crash, status pass", async () => {
53
+ const result = await parser.parse(makeCtx(""));
54
+ expect(result.status).toBe("pass");
55
+ expect(result.failures).toHaveLength(0);
56
+ });
57
+ });
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ParserModule } from "../types";
4
+
5
+ interface RuffItem {
6
+ filename: string;
7
+ code: string;
8
+ message: string;
9
+ location?: { row: number };
10
+ }
11
+
12
+ const parser: ParserModule = {
13
+ id: "ruff-json",
14
+ async parse(ctx) {
15
+ const stdout = fs.readFileSync(ctx.stdoutPath, "utf8").trim();
16
+ const items = stdout ? (JSON.parse(stdout) as RuffItem[]) : [];
17
+ const failures = (Array.isArray(items) ? items : []).map((item) => {
18
+ const relPath = path.relative(ctx.cwd, item.filename);
19
+ return {
20
+ id: `${relPath}:${item.location?.row}:${item.code}`,
21
+ file: relPath,
22
+ line: item.location?.row,
23
+ message: item.message,
24
+ rule: item.code,
25
+ };
26
+ });
27
+ return {
28
+ tool: "ruff",
29
+ status: failures.length > 0 ? "fail" : "pass",
30
+ summary: failures.length > 0 ? `${failures.length} lint errors` : "no lint errors",
31
+ failures,
32
+ logPath: ctx.logPath,
33
+ };
34
+ },
35
+ };
36
+
37
+ export default parser;
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, it, expect } from "vitest";
5
+ import parser from "./tail-fallback";
6
+ import type { RunContext } from "../types";
7
+
8
+ function makeCtx(logContent: string): RunContext {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tail-test-"));
10
+ const logPath = path.join(dir, "log");
11
+ fs.writeFileSync(logPath, logContent);
12
+ return {
13
+ command: "some-command",
14
+ argv: ["some-command"],
15
+ cwd: "/project",
16
+ artifactPaths: [],
17
+ stdoutPath: path.join(dir, "stdout"),
18
+ stderrPath: path.join(dir, "stderr"),
19
+ logPath,
20
+ };
21
+ }
22
+
23
+ describe("tail-fallback parser", () => {
24
+ it("any command → status error, summary contains log path", async () => {
25
+ const ctx = makeCtx("some output");
26
+ const result = await parser.parse(ctx);
27
+ expect(result.status).toBe("error");
28
+ expect(result.summary).toBe("no parser matched; returning tail + log path");
29
+ expect(result.logPath).toBe(ctx.logPath);
30
+ });
31
+
32
+ it("long stdout → tail is bounded to last 200 lines", async () => {
33
+ const lines = Array.from({ length: 300 }, (_, i) => `line ${i + 1}`);
34
+ const ctx = makeCtx(lines.join("\n"));
35
+ const result = await parser.parse(ctx);
36
+ const tailLines = result.rawTail!.split("\n");
37
+ expect(tailLines.length).toBeLessThanOrEqual(200);
38
+ expect(tailLines[tailLines.length - 1]).toBe("line 300");
39
+ expect(tailLines[0]).toBe("line 101");
40
+ });
41
+ });
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import type { ParserModule } from "../types";
3
+
4
+ const parser: ParserModule = {
5
+ id: "tail-fallback",
6
+ async parse(ctx) {
7
+ const log = fs.readFileSync(ctx.logPath, "utf8");
8
+ const lines = log.split(/\r?\n/);
9
+ const tail = lines.slice(-200).join("\n");
10
+ return {
11
+ tool: "unknown",
12
+ status: "error",
13
+ summary: "no parser matched; returning tail + log path",
14
+ logPath: ctx.logPath,
15
+ rawTail: tail,
16
+ };
17
+ },
18
+ };
19
+
20
+ export default parser;
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, it, expect } from "vitest";
5
+ import parser from "./vitest-json";
6
+ import type { RunContext } from "../types";
7
+
8
+ function makeCtx(stdout: string, cwd = "/project"): RunContext {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vitest-test-"));
10
+ const stdoutPath = path.join(dir, "stdout");
11
+ fs.writeFileSync(stdoutPath, stdout);
12
+ return {
13
+ command: "vitest run --reporter=json",
14
+ argv: ["vitest", "run", "--reporter=json"],
15
+ cwd,
16
+ artifactPaths: [],
17
+ stdoutPath,
18
+ stderrPath: path.join(dir, "stderr"),
19
+ logPath: path.join(dir, "log"),
20
+ };
21
+ }
22
+
23
+ const passing = (name: string) => ({
24
+ fullName: name,
25
+ status: "passed" as const,
26
+ failureMessages: [],
27
+ });
28
+
29
+ const failing = (name: string, message: string) => ({
30
+ fullName: name,
31
+ status: "failed" as const,
32
+ failureMessages: [`${message}\n at Object.<anonymous> (test.ts:10:5)`],
33
+ });
34
+
35
+ describe("vitest-json parser", () => {
36
+ it("all passing → status pass, summary reflects passed count", async () => {
37
+ const report = {
38
+ numPassedTests: 5,
39
+ numFailedTests: 0,
40
+ testResults: [
41
+ {
42
+ name: "/project/src/foo.test.ts",
43
+ status: "passed",
44
+ assertionResults: [passing("foo test a"), passing("foo test b")],
45
+ },
46
+ ],
47
+ };
48
+ const result = await parser.parse(makeCtx(JSON.stringify(report)));
49
+ expect(result.status).toBe("pass");
50
+ expect(result.summary).toBe("5 passed");
51
+ expect(result.failures).toHaveLength(0);
52
+ });
53
+
54
+ it("mix of passed and failed → status fail, correct counts, failures listed", async () => {
55
+ const cwd = "/project";
56
+ const report = {
57
+ numPassedTests: 3,
58
+ numFailedTests: 2,
59
+ testResults: [
60
+ {
61
+ name: "/project/src/foo.test.ts",
62
+ status: "failed",
63
+ assertionResults: [passing("foo passes"), failing("foo fails A", "AssertionError: expected 1 to equal 2")],
64
+ },
65
+ {
66
+ name: "/project/src/bar.test.ts",
67
+ status: "failed",
68
+ assertionResults: [failing("bar fails B", "AssertionError: expected true to be false")],
69
+ },
70
+ ],
71
+ };
72
+ const result = await parser.parse(makeCtx(JSON.stringify(report), cwd));
73
+ expect(result.status).toBe("fail");
74
+ expect(result.summary).toBe("2 failed, 3 passed");
75
+ expect(result.failures).toHaveLength(2);
76
+ expect(result.failures![0].file).toBe("src/foo.test.ts");
77
+ expect(result.failures![1].file).toBe("src/bar.test.ts");
78
+ });
79
+
80
+ it("failure message → first line only surfaced", async () => {
81
+ const report = {
82
+ numPassedTests: 0,
83
+ numFailedTests: 1,
84
+ testResults: [
85
+ {
86
+ name: "/project/src/foo.test.ts",
87
+ status: "failed",
88
+ assertionResults: [failing("foo fails", "AssertionError: expected 1 to equal 2")],
89
+ },
90
+ ],
91
+ };
92
+ const result = await parser.parse(makeCtx(JSON.stringify(report)));
93
+ expect(result.failures![0].message).toBe("AssertionError: expected 1 to equal 2");
94
+ });
95
+
96
+ it("relative paths in failure file field", async () => {
97
+ const cwd = "/project";
98
+ const report = {
99
+ numPassedTests: 0,
100
+ numFailedTests: 1,
101
+ testResults: [
102
+ {
103
+ name: "/project/src/deep/foo.test.ts",
104
+ status: "failed",
105
+ assertionResults: [failing("foo fails", "Error")],
106
+ },
107
+ ],
108
+ };
109
+ const result = await parser.parse(makeCtx(JSON.stringify(report), cwd));
110
+ expect(result.failures![0].file).toBe("src/deep/foo.test.ts");
111
+ expect(result.failures![0].file).not.toContain("/project");
112
+ });
113
+
114
+ it("empty stdout → status error, no crash", async () => {
115
+ const result = await parser.parse(makeCtx(""));
116
+ expect(result.status).toBe("error");
117
+ });
118
+ });
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ParserModule } from "../types";
4
+
5
+ interface VitestAssertionResult {
6
+ fullName: string;
7
+ status: "passed" | "failed";
8
+ failureMessages: string[];
9
+ }
10
+
11
+ interface VitestTestResult {
12
+ name: string;
13
+ status: "passed" | "failed";
14
+ assertionResults: VitestAssertionResult[];
15
+ }
16
+
17
+ interface VitestReport {
18
+ numPassedTests: number;
19
+ numFailedTests: number;
20
+ testResults: VitestTestResult[];
21
+ }
22
+
23
+ const parser: ParserModule = {
24
+ id: "vitest-json",
25
+ async parse(ctx) {
26
+ const stdout = fs.readFileSync(ctx.stdoutPath, "utf8").trim();
27
+ const report = stdout ? (JSON.parse(stdout) as VitestReport) : null;
28
+ if (!report) {
29
+ return {
30
+ tool: "vitest",
31
+ status: "error",
32
+ summary: "no output",
33
+ logPath: ctx.logPath,
34
+ };
35
+ }
36
+
37
+ const failures = report.testResults.flatMap((suite) =>
38
+ suite.assertionResults
39
+ .filter((t) => t.status === "failed")
40
+ .map((t) => ({
41
+ id: t.fullName,
42
+ file: path.relative(ctx.cwd, suite.name),
43
+ message: t.failureMessages[0]?.split("\n")[0] ?? "test failed",
44
+ }))
45
+ );
46
+
47
+ const failed = report.numFailedTests;
48
+ const passed = report.numPassedTests;
49
+
50
+ return {
51
+ tool: "vitest",
52
+ status: failed > 0 ? "fail" : "pass",
53
+ summary: failed > 0 ? `${failed} failed, ${passed} passed` : `${passed} passed`,
54
+ failures,
55
+ logPath: ctx.logPath,
56
+ };
57
+ },
58
+ };
59
+
60
+ export default parser;
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function ensureRunDir(cwd: string): string {
5
+ const dir = path.join(cwd, ".pi", "structured-returns");
6
+ fs.mkdirSync(dir, { recursive: true });
7
+ return dir;
8
+ }
9
+
10
+ export function writeRunArtifacts(
11
+ dir: string,
12
+ runId: string,
13
+ stdout: string,
14
+ stderr: string
15
+ ): { stdoutPath: string; stderrPath: string; logPath: string } {
16
+ const stdoutPath = path.join(dir, `${runId}.stdout.log`);
17
+ const stderrPath = path.join(dir, `${runId}.stderr.log`);
18
+ const logPath = path.join(dir, `${runId}.combined.log`);
19
+ fs.writeFileSync(stdoutPath, stdout);
20
+ fs.writeFileSync(stderrPath, stderr);
21
+ fs.writeFileSync(logPath, [stdout, stderr].filter(Boolean).join("\n"));
22
+ return { stdoutPath, stderrPath, logPath };
23
+ }
@@ -0,0 +1,55 @@
1
+ export type ParsedFailure = {
2
+ id: string;
3
+ file?: string;
4
+ line?: number;
5
+ message?: string;
6
+ rule?: string;
7
+ };
8
+
9
+ export type ParsedResult = {
10
+ tool: string;
11
+ exitCode: number;
12
+ status: "pass" | "fail" | "error";
13
+ summary: string;
14
+ cwd?: string;
15
+ failures?: ParsedFailure[];
16
+ artifact?: string;
17
+ logPath?: string;
18
+ rawTail?: string;
19
+ };
20
+
21
+ export type ObservedRunArgs = {
22
+ command: string;
23
+ cwd?: string;
24
+ parseAs?: string;
25
+ artifactPaths?: string[];
26
+ };
27
+
28
+ export type RunContext = {
29
+ command: string;
30
+ argv: string[];
31
+ cwd: string;
32
+ artifactPaths: string[];
33
+ stdoutPath: string;
34
+ stderrPath: string;
35
+ logPath: string;
36
+ };
37
+
38
+ export type ParserModule = {
39
+ id: string;
40
+ parse: (ctx: RunContext) => Promise<Omit<ParsedResult, "exitCode">>;
41
+ };
42
+
43
+ export type ParserRegistration = {
44
+ id: string;
45
+ match?: {
46
+ argvIncludes?: string[];
47
+ regex?: string;
48
+ };
49
+ parseAs?: string;
50
+ module?: string;
51
+ };
52
+
53
+ export type ParserConfigFile = {
54
+ parsers: ParserRegistration[];
55
+ };
@@ -0,0 +1,3 @@
1
+ export function renderWidgetLine(command: string, summary: string): string[] {
2
+ return [`structured_return ${command}`, `→ ${summary}`];
3
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true
9
+ },
10
+ "include": ["src/**/*.ts", "examples/.pi/parsers/**/*.ts"]
11
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import path from "node:path";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ root: path.resolve(import.meta.dirname, "src"),
7
+ include: ["**/*.test.ts"],
8
+ },
9
+ });