pi-cicd 1.0.0 → 1.0.2

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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Landing Queue - Process deployments in order
3
+ * Based on gstack /landing-report pattern
4
+ */
5
+
6
+ export type DeployStatus = 'pending' | 'deploying' | 'deployed' | 'failed' | 'cancelled';
7
+ export type DeployEnvironment = 'staging' | 'production';
8
+
9
+ export interface QueuedDeploy {
10
+ id: string;
11
+ version: string;
12
+ environment: DeployEnvironment;
13
+ status: DeployStatus;
14
+ createdAt: number;
15
+ deployedAt?: number;
16
+ message?: string;
17
+ logs: string[];
18
+ }
19
+
20
+ export interface LandingQueueStats {
21
+ total: number;
22
+ pending: number;
23
+ deploying: number;
24
+ deployed: number;
25
+ failed: number;
26
+ }
27
+
28
+ /**
29
+ * Landing Queue Manager
30
+ */
31
+ export class LandingQueue {
32
+ private queue: QueuedDeploy[] = [];
33
+ private current: QueuedDeploy | null = null;
34
+
35
+ /**
36
+ * Add deployment to queue
37
+ */
38
+ enqueue(version: string, environment: DeployEnvironment, message?: string): QueuedDeploy {
39
+ const deploy: QueuedDeploy = {
40
+ id: `deploy-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
41
+ version,
42
+ environment,
43
+ status: 'pending',
44
+ createdAt: Date.now(),
45
+ message,
46
+ logs: [],
47
+ };
48
+
49
+ this.queue.push(deploy);
50
+ this.log(deploy.id, `Added to queue: ${version} -> ${environment}`);
51
+
52
+ return deploy;
53
+ }
54
+
55
+ /**
56
+ * Get next pending deployment
57
+ */
58
+ peek(): QueuedDeploy | undefined {
59
+ return this.queue.find((d) => d.status === 'pending');
60
+ }
61
+
62
+ /**
63
+ * Start deploying next item
64
+ */
65
+ async startNext(): Promise<QueuedDeploy | null> {
66
+ const next = this.peek();
67
+ if (!next) return null;
68
+
69
+ // Mark as deploying
70
+ next.status = 'deploying';
71
+ this.current = next;
72
+ this.log(next.id, 'Starting deployment');
73
+
74
+ return next;
75
+ }
76
+
77
+ /**
78
+ * Mark deployment as complete
79
+ */
80
+ complete(id: string, success: boolean): void {
81
+ const deploy = this.queue.find((d) => d.id === id);
82
+ if (!deploy) return;
83
+
84
+ deploy.status = success ? 'deployed' : 'failed';
85
+ deploy.deployedAt = Date.now();
86
+ this.log(id, success ? 'Deployment successful' : 'Deployment failed');
87
+
88
+ if (this.current?.id === id) {
89
+ this.current = null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Cancel a deployment
95
+ */
96
+ cancel(id: string): void {
97
+ const deploy = this.queue.find((d) => d.id === id);
98
+ if (!deploy) return;
99
+
100
+ if (deploy.status === 'deploying') {
101
+ this.log(id, 'Cannot cancel - deployment in progress');
102
+ return;
103
+ }
104
+
105
+ deploy.status = 'cancelled';
106
+ this.log(id, 'Cancelled');
107
+ }
108
+
109
+ /**
110
+ * Get deployment by ID
111
+ */
112
+ get(id: string): QueuedDeploy | undefined {
113
+ return this.queue.find((d) => d.id === id);
114
+ }
115
+
116
+ /**
117
+ * Get queue status
118
+ */
119
+ getStats(): LandingQueueStats {
120
+ return {
121
+ total: this.queue.length,
122
+ pending: this.queue.filter((d) => d.status === 'pending').length,
123
+ deploying: this.queue.filter((d) => d.status === 'deploying').length,
124
+ deployed: this.queue.filter((d) => d.status === 'deployed').length,
125
+ failed: this.queue.filter((d) => d.status === 'failed').length,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Get all deployments
131
+ */
132
+ getAll(): QueuedDeploy[] {
133
+ return [...this.queue].sort((a, b) => b.createdAt - a.createdAt);
134
+ }
135
+
136
+ /**
137
+ * Get pending deployments
138
+ */
139
+ getPending(): QueuedDeploy[] {
140
+ return this.queue
141
+ .filter((d) => d.status === 'pending')
142
+ .sort((a, b) => a.createdAt - b.createdAt);
143
+ }
144
+
145
+ /**
146
+ * Get current deploying
147
+ */
148
+ getCurrent(): QueuedDeploy | null {
149
+ return this.current;
150
+ }
151
+
152
+ /**
153
+ * Add log entry
154
+ */
155
+ private log(id: string, message: string): void {
156
+ const deploy = this.queue.find((d) => d.id === id);
157
+ if (deploy) {
158
+ deploy.logs.push(`[${new Date().toISOString()}] ${message}`);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Clear completed deployments
164
+ */
165
+ clearCompleted(): void {
166
+ this.queue = this.queue.filter(
167
+ (d) => d.status === 'pending' || d.status === 'deploying'
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Format queue as markdown report
173
+ */
174
+ formatReport(): string {
175
+ const stats = this.getStats();
176
+ const lines: string[] = [];
177
+
178
+ lines.push('## Landing Queue Report\n');
179
+ lines.push(`**Total:** ${stats.total} | **Pending:** ${stats.pending} | **Deploying:** ${stats.deploying} | **Deployed:** ${stats.deployed} | **Failed:** ${stats.failed}\n`);
180
+
181
+ const current = this.getCurrent();
182
+ if (current) {
183
+ lines.push('### Currently Deploying\n');
184
+ lines.push(`**${current.version}** -> ${current.environment}`);
185
+ lines.push(`Status: ${current.status}`);
186
+ lines.push('');
187
+ }
188
+
189
+ const pending = this.getPending();
190
+ if (pending.length > 0) {
191
+ lines.push('### Queue\n');
192
+ lines.push('| # | Version | Environment | Message |');
193
+ lines.push('|---|--------|------------|---------|');
194
+
195
+ pending.forEach((d, i) => {
196
+ lines.push(`| ${i + 1} | ${d.version} | ${d.environment} | ${d.message || '-'} |`);
197
+ });
198
+ lines.push('');
199
+ }
200
+
201
+ const recent = this.queue
202
+ .filter((d) => d.status === 'deployed' || d.status === 'failed')
203
+ .slice(0, 5);
204
+
205
+ if (recent.length > 0) {
206
+ lines.push('### Recent\n');
207
+ lines.push('| Version | Environment | Status | Time |');
208
+ lines.push('|---------|------------|--------|------|');
209
+
210
+ for (const d of recent) {
211
+ const icon = d.status === 'deployed' ? '✅' : '❌';
212
+ const time = d.deployedAt
213
+ ? new Date(d.deployedAt).toLocaleTimeString()
214
+ : '-';
215
+ lines.push(`| ${d.version} | ${d.environment} | ${icon} ${d.status} | ${time} |`);
216
+ }
217
+ lines.push('');
218
+ }
219
+
220
+ return lines.join('\n');
221
+ }
222
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { AnswerEntry, AnswerFile } from "../types.ts";
9
+ import * as fs from "fs";
9
10
 
10
11
  /**
11
12
  * Read and validate an answers JSON file.
@@ -17,7 +18,7 @@ import type { AnswerEntry, AnswerFile } from "../types.ts";
17
18
  export async function loadAnswers(filePath: string): Promise<AnswerEntry[]> {
18
19
  let text: string;
19
20
  try {
20
- text = await Bun.file(filePath).text();
21
+ text = fs.readFileSync(filePath, "utf-8");
21
22
  } catch {
22
23
  return [];
23
24
  }
@@ -8,7 +8,8 @@
8
8
  import type { ExitCode, CIEvent, CIOptions } from "../types.ts";
9
9
  import { EXIT_CODES } from "../types.ts";
10
10
  import { resolveExitCode } from "./exit-codes.ts";
11
- import { matchAnswer, type AnswerEntry } from "./answer-injector.ts";
11
+ import { matchAnswer } from "./answer-injector.ts";
12
+ import type { AnswerEntry } from "../types.ts";
12
13
  import { IdleDetector } from "./idle-detector.ts";
13
14
  import { CIEventCollector, writeCIEvent } from "./jsonl-stream.ts";
14
15
  import type { Writable } from "node:stream";
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * pi-ci — Pi extension entry point.
3
+ *
4
+ * Registers the /ci status command and CI lifecycle hooks.
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
20
+ export { EXIT_CODES } from "./src/types.ts";
21
+ export { resolveExitCode } from "./src/headless/exit-codes.ts";
22
+ export { loadAnswers, matchAnswer, parseAnswers } from "./src/headless/answer-injector.ts";
23
+ export { IdleDetector } from "./src/headless/idle-detector.ts";
24
+ export { CIEventCollector, writeCIEvent } from "./src/headless/jsonl-stream.ts";
25
+ export { HeadlessOrchestrator } from "./src/headless/orchestrator.ts";
26
+ export { CIPipeline } from "./src/ci/pipeline.ts";
27
+ export { createPR, detectBaseBranch } from "./src/ci/pr-creator.ts";
28
+ export { parseTestResults } from "./src/ci/test-runner.ts";
29
+ export { generateReport } from "./src/ci/report.ts";
30
+ export { ciStatusHandler, registerRun, clearRuns, createRunTracker } from "./src/tools/ci_status.ts";
31
+ export { loadCiConfig, DEFAULT_CONFIG } from "./src/config.ts";
32
+ export type { PiCiConfig } from "./src/config.ts";
33
+
34
+ /**
35
+ * Extension API type (minimal — avoids hard dep on pi-coding-agent types).
36
+ */
37
+ interface ExtensionAPI {
38
+ registerCommand?: (name: string, handler: (args: unknown) => string | Promise<string>) => void;
39
+ on?: (event: string, handler: (...args: unknown[]) => void) => void;
40
+ }
41
+
42
+ /**
43
+ * Default export — Pi extension registration.
44
+ */
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
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Deployment Workflow - Pattern from pi-crew workflow-config.ts
3
+ *
4
+ * Declarative deployment workflow definition.
5
+ */
6
+
7
+ export interface DeploymentStep {
8
+ id: string;
9
+ name: string;
10
+ action: "validate" | "build" | "test" | "deploy" | "verify" | "rollback";
11
+ environment: "staging" | "production" | "preview";
12
+ dependsOn?: string[];
13
+ parallelGroup?: string;
14
+ timeout?: number; // ms
15
+ retry?: {
16
+ maxAttempts: number;
17
+ backoffMs: number;
18
+ };
19
+ conditions?: {
20
+ onlyIf?: string;
21
+ skipIf?: string;
22
+ };
23
+ }
24
+
25
+ export interface DeploymentWorkflow {
26
+ name: string;
27
+ description: string;
28
+ version: string;
29
+ steps: DeploymentStep[];
30
+ maxConcurrency?: number;
31
+ rollbackOnFailure?: boolean;
32
+ }
33
+
34
+ export interface DeploymentState {
35
+ workflowId: string;
36
+ runId: string;
37
+ status: "pending" | "running" | "completed" | "failed" | "rolled_back";
38
+ currentStep?: string;
39
+ completedSteps: string[];
40
+ failedSteps: string[];
41
+ startedAt: string;
42
+ finishedAt?: string;
43
+ }
44
+
45
+ /**
46
+ * Create a standard deployment workflow
47
+ */
48
+ export function createDeploymentWorkflow(
49
+ options: {
50
+ name: string;
51
+ environments?: string[];
52
+ includeStaging?: boolean;
53
+ }
54
+ ): DeploymentWorkflow {
55
+ const envs = options.environments ?? ["staging", "production"];
56
+
57
+ const steps: DeploymentStep[] = [
58
+ { id: "validate", name: "Validate", action: "validate", environment: "staging" as const },
59
+ { id: "build", name: "Build", action: "build", environment: "staging" as const, dependsOn: ["validate"] },
60
+ { id: "test", name: "Test", action: "test", environment: "staging" as const, dependsOn: ["build"] },
61
+ ];
62
+
63
+ if (options.includeStaging !== false) {
64
+ steps.push({
65
+ id: "deploy-staging",
66
+ name: "Deploy to Staging",
67
+ action: "deploy",
68
+ environment: "staging",
69
+ dependsOn: ["test"],
70
+ retry: { maxAttempts: 3, backoffMs: 5000 },
71
+ });
72
+ steps.push({
73
+ id: "verify-staging",
74
+ name: "Verify Staging",
75
+ action: "verify",
76
+ environment: "staging",
77
+ dependsOn: ["deploy-staging"],
78
+ });
79
+ }
80
+
81
+ if (envs.includes("production")) {
82
+ steps.push({
83
+ id: "deploy-production",
84
+ name: "Deploy to Production",
85
+ action: "deploy",
86
+ environment: "production",
87
+ dependsOn: options.includeStaging !== false ? ["verify-staging"] : ["test"],
88
+ retry: { maxAttempts: 3, backoffMs: 10000 },
89
+ conditions: {
90
+ onlyIf: "BRANCH=main",
91
+ },
92
+ });
93
+ }
94
+
95
+ return {
96
+ name: options.name,
97
+ description: `Deployment workflow for ${options.name}`,
98
+ version: "1.0.0",
99
+ steps,
100
+ maxConcurrency: 1,
101
+ rollbackOnFailure: true,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Execute a deployment workflow
107
+ */
108
+ export async function* executeDeploymentWorkflow(
109
+ workflow: DeploymentWorkflow,
110
+ executor: (step: DeploymentStep) => Promise<{ success: boolean; error?: string }>
111
+ ): AsyncGenerator<DeploymentState> {
112
+ const state: DeploymentState = {
113
+ workflowId: workflow.name,
114
+ runId: `deploy-${Date.now()}`,
115
+ status: "pending",
116
+ completedSteps: [],
117
+ failedSteps: [],
118
+ startedAt: new Date().toISOString(),
119
+ };
120
+
121
+ yield state;
122
+
123
+ const stepMap = new Map(workflow.steps.map(s => [s.id, s]));
124
+ const completed = new Set<string>();
125
+
126
+ for (const step of workflow.steps) {
127
+ // Check dependencies
128
+ if (step.dependsOn?.some(dep => !completed.has(dep))) {
129
+ continue; // Dependencies not met
130
+ }
131
+
132
+ state.status = "running";
133
+ state.currentStep = step.id;
134
+ yield state;
135
+
136
+ const result = await executor(step);
137
+
138
+ if (result.success) {
139
+ completed.add(step.id);
140
+ state.completedSteps.push(step.id);
141
+ } else {
142
+ state.failedSteps.push(step.id);
143
+ state.status = "failed";
144
+ state.finishedAt = new Date().toISOString();
145
+ yield state;
146
+ return;
147
+ }
148
+ }
149
+
150
+ state.status = "completed";
151
+ state.finishedAt = new Date().toISOString();
152
+ yield state;
153
+ }
package/AGENTS.md DELETED
@@ -1,25 +0,0 @@
1
- # pi-ci Development Notes
2
-
3
- Pi extension for headless CI mode.
4
-
5
- ## Rules
6
-
7
- - Keep `index.ts` minimal; re-export from `src/` modules.
8
- - Avoid `any`; use `unknown` plus validation.
9
- - After code changes, run `npm test` from `pi-ci/` unless explicitly told not to.
10
-
11
- ## Important commands
12
-
13
- ```bash
14
- npm test
15
- npm run typecheck
16
- ```
17
-
18
- ## Important paths
19
-
20
- - `index.ts` — extension entry point
21
- - `src/headless/` — core headless mode (exit codes, answers, idle, JSONL, orchestrator)
22
- - `src/ci/` — CI pipeline, PR creation, test runner, reports
23
- - `src/tools/` — /ci status command
24
- - `src/config.ts` — configuration loading
25
- - `test/unit/` — unit tests
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "strict": true,
7
- "noImplicitAny": true,
8
- "exactOptionalPropertyTypes": false,
9
- "skipLibCheck": true,
10
- "allowImportingTsExtensions": true,
11
- "noEmit": true,
12
- "types": ["node"]
13
- },
14
- "include": [
15
- "*.ts",
16
- "src/**/*.ts",
17
- "test/**/*.ts"
18
- ]
19
- }