pi-cicd 0.3.0 → 1.0.1

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 (90) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +34 -40
  3. package/docs/API.md +61 -0
  4. package/docs/COMMANDS.md +138 -0
  5. package/docs/CONFIG.md +123 -0
  6. package/docs/GUIDE.md +171 -0
  7. package/docs/PATTERNS.md +49 -0
  8. package/docs/QUICKSTART.md +99 -0
  9. package/{dist/index.d.ts → index.ts} +26 -4
  10. package/install.mjs +34 -0
  11. package/package.json +21 -21
  12. package/skills/intelligent-deploy/SKILL.md +229 -0
  13. package/src/ci/pipeline.ts +130 -0
  14. package/src/ci/pr-creator.ts +74 -0
  15. package/src/ci/report.ts +65 -0
  16. package/src/ci/test-runner.ts +129 -0
  17. package/src/config.ts +99 -0
  18. package/src/deploy/canary-deploy.ts +211 -0
  19. package/src/deploy/landing-queue.ts +222 -0
  20. package/src/headless/answer-injector.ts +99 -0
  21. package/src/headless/exit-codes.ts +32 -0
  22. package/src/headless/idle-detector.ts +76 -0
  23. package/src/headless/jsonl-stream.ts +90 -0
  24. package/src/headless/orchestrator.ts +207 -0
  25. package/{dist/index.js → src/index.ts} +30 -9
  26. package/src/tools/ci_status.ts +137 -0
  27. package/src/types.ts +149 -0
  28. package/src/workflow/deployment-workflow.ts +153 -0
  29. package/dist/ci/pipeline.d.ts +0 -43
  30. package/dist/ci/pipeline.d.ts.map +0 -1
  31. package/dist/ci/pipeline.js +0 -107
  32. package/dist/ci/pipeline.js.map +0 -1
  33. package/dist/ci/pr-creator.d.ts +0 -17
  34. package/dist/ci/pr-creator.d.ts.map +0 -1
  35. package/dist/ci/pr-creator.js +0 -67
  36. package/dist/ci/pr-creator.js.map +0 -1
  37. package/dist/ci/report.d.ts +0 -14
  38. package/dist/ci/report.d.ts.map +0 -1
  39. package/dist/ci/report.js +0 -51
  40. package/dist/ci/report.js.map +0 -1
  41. package/dist/ci/test-runner.d.ts +0 -10
  42. package/dist/ci/test-runner.d.ts.map +0 -1
  43. package/dist/ci/test-runner.js +0 -111
  44. package/dist/ci/test-runner.js.map +0 -1
  45. package/dist/config.d.ts +0 -33
  46. package/dist/config.d.ts.map +0 -1
  47. package/dist/config.js +0 -67
  48. package/dist/config.js.map +0 -1
  49. package/dist/deploy/canary-deploy.d.ts +0 -80
  50. package/dist/deploy/canary-deploy.d.ts.map +0 -1
  51. package/dist/deploy/canary-deploy.js +0 -145
  52. package/dist/deploy/canary-deploy.js.map +0 -1
  53. package/dist/deploy/landing-queue.d.ts +0 -83
  54. package/dist/deploy/landing-queue.d.ts.map +0 -1
  55. package/dist/deploy/landing-queue.js +0 -172
  56. package/dist/deploy/landing-queue.js.map +0 -1
  57. package/dist/headless/answer-injector.d.ts +0 -27
  58. package/dist/headless/answer-injector.d.ts.map +0 -1
  59. package/dist/headless/answer-injector.js +0 -80
  60. package/dist/headless/answer-injector.js.map +0 -1
  61. package/dist/headless/exit-codes.d.ts +0 -13
  62. package/dist/headless/exit-codes.d.ts.map +0 -1
  63. package/dist/headless/exit-codes.js +0 -29
  64. package/dist/headless/exit-codes.js.map +0 -1
  65. package/dist/headless/idle-detector.d.ts +0 -32
  66. package/dist/headless/idle-detector.d.ts.map +0 -1
  67. package/dist/headless/idle-detector.js +0 -62
  68. package/dist/headless/idle-detector.js.map +0 -1
  69. package/dist/headless/jsonl-stream.d.ts +0 -28
  70. package/dist/headless/jsonl-stream.d.ts.map +0 -1
  71. package/dist/headless/jsonl-stream.js +0 -65
  72. package/dist/headless/jsonl-stream.js.map +0 -1
  73. package/dist/headless/orchestrator.d.ts +0 -63
  74. package/dist/headless/orchestrator.d.ts.map +0 -1
  75. package/dist/headless/orchestrator.js +0 -156
  76. package/dist/headless/orchestrator.js.map +0 -1
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/index.js.map +0 -1
  79. package/dist/tools/ci_status.d.ts +0 -40
  80. package/dist/tools/ci_status.d.ts.map +0 -1
  81. package/dist/tools/ci_status.js +0 -110
  82. package/dist/tools/ci_status.js.map +0 -1
  83. package/dist/types.d.ts +0 -93
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js +0 -17
  86. package/dist/types.js.map +0 -1
  87. package/dist/workflow/deployment-workflow.d.ts +0 -56
  88. package/dist/workflow/deployment-workflow.d.ts.map +0 -1
  89. package/dist/workflow/deployment-workflow.js +0 -95
  90. package/dist/workflow/deployment-workflow.js.map +0 -1
@@ -0,0 +1,99 @@
1
+ # Quick Start - pi-cicd
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pi install npm:pi-cicd
7
+ ```
8
+
9
+ ## Basic Usage
10
+
11
+ ### 1. Run Pipeline
12
+
13
+ ```bash
14
+ # Run default pipeline
15
+ /ci run
16
+
17
+ # Run specific environment
18
+ /ci run production
19
+
20
+ # Run with variables
21
+ /ci run staging --var=VERSION=1.2.3
22
+ ```
23
+
24
+ ### 2. Check Status
25
+
26
+ ```bash
27
+ # Check current run status
28
+ /ci status
29
+
30
+ # Check specific run
31
+ /ci status --run-id=abc123
32
+ ```
33
+
34
+ ### 3. Deploy
35
+
36
+ ```bash
37
+ # Deploy to environment
38
+ /ci deploy production
39
+
40
+ # Deploy with confirmation
41
+ /ci deploy staging --confirm
42
+ ```
43
+
44
+ ### 4. Canary Deployment
45
+
46
+ ```bash
47
+ # Start canary (10% traffic)
48
+ /ci canary deploy --percentage 10
49
+
50
+ # Increase canary
51
+ /ci canary promote --percentage 25
52
+
53
+ # Rollback canary
54
+ /ci canary rollback
55
+ ```
56
+
57
+ ## Examples
58
+
59
+ ### Example: Production Deploy
60
+
61
+ ```
62
+ /ci run production
63
+
64
+ Output:
65
+ ## CI/CD Pipeline: production
66
+
67
+ ### Stage: build
68
+ ✅ npm install (23s)
69
+ ✅ npm run build (45s)
70
+
71
+ ### Stage: test
72
+ ✅ Unit tests (89 tests, 12s)
73
+ ✅ Integration tests (34 tests, 28s)
74
+
75
+ ### Stage: deploy
76
+ 🚀 Deploying to production...
77
+
78
+ ✅ Pipeline complete (2m 34s)
79
+ ```
80
+
81
+ ### Example: Headless CI
82
+
83
+ ```bash
84
+ # In GitHub Actions
85
+ - name: Run Pi CI
86
+ run: |
87
+ pi ci run --headless --exit-code
88
+ ```
89
+
90
+ ## Exit Codes
91
+
92
+ | Code | Meaning |
93
+ |------|---------|
94
+ | 0 | Success |
95
+ | 1 | General error |
96
+ | 2 | Timeout |
97
+ | 10 | Blocked (verification failed) |
98
+ | 11 | Cancelled |
99
+ | 12 | Deployment failed |
@@ -3,6 +3,20 @@
3
3
  *
4
4
  * Registers the /ci status command and CI lifecycle hooks.
5
5
  */
6
+
7
+ import type { CIEvent, CIOptions, ExitCode } from "./src/types.ts";
8
+ import { EXIT_CODES } from "./src/types.ts";
9
+ import {
10
+ ciStatusHandler,
11
+ createRunTracker,
12
+ clearRuns,
13
+ registerRun,
14
+ type CIRunRecord,
15
+ } from "./src/tools/ci_status.ts";
16
+ import { CIPipeline, type PipelineResult } from "./src/ci/pipeline.ts";
17
+ import { generateReport } from "./src/ci/report.ts";
18
+
19
+ // Re-export for consumers
6
20
  export { EXIT_CODES } from "./src/types.ts";
7
21
  export { resolveExitCode } from "./src/headless/exit-codes.ts";
8
22
  export { loadAnswers, matchAnswer, parseAnswers } from "./src/headless/answer-injector.ts";
@@ -16,15 +30,23 @@ export { generateReport } from "./src/ci/report.ts";
16
30
  export { ciStatusHandler, registerRun, clearRuns, createRunTracker } from "./src/tools/ci_status.ts";
17
31
  export { loadCiConfig, DEFAULT_CONFIG } from "./src/config.ts";
18
32
  export type { PiCiConfig } from "./src/config.ts";
33
+
19
34
  /**
20
35
  * Extension API type (minimal — avoids hard dep on pi-coding-agent types).
21
36
  */
22
37
  interface ExtensionAPI {
23
- registerCommand?: (name: string, handler: (args: unknown) => string | Promise<string>) => void;
24
- on?: (event: string, handler: (...args: unknown[]) => void) => void;
38
+ registerCommand?: (name: string, handler: (args: unknown) => string | Promise<string>) => void;
39
+ on?: (event: string, handler: (...args: unknown[]) => void) => void;
25
40
  }
41
+
26
42
  /**
27
43
  * Default export — Pi extension registration.
28
44
  */
29
- export default function piCiExtension(pi: ExtensionAPI): void;
30
- //# sourceMappingURL=index.d.ts.map
45
+ export default function piCiExtension(pi: ExtensionAPI): void {
46
+ // Register /ci status command
47
+ if (pi.registerCommand) {
48
+ pi.registerCommand("ci", (args: unknown) => {
49
+ return ciStatusHandler(args);
50
+ });
51
+ }
52
+ }
package/install.mjs ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install script for <PKG-NAME>
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+
10
+ const home = os.homedir();
11
+ const agentDir = path.join(home, ".pi", "agent");
12
+ const pkgName = path.basename(process.cwd());
13
+ const configPath = path.join(agentDir, `${pkgName}.json`);
14
+
15
+ // Create config directory
16
+ fs.mkdirSync(agentDir, { recursive: true });
17
+
18
+ // Check if config already exists
19
+ if (!fs.existsSync(configPath)) {
20
+ const defaultConfig = {
21
+ enabled: true
22
+ };
23
+ fs.writeFileSync(configPath, `${JSON.stringify(defaultConfig, null, 2)}\n`, "utf-8");
24
+ console.log(`Created default ${pkgName} config: ${configPath}`);
25
+ } else {
26
+ console.log(`${pkgName} config already exists: ${configPath}`);
27
+ }
28
+
29
+ console.log(`\nInstall the published package in Pi with:`);
30
+ console.log(` pi install npm:@baphuongna/${pkgName}`);
31
+ console.log(`\nFor local development from a cloned repo:`);
32
+ console.log(` pi install .`);
33
+ console.log(`\nVerify installation:`);
34
+ console.log(` pi list\n`);
package/package.json CHANGED
@@ -1,25 +1,32 @@
1
1
  {
2
2
  "name": "pi-cicd",
3
- "version": "0.3.0",
4
- "description": "Pi extension for headless CI mode with structured exit codes, answer injection, and pipeline automation",
3
+ "version": "1.0.1",
4
+ "description": "Extension for Pi coding agent",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
6
+ "main": "./index.ts",
7
+ "module": "./index.ts",
8
+ "types": "./index.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
11
+ "import": "./index.ts",
12
+ "types": "./index.ts"
13
13
  }
14
14
  },
15
- "scripts": {
16
- "build": "tsc --noEmitOnError false",
17
- "typecheck": "tsc --noEmit",
18
- "test": "npx tsx --test --test-concurrency=1 --test-timeout=30000 test/unit/*.test.ts"
19
- },
20
15
  "files": [
21
- "dist"
16
+ "*.ts",
17
+ "*.mjs",
18
+ "src/**/*.ts",
19
+ "skills/**/*",
20
+ "README.md",
21
+ "docs/",
22
+ "CHANGELOG.md",
23
+ "LICENSE"
22
24
  ],
25
+ "pi": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ },
23
30
  "keywords": [
24
31
  "pi",
25
32
  "coding-agent",
@@ -27,15 +34,8 @@
27
34
  ],
28
35
  "author": "BaphuongNA",
29
36
  "license": "MIT",
30
- "pi": {
31
- "extensions": [
32
- "./index.ts"
33
- ]
34
- },
37
+ "repository": "https://github.com/baphuongna/pi-cicd",
35
38
  "peerDependencies": {
36
39
  "typescript": "^5.0.0"
37
- },
38
- "devDependencies": {
39
- "tsx": "^4.19.0"
40
40
  }
41
41
  }
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: intelligent-deploy
3
+ description: Canary deployment with monitoring and landing queue management
4
+ triggers:
5
+ - deploy
6
+ - canary
7
+ - rollout
8
+ - landing queue
9
+ - production
10
+ requirements:
11
+ tools: [bash]
12
+ context: [deployment target]
13
+ ---
14
+
15
+ # Intelligent Deploy Skill
16
+
17
+ ## Objective
18
+ Execute safe deployments with canary monitoring, automatic rollback, and landing queue management.
19
+
20
+ ## When to Use
21
+ - When user asks to "deploy" or "ship to production"
22
+ - When running CI/CD pipelines
23
+ - When managing multiple deployments
24
+ - When requiring gradual rollouts with monitoring
25
+
26
+ ## Workflow
27
+
28
+ ### Step 1: Canary Deployment
29
+ ```typescript
30
+ import { CanaryDeploy } from '../../src/deploy/canary-deploy';
31
+
32
+ const canary = new CanaryDeploy({
33
+ initialPercentage: 10,
34
+ incrementPercentage: 10,
35
+ stepInterval: 60000, // 1 minute
36
+ totalDuration: 300000, // 5 minutes
37
+ metrics: {
38
+ successRate: { min: 95 },
39
+ latency: { max: 500 },
40
+ errorRate: { max: 5 },
41
+ },
42
+ });
43
+
44
+ const result = await canary.deploy({
45
+ name: 'production',
46
+ url: 'https://api.example.com',
47
+ healthy: true,
48
+ });
49
+
50
+ console.log(canary.formatReport(result));
51
+ ```
52
+
53
+ ### Step 2: Landing Queue
54
+ ```typescript
55
+ import { LandingQueue } from '../../src/deploy/landing-queue';
56
+
57
+ const queue = new LandingQueue();
58
+
59
+ // Add to queue
60
+ queue.enqueue('v1.2.0', 'production', 'New feature release');
61
+ queue.enqueue('v1.2.1', 'production', 'Bug fix');
62
+
63
+ // Process queue
64
+ while (true) {
65
+ const next = await queue.startNext();
66
+ if (!next) break;
67
+
68
+ // Deploy
69
+ const success = await deploy(next);
70
+
71
+ // Mark complete
72
+ queue.complete(next.id, success);
73
+
74
+ if (!success) break; // Stop on failure
75
+ }
76
+ ```
77
+
78
+ ### Step 3: Monitor and Rollback
79
+ ```typescript
80
+ // Automatic rollback on issues
81
+ if (result.rolledBack) {
82
+ console.log('Deployment rolled back due to issues');
83
+ await canary.rollback(target);
84
+ }
85
+ ```
86
+
87
+ ## Canary Configuration
88
+
89
+ | Parameter | Default | Description |
90
+ |-----------|---------|-------------|
91
+ | initialPercentage | 10% | Starting traffic |
92
+ | incrementPercentage | 10% | Traffic increase per step |
93
+ | stepInterval | 1 min | Time between increments |
94
+ | totalDuration | 5 min | Total deployment time |
95
+
96
+ ### Metrics Thresholds
97
+
98
+ | Metric | Threshold | Action |
99
+ |--------|-----------|--------|
100
+ | Success Rate | > 95% | Continue |
101
+ | Latency | < 500ms | Continue |
102
+ | Error Rate | < 5% | Continue |
103
+
104
+ If metrics drop below thresholds:
105
+ - Warning at threshold
106
+ - Auto-rollback at critical level (90% success, 10% errors)
107
+
108
+ ## Landing Queue Commands
109
+
110
+ ### Enqueue Deployment
111
+ ```typescript
112
+ queue.enqueue('v1.2.0', 'production', 'Feature release');
113
+ ```
114
+
115
+ ### Process Queue
116
+ ```typescript
117
+ while (const next = queue.startNext()) {
118
+ await deploy(next);
119
+ queue.complete(next.id, success);
120
+ }
121
+ ```
122
+
123
+ ### View Status
124
+ ```typescript
125
+ console.log(queue.formatReport());
126
+ ```
127
+
128
+ ## Examples
129
+
130
+ ### Deploy with Canary
131
+ ```
132
+ User: Deploy v1.2.0 to production with canary
133
+ Agent:
134
+ const result = await canary.deploy({ name: 'production', url: '...', healthy: true });
135
+ if (result.success) {
136
+ console.log('Deployment successful!');
137
+ } else {
138
+ console.log('Rolled back - check issues');
139
+ }
140
+ ```
141
+
142
+ ### Process Landing Queue
143
+ ```
144
+ User: Queue these deployments: v1.2.0, v1.2.1, v1.2.2
145
+ Agent:
146
+ queue.enqueue('v1.2.0', 'production');
147
+ queue.enqueue('v1.2.1', 'production');
148
+ queue.enqueue('v1.2.2', 'staging');
149
+
150
+ const result = await queue.processAll();
151
+ ```
152
+
153
+ ### View Queue Status
154
+ ```
155
+ User: Show me the current deployment queue
156
+ Agent:
157
+ console.log(queue.formatReport());
158
+ ```
159
+
160
+ ## Output Format
161
+
162
+ ### Canary Report
163
+ ```markdown
164
+ ## Canary Deployment Report
165
+ **Status:** ✅ SUCCESS
166
+ **Final Traffic:** 100%
167
+ **Rolled Back:** No
168
+
169
+ ### Metrics History
170
+ | Time | Success | Latency | Error Rate |
171
+ |------|---------|---------|------------|
172
+ | 0min | 98.5% | 120ms | 1.2% |
173
+ | 1min | 99.1% | 115ms | 0.8% |
174
+ | 2min | 99.3% | 110ms | 0.5% |
175
+ ```
176
+
177
+ ### Landing Queue Report
178
+ ```markdown
179
+ ## Landing Queue Report
180
+ **Total:** 5 | **Pending:** 2 | **Deploying:** 1 | **Deployed:** 2 | **Failed:** 0
181
+
182
+ ### Currently Deploying
183
+ **v1.2.1** -> production
184
+ Status: deploying
185
+
186
+ ### Queue
187
+ | # | Version | Environment | Message |
188
+ |---|--------|------------|---------|
189
+ | 1 | v1.2.2 | production | Hotfix |
190
+ | 2 | v1.2.3 | staging | New feature |
191
+ ```
192
+
193
+ ## Integration
194
+
195
+ ### With pi-pipeline
196
+ ```typescript
197
+ // Run as part of ship workflow
198
+ const gates = new QualityGates();
199
+ const gatesResult = await gates.run('pre-push');
200
+
201
+ if (gatesResult.passed) {
202
+ const result = await canary.deploy(target);
203
+ if (!result.success) {
204
+ throw new Error('Deployment failed');
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### With pi-audit
210
+ ```typescript
211
+ // Security scan before deploy
212
+ const shield = new AgentShield();
213
+ const scan = shield.scan(deploymentCode);
214
+
215
+ if (!scan.passed) {
216
+ console.log('Security issues must be fixed');
217
+ process.exit(1);
218
+ }
219
+ ```
220
+
221
+ ### With pi-recollect
222
+ ```typescript
223
+ // Remember deployment
224
+ await memory.remember(
225
+ 'deployment',
226
+ `Deployed ${version} to ${environment}`,
227
+ 'observation'
228
+ );
229
+ ```
@@ -0,0 +1,130 @@
1
+ /**
2
+ * pi-ci — CI pipeline wrapper.
3
+ *
4
+ * Provides single, plan, review, and supervised execution modes.
5
+ */
6
+
7
+ import type { CIEvent, CIOptions, CIPipelineMode, ExitCode, TestSummary } from "../types.ts";
8
+ import { EXIT_CODES } from "../types.ts";
9
+ import { HeadlessOrchestrator, type OrchestratorHooks } from "../headless/orchestrator.ts";
10
+ import { loadAnswers } from "../headless/answer-injector.ts";
11
+ import { parseTestResults } from "./test-runner.ts";
12
+ import { generateReport } from "./report.ts";
13
+
14
+ export interface PipelineResult {
15
+ exitCode: ExitCode;
16
+ events: CIEvent[];
17
+ report: string;
18
+ testSummary?: TestSummary;
19
+ }
20
+
21
+ export class CIPipeline {
22
+ private readonly options: CIOptions;
23
+ private readonly hooks: OrchestratorHooks;
24
+
25
+ constructor(options: CIOptions, hooks: OrchestratorHooks) {
26
+ this.options = options;
27
+ this.hooks = hooks;
28
+ }
29
+
30
+ /**
31
+ * Execute the pipeline in the configured mode.
32
+ */
33
+ async execute(): Promise<PipelineResult> {
34
+ switch (this.options.mode) {
35
+ case "single":
36
+ return this.executeSingle();
37
+ case "plan":
38
+ return this.executePlan();
39
+ case "review":
40
+ return this.executeReview();
41
+ case "supervised":
42
+ return this.executeSupervised();
43
+ default:
44
+ throw new Error(`Unknown CI pipeline mode: ${this.options.mode as string}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Single task mode — run one prompt to completion.
50
+ */
51
+ private async executeSingle(): Promise<PipelineResult> {
52
+ const answers = await this.loadAnswers();
53
+ const orchestrator = new HeadlessOrchestrator(answers, this.options, this.hooks);
54
+ const result = await orchestrator.run(this.options.prompt, "single");
55
+
56
+ return {
57
+ exitCode: result.exitCode,
58
+ events: result.events,
59
+ report: generateReport(result.events),
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Plan mode — execute steps from a plan file.
65
+ *
66
+ * Each step becomes a sequential orchestrator invocation. If any step fails,
67
+ * the pipeline stops.
68
+ */
69
+ private async executePlan(): Promise<PipelineResult> {
70
+ const allEvents: CIEvent[] = [];
71
+ let finalExitCode: ExitCode = EXIT_CODES.SUCCESS;
72
+ let totalDuration = 0;
73
+
74
+ // Plan steps are provided via the executeStep hook — the orchestrator
75
+ // iterates over steps internally.
76
+ const answers = await this.loadAnswers();
77
+ const orchestrator = new HeadlessOrchestrator(answers, this.options, this.hooks);
78
+ const result = await orchestrator.run(this.options.prompt, "plan");
79
+
80
+ allEvents.push(...result.events);
81
+ finalExitCode = result.exitCode;
82
+ totalDuration = result.durationMs;
83
+
84
+ return {
85
+ exitCode: finalExitCode,
86
+ events: allEvents,
87
+ report: generateReport(allEvents),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * PR review mode — review a PR by number.
93
+ */
94
+ private async executeReview(): Promise<PipelineResult> {
95
+ const answers = await this.loadAnswers();
96
+ const orchestrator = new HeadlessOrchestrator(answers, this.options, this.hooks);
97
+ const prompt = this.options.prNumber
98
+ ? `Review PR #${this.options.prNumber}`
99
+ : this.options.prompt;
100
+ const result = await orchestrator.run(prompt, "single");
101
+
102
+ return {
103
+ exitCode: result.exitCode,
104
+ events: result.events,
105
+ report: generateReport(result.events),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Supervised mode — stdin/stdout forwarding for an external orchestrator.
111
+ */
112
+ private async executeSupervised(): Promise<PipelineResult> {
113
+ const answers = await this.loadAnswers();
114
+ const orchestrator = new HeadlessOrchestrator(answers, this.options, this.hooks);
115
+ const result = await orchestrator.run(this.options.prompt, "single");
116
+
117
+ return {
118
+ exitCode: result.exitCode,
119
+ events: result.events,
120
+ report: generateReport(result.events),
121
+ };
122
+ }
123
+
124
+ private async loadAnswers() {
125
+ if (this.options.answersFile) {
126
+ return loadAnswers(this.options.answersFile);
127
+ }
128
+ return [];
129
+ }
130
+ }
@@ -0,0 +1,74 @@
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
+ }