pi-cicd 0.1.1 → 0.3.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/README.md +62 -0
- package/dist/ci/pipeline.d.ts +43 -0
- package/dist/ci/pipeline.d.ts.map +1 -0
- package/dist/ci/pipeline.js +107 -0
- package/dist/ci/pipeline.js.map +1 -0
- package/dist/ci/pr-creator.d.ts +17 -0
- package/dist/ci/pr-creator.d.ts.map +1 -0
- package/dist/ci/pr-creator.js +67 -0
- package/dist/ci/pr-creator.js.map +1 -0
- package/dist/ci/report.d.ts +14 -0
- package/dist/ci/report.d.ts.map +1 -0
- package/dist/ci/report.js +51 -0
- package/dist/ci/report.js.map +1 -0
- package/dist/ci/test-runner.d.ts +10 -0
- package/dist/ci/test-runner.d.ts.map +1 -0
- package/dist/ci/test-runner.js +111 -0
- package/dist/ci/test-runner.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +67 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy/canary-deploy.d.ts +80 -0
- package/dist/deploy/canary-deploy.d.ts.map +1 -0
- package/dist/deploy/canary-deploy.js +145 -0
- package/dist/deploy/canary-deploy.js.map +1 -0
- package/dist/deploy/landing-queue.d.ts +83 -0
- package/dist/deploy/landing-queue.d.ts.map +1 -0
- package/dist/deploy/landing-queue.js +172 -0
- package/dist/deploy/landing-queue.js.map +1 -0
- package/dist/headless/answer-injector.d.ts +27 -0
- package/dist/headless/answer-injector.d.ts.map +1 -0
- package/dist/headless/answer-injector.js +80 -0
- package/dist/headless/answer-injector.js.map +1 -0
- package/dist/headless/exit-codes.d.ts +13 -0
- package/dist/headless/exit-codes.d.ts.map +1 -0
- package/dist/headless/exit-codes.js +29 -0
- package/dist/headless/exit-codes.js.map +1 -0
- package/dist/headless/idle-detector.d.ts +32 -0
- package/dist/headless/idle-detector.d.ts.map +1 -0
- package/dist/headless/idle-detector.js +62 -0
- package/dist/headless/idle-detector.js.map +1 -0
- package/dist/headless/jsonl-stream.d.ts +28 -0
- package/dist/headless/jsonl-stream.d.ts.map +1 -0
- package/dist/headless/jsonl-stream.js +65 -0
- package/dist/headless/jsonl-stream.js.map +1 -0
- package/dist/headless/orchestrator.d.ts +63 -0
- package/dist/headless/orchestrator.d.ts.map +1 -0
- package/dist/headless/orchestrator.js +156 -0
- package/dist/headless/orchestrator.js.map +1 -0
- package/{index.ts → dist/index.d.ts} +4 -26
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/ci_status.d.ts +40 -0
- package/dist/tools/ci_status.d.ts.map +1 -0
- package/dist/tools/ci_status.js +110 -0
- package/dist/tools/ci_status.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow/deployment-workflow.d.ts +56 -0
- package/dist/workflow/deployment-workflow.d.ts.map +1 -0
- package/dist/workflow/deployment-workflow.js +95 -0
- package/dist/workflow/deployment-workflow.js.map +1 -0
- package/package.json +26 -26
- package/AGENTS.md +0 -25
- package/src/ci/pipeline.ts +0 -130
- package/src/ci/pr-creator.ts +0 -74
- package/src/ci/report.ts +0 -65
- package/src/ci/test-runner.ts +0 -129
- package/src/config.ts +0 -99
- package/src/headless/answer-injector.ts +0 -98
- package/src/headless/exit-codes.ts +0 -32
- package/src/headless/idle-detector.ts +0 -76
- package/src/headless/jsonl-stream.ts +0 -90
- package/src/headless/orchestrator.ts +0 -206
- package/src/tools/ci_status.ts +0 -137
- package/src/types.ts +0 -149
- package/tsconfig.json +0 -19
package/src/ci/pr-creator.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — PR creation via the GitHub CLI (`gh`).
|
|
3
|
-
*
|
|
4
|
-
* Wraps `gh pr create` with structured error handling.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
|
-
import type { PROptions, PRResult } from "../types.ts";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Create a pull request using the `gh` CLI.
|
|
12
|
-
*
|
|
13
|
-
* Throws if `gh` is not installed or the command fails.
|
|
14
|
-
*/
|
|
15
|
-
export async function createPR(options: PROptions): Promise<PRResult> {
|
|
16
|
-
const args = ["pr", "create", "--title", options.title];
|
|
17
|
-
|
|
18
|
-
if (options.body) {
|
|
19
|
-
args.push("--body", options.body);
|
|
20
|
-
}
|
|
21
|
-
if (options.base) {
|
|
22
|
-
args.push("--base", options.base);
|
|
23
|
-
}
|
|
24
|
-
if (options.head) {
|
|
25
|
-
args.push("--head", options.head);
|
|
26
|
-
}
|
|
27
|
-
if (options.draft) {
|
|
28
|
-
args.push("--draft");
|
|
29
|
-
}
|
|
30
|
-
for (const label of options.labels ?? []) {
|
|
31
|
-
args.push("--label", label);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const output = await runGh(args);
|
|
35
|
-
|
|
36
|
-
// Parse the PR URL from the output
|
|
37
|
-
const urlMatch = output.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
|
|
38
|
-
if (!urlMatch) {
|
|
39
|
-
throw new Error(`Failed to parse PR URL from gh output: ${output}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
url: urlMatch[0],
|
|
44
|
-
number: parseInt(urlMatch[1], 10),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Detect the default (base) branch for the current repository.
|
|
50
|
-
*/
|
|
51
|
-
export async function detectBaseBranch(): Promise<string> {
|
|
52
|
-
const output = await runGh(["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"]);
|
|
53
|
-
return output.trim() || "main";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Execute a `gh` command and return stdout.
|
|
58
|
-
*/
|
|
59
|
-
function runGh(args: string[]): Promise<string> {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
execFile("gh", args, { timeout: 60_000 }, (err, stdout, stderr) => {
|
|
62
|
-
if (err) {
|
|
63
|
-
const message = stderr?.trim() || err.message;
|
|
64
|
-
if (err.code === "ENOENT") {
|
|
65
|
-
reject(new Error("gh CLI is not installed. Install it from https://cli.github.com"));
|
|
66
|
-
} else {
|
|
67
|
-
reject(new Error(`gh ${args.join(" ")} failed: ${message}`));
|
|
68
|
-
}
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
resolve(stdout);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
}
|
package/src/ci/report.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — CI report generation.
|
|
3
|
-
*
|
|
4
|
-
* Produces JSONL or human-readable summary reports from collected CI events.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { CIEvent, CIEndEvent, CICostEvent, CITestEvent } from "../types.ts";
|
|
8
|
-
import { isCIEndEvent, isCICostEvent, isCITestEvent } from "../headless/jsonl-stream.ts";
|
|
9
|
-
|
|
10
|
-
export interface ReportOptions {
|
|
11
|
-
format: "jsonl" | "summary";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generate a report from a list of CI events.
|
|
16
|
-
*/
|
|
17
|
-
export function generateReport(events: CIEvent[], options?: ReportOptions): string {
|
|
18
|
-
const format = options?.format ?? "jsonl";
|
|
19
|
-
|
|
20
|
-
if (format === "summary") {
|
|
21
|
-
return generateSummary(events);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// JSONL: one event per line
|
|
25
|
-
return events.map((e) => JSON.stringify(e)).join("\n");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Generate a human-readable summary.
|
|
30
|
-
*/
|
|
31
|
-
function generateSummary(events: CIEvent[]): string {
|
|
32
|
-
const lines: string[] = ["=== CI Run Summary ===", ""];
|
|
33
|
-
|
|
34
|
-
let totalTestsPassed = 0;
|
|
35
|
-
let totalTestsFailed = 0;
|
|
36
|
-
let totalCostUsd = 0;
|
|
37
|
-
let exitCode = -1;
|
|
38
|
-
let durationMs = 0;
|
|
39
|
-
|
|
40
|
-
for (const event of events) {
|
|
41
|
-
if (isCITestEvent(event)) {
|
|
42
|
-
totalTestsPassed += event.passed;
|
|
43
|
-
totalTestsFailed += event.failed;
|
|
44
|
-
}
|
|
45
|
-
if (isCICostEvent(event)) {
|
|
46
|
-
totalCostUsd += event.cost_usd;
|
|
47
|
-
}
|
|
48
|
-
if (isCIEndEvent(event)) {
|
|
49
|
-
exitCode = event.exit_code;
|
|
50
|
-
durationMs = event.duration_ms;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
lines.push(`Exit Code: ${exitCode}`);
|
|
55
|
-
lines.push(`Duration: ${(durationMs / 1000).toFixed(1)}s`);
|
|
56
|
-
lines.push(`Tests: ${totalTestsPassed} passed, ${totalTestsFailed} failed`);
|
|
57
|
-
if (totalCostUsd > 0) {
|
|
58
|
-
lines.push(`Cost: $${totalCostUsd.toFixed(4)}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const status = exitCode === 0 ? "SUCCESS" : exitCode === 10 ? "BLOCKED" : exitCode === 11 ? "CANCELLED" : "ERROR";
|
|
62
|
-
lines.push(`Status: ${status}`);
|
|
63
|
-
|
|
64
|
-
return lines.join("\n");
|
|
65
|
-
}
|
package/src/ci/test-runner.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — Test result parsing for TAP, Jest, and Vitest output formats.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { TestSummary } from "../types.ts";
|
|
6
|
-
|
|
7
|
-
export type TestOutputFormat = "tap" | "jest" | "vitest";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse test output and return a summary.
|
|
11
|
-
*/
|
|
12
|
-
export function parseTestResults(output: string, format: TestOutputFormat): TestSummary {
|
|
13
|
-
if (!output || !output.trim()) {
|
|
14
|
-
return { passed: 0, failed: 0, total: 0, duration_ms: 0 };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
switch (format) {
|
|
18
|
-
case "tap":
|
|
19
|
-
return parseTap(output);
|
|
20
|
-
case "jest":
|
|
21
|
-
return parseJest(output);
|
|
22
|
-
case "vitest":
|
|
23
|
-
return parseVitest(output);
|
|
24
|
-
default:
|
|
25
|
-
return { passed: 0, failed: 0, total: 0, duration_ms: 0 };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Parse TAP (Test Anything Protocol) output.
|
|
31
|
-
*
|
|
32
|
-
* Example:
|
|
33
|
-
* 1..5
|
|
34
|
-
* ok 1 - first test
|
|
35
|
-
* not ok 2 - failing test
|
|
36
|
-
* ok 3 - third test
|
|
37
|
-
*/
|
|
38
|
-
function parseTap(output: string): TestSummary {
|
|
39
|
-
let passed = 0;
|
|
40
|
-
let failed = 0;
|
|
41
|
-
|
|
42
|
-
const lines = output.split("\n");
|
|
43
|
-
for (const line of lines) {
|
|
44
|
-
const trimmed = line.trim();
|
|
45
|
-
if (trimmed.startsWith("ok ")) {
|
|
46
|
-
passed++;
|
|
47
|
-
} else if (trimmed.startsWith("not ok ")) {
|
|
48
|
-
failed++;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
passed,
|
|
54
|
-
failed,
|
|
55
|
-
total: passed + failed,
|
|
56
|
-
duration_ms: 0, // TAP doesn't have standard duration
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Parse Jest-style test output.
|
|
62
|
-
*
|
|
63
|
-
* Looks for patterns like:
|
|
64
|
-
* Tests: 5 passed, 2 failed, 7 total
|
|
65
|
-
* Time: 3.456 s
|
|
66
|
-
*/
|
|
67
|
-
function parseJest(output: string): TestSummary {
|
|
68
|
-
let passed = 0;
|
|
69
|
-
let failed = 0;
|
|
70
|
-
let total = 0;
|
|
71
|
-
let durationMs = 0;
|
|
72
|
-
|
|
73
|
-
// Match test summary line
|
|
74
|
-
const testMatch = output.match(
|
|
75
|
-
/Tests:\s*(\d+)\s*passed(?:,\s*(\d+)\s*failed)?(?:,\s*(\d+)\s*total)?/,
|
|
76
|
-
);
|
|
77
|
-
if (testMatch) {
|
|
78
|
-
passed = parseInt(testMatch[1], 10);
|
|
79
|
-
failed = parseInt(testMatch[2] || "0", 10);
|
|
80
|
-
total = parseInt(testMatch[3] || String(passed + failed), 10);
|
|
81
|
-
} else {
|
|
82
|
-
// Fallback: count individual test lines
|
|
83
|
-
const passMatches = output.match(/✓|PASS/g);
|
|
84
|
-
const failMatches = output.match(/✕|FAIL/g);
|
|
85
|
-
passed = passMatches?.length ?? 0;
|
|
86
|
-
failed = failMatches?.length ?? 0;
|
|
87
|
-
total = passed + failed;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Match time
|
|
91
|
-
const timeMatch = output.match(/Time:\s*([\d.]+)\s*s/);
|
|
92
|
-
if (timeMatch) {
|
|
93
|
-
durationMs = Math.round(parseFloat(timeMatch[1]) * 1000);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return { passed, failed, total, duration_ms: durationMs };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Parse Vitest-style test output.
|
|
101
|
-
*
|
|
102
|
-
* Looks for patterns like:
|
|
103
|
-
* Tests 5 passed | 2 failed (7)
|
|
104
|
-
* Duration 3.45s
|
|
105
|
-
*/
|
|
106
|
-
function parseVitest(output: string): TestSummary {
|
|
107
|
-
let passed = 0;
|
|
108
|
-
let failed = 0;
|
|
109
|
-
let total = 0;
|
|
110
|
-
let durationMs = 0;
|
|
111
|
-
|
|
112
|
-
// Match test summary line: "Tests 5 passed | 2 failed (7)"
|
|
113
|
-
const testMatch = output.match(
|
|
114
|
-
/Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\((\d+)\))?/,
|
|
115
|
-
);
|
|
116
|
-
if (testMatch) {
|
|
117
|
-
passed = parseInt(testMatch[1], 10);
|
|
118
|
-
failed = parseInt(testMatch[2] || "0", 10);
|
|
119
|
-
total = parseInt(testMatch[3] || String(passed + failed), 10);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Match duration: "Duration 3.45s"
|
|
123
|
-
const durMatch = output.match(/Duration\s+([\d.]+)s/);
|
|
124
|
-
if (durMatch) {
|
|
125
|
-
durationMs = Math.round(parseFloat(durMatch[1]) * 1000);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { passed, failed, total, duration_ms: durationMs };
|
|
129
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — Configuration loading and defaults.
|
|
3
|
-
*
|
|
4
|
-
* Reads `.pi/pi-ci.json` from the given directory (or cwd) and merges with defaults.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
|
|
10
|
-
export interface PiCiReportConfig {
|
|
11
|
-
format: "jsonl" | "summary";
|
|
12
|
-
includeCost: boolean;
|
|
13
|
-
includeEdits: boolean;
|
|
14
|
-
includeTests: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface PiCiExitCodeConfig {
|
|
18
|
-
success: number;
|
|
19
|
-
error: number;
|
|
20
|
-
blocked: number;
|
|
21
|
-
cancelled: number;
|
|
22
|
-
needsInput: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface PiCiConfig {
|
|
26
|
-
enabled: boolean;
|
|
27
|
-
idleTimeoutMs: number;
|
|
28
|
-
maxRetries: number;
|
|
29
|
-
retryBackoffMaxMs: number;
|
|
30
|
-
exitCodes: PiCiExitCodeConfig;
|
|
31
|
-
report: PiCiReportConfig;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const DEFAULT_CONFIG: PiCiConfig = {
|
|
35
|
-
enabled: true,
|
|
36
|
-
idleTimeoutMs: 15_000,
|
|
37
|
-
maxRetries: 3,
|
|
38
|
-
retryBackoffMaxMs: 30_000,
|
|
39
|
-
exitCodes: {
|
|
40
|
-
success: 0,
|
|
41
|
-
error: 1,
|
|
42
|
-
blocked: 10,
|
|
43
|
-
cancelled: 11,
|
|
44
|
-
needsInput: 12,
|
|
45
|
-
},
|
|
46
|
-
report: {
|
|
47
|
-
format: "jsonl",
|
|
48
|
-
includeCost: true,
|
|
49
|
-
includeEdits: true,
|
|
50
|
-
includeTests: true,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Load pi-ci configuration from `.pi/pi-ci.json` (if present) merged with defaults.
|
|
56
|
-
*/
|
|
57
|
-
export async function loadCiConfig(cwd?: string): Promise<PiCiConfig> {
|
|
58
|
-
const dir = cwd ?? process.cwd();
|
|
59
|
-
const configPath = join(dir, ".pi", "pi-ci.json");
|
|
60
|
-
|
|
61
|
-
let text: string;
|
|
62
|
-
try {
|
|
63
|
-
text = await readFile(configPath, "utf-8");
|
|
64
|
-
} catch {
|
|
65
|
-
return { ...DEFAULT_CONFIG };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const raw: unknown = JSON.parse(text);
|
|
69
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
70
|
-
return { ...DEFAULT_CONFIG };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const user = raw as Record<string, unknown>;
|
|
74
|
-
return {
|
|
75
|
-
enabled: typeof user.enabled === "boolean" ? user.enabled : DEFAULT_CONFIG.enabled,
|
|
76
|
-
idleTimeoutMs:
|
|
77
|
-
typeof user.idleTimeoutMs === "number" ? user.idleTimeoutMs : DEFAULT_CONFIG.idleTimeoutMs,
|
|
78
|
-
maxRetries:
|
|
79
|
-
typeof user.maxRetries === "number" ? user.maxRetries : DEFAULT_CONFIG.maxRetries,
|
|
80
|
-
retryBackoffMaxMs:
|
|
81
|
-
typeof user.retryBackoffMaxMs === "number"
|
|
82
|
-
? user.retryBackoffMaxMs
|
|
83
|
-
: DEFAULT_CONFIG.retryBackoffMaxMs,
|
|
84
|
-
exitCodes: {
|
|
85
|
-
...DEFAULT_CONFIG.exitCodes,
|
|
86
|
-
...(typeof user.exitCodes === "object" && user.exitCodes !== null
|
|
87
|
-
? (user.exitCodes as Partial<PiCiExitCodeConfig>)
|
|
88
|
-
: {}),
|
|
89
|
-
},
|
|
90
|
-
report: {
|
|
91
|
-
...DEFAULT_CONFIG.report,
|
|
92
|
-
...(typeof user.report === "object" && user.report !== null
|
|
93
|
-
? (user.report as Partial<PiCiReportConfig>)
|
|
94
|
-
: {}),
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export { DEFAULT_CONFIG };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — Answer injection from a JSON file.
|
|
3
|
-
*
|
|
4
|
-
* When Pi encounters an interactive prompt in CI mode, it consults an
|
|
5
|
-
* answers file for a pre-supplied response.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AnswerEntry, AnswerFile } from "../types.ts";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Read and validate an answers JSON file.
|
|
12
|
-
*
|
|
13
|
-
* - Returns an empty array if the file cannot be read.
|
|
14
|
-
* - Skips entries that are missing `match` or `answer` fields.
|
|
15
|
-
* - Throws on invalid JSON.
|
|
16
|
-
*/
|
|
17
|
-
export async function loadAnswers(filePath: string): Promise<AnswerEntry[]> {
|
|
18
|
-
let text: string;
|
|
19
|
-
try {
|
|
20
|
-
text = await Bun.file(filePath).text();
|
|
21
|
-
} catch {
|
|
22
|
-
return [];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const raw: unknown = JSON.parse(text);
|
|
26
|
-
|
|
27
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
28
|
-
throw new Error(`Answers file must contain a JSON object with an "answers" array`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const obj = raw as Record<string, unknown>;
|
|
32
|
-
if (!Array.isArray(obj.answers)) {
|
|
33
|
-
throw new Error(`Answers file must contain an "answers" array`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const entries: AnswerEntry[] = [];
|
|
37
|
-
for (const item of obj.answers) {
|
|
38
|
-
if (
|
|
39
|
-
typeof item === "object" &&
|
|
40
|
-
item !== null &&
|
|
41
|
-
typeof (item as Record<string, unknown>).match === "string" &&
|
|
42
|
-
typeof (item as Record<string, unknown>).answer === "string"
|
|
43
|
-
) {
|
|
44
|
-
entries.push(item as AnswerEntry);
|
|
45
|
-
}
|
|
46
|
-
// Silently skip malformed entries
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return entries;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Synchronous variant that reads from a string (useful for testing).
|
|
54
|
-
*/
|
|
55
|
-
export function parseAnswers(jsonText: string): AnswerEntry[] {
|
|
56
|
-
const raw: unknown = JSON.parse(jsonText);
|
|
57
|
-
|
|
58
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
59
|
-
throw new Error(`Answers file must contain a JSON object with an "answers" array`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const obj = raw as Record<string, unknown>;
|
|
63
|
-
if (!Array.isArray(obj.answers)) {
|
|
64
|
-
throw new Error(`Answers file must contain an "answers" array`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const entries: AnswerEntry[] = [];
|
|
68
|
-
for (const item of obj.answers) {
|
|
69
|
-
if (
|
|
70
|
-
typeof item === "object" &&
|
|
71
|
-
item !== null &&
|
|
72
|
-
typeof (item as Record<string, unknown>).match === "string" &&
|
|
73
|
-
typeof (item as Record<string, unknown>).answer === "string"
|
|
74
|
-
) {
|
|
75
|
-
entries.push(item as AnswerEntry);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return entries;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Find a matching answer for the given prompt using substring matching.
|
|
84
|
-
*
|
|
85
|
-
* Returns the first answer whose `match` is found as a substring of `prompt`,
|
|
86
|
-
* or `undefined` if no match is found.
|
|
87
|
-
*/
|
|
88
|
-
export function matchAnswer(
|
|
89
|
-
entries: AnswerEntry[],
|
|
90
|
-
prompt: string,
|
|
91
|
-
): string | undefined {
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
if (prompt.includes(entry.match)) {
|
|
94
|
-
return entry.answer;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — Exit code resolution helpers.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { EXIT_CODES, type ExitCode } from "../types.ts";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Map a symbolic status string to a numeric exit code.
|
|
9
|
-
*
|
|
10
|
-
* Unknown / unexpected statuses resolve to ERROR (1).
|
|
11
|
-
*/
|
|
12
|
-
export function resolveExitCode(status: string): ExitCode {
|
|
13
|
-
switch (status) {
|
|
14
|
-
case "success":
|
|
15
|
-
return EXIT_CODES.SUCCESS;
|
|
16
|
-
case "error":
|
|
17
|
-
case "timeout":
|
|
18
|
-
return EXIT_CODES.ERROR;
|
|
19
|
-
case "blocked":
|
|
20
|
-
return EXIT_CODES.BLOCKED;
|
|
21
|
-
case "cancelled":
|
|
22
|
-
return EXIT_CODES.CANCELLED;
|
|
23
|
-
case "needs_input":
|
|
24
|
-
case "needs-input":
|
|
25
|
-
return EXIT_CODES.NEEDS_INPUT;
|
|
26
|
-
default:
|
|
27
|
-
return EXIT_CODES.ERROR;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export { EXIT_CODES };
|
|
32
|
-
export type { ExitCode };
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — Idle timeout detection.
|
|
3
|
-
*
|
|
4
|
-
* If no activity is detected within the configured timeout, the callback fires.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface IdleDetectorOptions {
|
|
8
|
-
/** Timeout in milliseconds. Default: 15 000. */
|
|
9
|
-
idleTimeoutMs?: number;
|
|
10
|
-
/** Called when the idle timeout is reached. */
|
|
11
|
-
onTimeout: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DEFAULT_IDLE_TIMEOUT_MS = 15_000;
|
|
15
|
-
|
|
16
|
-
export class IdleDetector {
|
|
17
|
-
private readonly idleTimeoutMs: number;
|
|
18
|
-
private readonly onTimeout: () => void;
|
|
19
|
-
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
-
private _running = false;
|
|
21
|
-
private _fired = false;
|
|
22
|
-
|
|
23
|
-
constructor(options: IdleDetectorOptions) {
|
|
24
|
-
this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
25
|
-
this.onTimeout = options.onTimeout;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Whether the detector is currently running. */
|
|
29
|
-
get running(): boolean {
|
|
30
|
-
return this._running;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Whether the timeout has already fired. */
|
|
34
|
-
get fired(): boolean {
|
|
35
|
-
return this._fired;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Start the idle timer. Safe to call multiple times (no-op if already running). */
|
|
39
|
-
start(): void {
|
|
40
|
-
if (this._running) return;
|
|
41
|
-
this._running = true;
|
|
42
|
-
this._fired = false;
|
|
43
|
-
this.scheduleTimer();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Reset the idle timer. If not running, this is a no-op. */
|
|
47
|
-
reset(): void {
|
|
48
|
-
if (!this._running) return;
|
|
49
|
-
this.clearTimer();
|
|
50
|
-
this.scheduleTimer();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Stop the idle timer. */
|
|
54
|
-
stop(): void {
|
|
55
|
-
this._running = false;
|
|
56
|
-
this._fired = false;
|
|
57
|
-
this.clearTimer();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private scheduleTimer(): void {
|
|
61
|
-
this.timer = setTimeout(() => {
|
|
62
|
-
if (this._running) {
|
|
63
|
-
this._running = false;
|
|
64
|
-
this._fired = true;
|
|
65
|
-
this.onTimeout();
|
|
66
|
-
}
|
|
67
|
-
}, this.idleTimeoutMs);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private clearTimer(): void {
|
|
71
|
-
if (this.timer !== null) {
|
|
72
|
-
clearTimeout(this.timer);
|
|
73
|
-
this.timer = null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-ci — JSONL event stream utilities.
|
|
3
|
-
*
|
|
4
|
-
* Writes CI events as single-line JSON to a writable stream and provides
|
|
5
|
-
* type-guard helpers for event discrimination.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Writable } from "node:stream";
|
|
9
|
-
import type {
|
|
10
|
-
CIEvent,
|
|
11
|
-
CIStartEvent,
|
|
12
|
-
CIProgressEvent,
|
|
13
|
-
CIEditEvent,
|
|
14
|
-
CITestEvent,
|
|
15
|
-
CICostEvent,
|
|
16
|
-
CIEndEvent,
|
|
17
|
-
} from "../types.ts";
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Write helpers
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Ensure an event has a timestamp, injecting `now` if missing.
|
|
25
|
-
*/
|
|
26
|
-
function ensureTimestamp<T extends CIEvent>(event: T): T & { timestamp: string } {
|
|
27
|
-
if (!event.timestamp) {
|
|
28
|
-
return { ...event, timestamp: new Date().toISOString() };
|
|
29
|
-
}
|
|
30
|
-
return event as T & { timestamp: string };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Serialise a CI event and write it as a single JSONL line to the stream.
|
|
35
|
-
*/
|
|
36
|
-
export function writeCIEvent(stream: Writable, event: CIEvent): void {
|
|
37
|
-
const stamped = ensureTimestamp(event);
|
|
38
|
-
stream.write(JSON.stringify(stamped) + "\n");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Event emitter (collects events for reporting)
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
export class CIEventCollector {
|
|
46
|
-
private readonly events: CIEvent[] = [];
|
|
47
|
-
|
|
48
|
-
/** Record an event. Auto-fills timestamp if missing. */
|
|
49
|
-
emit(event: CIEvent): void {
|
|
50
|
-
this.events.push(ensureTimestamp(event));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Return all collected events in order. */
|
|
54
|
-
all(): CIEvent[] {
|
|
55
|
-
return [...this.events];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Reset the collector. */
|
|
59
|
-
clear(): void {
|
|
60
|
-
this.events.length = 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Type guards
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
export function isCIStartEvent(e: CIEvent): e is CIStartEvent {
|
|
69
|
-
return e.type === "ci_start";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function isCIProgressEvent(e: CIEvent): e is CIProgressEvent {
|
|
73
|
-
return e.type === "ci_progress";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function isCIEditEvent(e: CIEvent): e is CIEditEvent {
|
|
77
|
-
return e.type === "ci_edit";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isCITestEvent(e: CIEvent): e is CITestEvent {
|
|
81
|
-
return e.type === "ci_test";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function isCICostEvent(e: CIEvent): e is CICostEvent {
|
|
85
|
-
return e.type === "ci_cost";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isCIEndEvent(e: CIEvent): e is CIEndEvent {
|
|
89
|
-
return e.type === "ci_end";
|
|
90
|
-
}
|