neuro-cli 4.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1443 @@
1
+ // ============================================================
2
+ // NeuroCLI - CI/CD Pipeline Integration
3
+ // Detect, run, and manage CI/CD pipelines across platforms
4
+ // ============================================================
5
+ import { execSync } from 'child_process';
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, } from 'fs';
7
+ import { join, basename } from 'path';
8
+ // ---- Config file paths ----
9
+ const GITHUB_WORKFLOWS_DIR = '.github/workflows';
10
+ const GITLAB_CI_FILE = '.gitlab-ci.yml';
11
+ const JENKINS_FILE = 'Jenkinsfile';
12
+ const CIRCLECI_DIR = '.circleci';
13
+ const CIRCLECI_CONFIG_FILE = '.circleci/config.yml';
14
+ function execCommand(command, options) {
15
+ const timeout = options?.timeout ?? 60000;
16
+ const cwd = options?.cwd ?? process.cwd();
17
+ try {
18
+ const stdout = execSync(command, {
19
+ encoding: 'utf-8',
20
+ cwd,
21
+ timeout,
22
+ maxBuffer: 50 * 1024 * 1024,
23
+ env: { ...process.env, GH_PROMPT_DISABLED: '1', NO_COLOR: '1' },
24
+ });
25
+ return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
26
+ }
27
+ catch (error) {
28
+ return {
29
+ stdout: error.stdout?.toString().trim() ?? '',
30
+ stderr: error.stderr?.toString().trim() ?? '',
31
+ exitCode: error.status ?? 1,
32
+ };
33
+ }
34
+ }
35
+ function parseJSON(text) {
36
+ try {
37
+ return JSON.parse(text);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function isGhAvailable() {
44
+ return execCommand('gh --version').exitCode === 0;
45
+ }
46
+ // ---- CICDIntegration Class ----
47
+ export class CICDIntegration {
48
+ config = null;
49
+ projectRoot;
50
+ ghAvailable = null;
51
+ constructor(projectRoot) {
52
+ this.projectRoot = projectRoot ?? process.cwd();
53
+ }
54
+ ensureGh() {
55
+ if (this.ghAvailable === null) {
56
+ this.ghAvailable = isGhAvailable();
57
+ }
58
+ return this.ghAvailable;
59
+ }
60
+ gh(args, timeout) {
61
+ return execCommand(`gh ${args}`, { cwd: this.projectRoot, timeout });
62
+ }
63
+ git(args) {
64
+ return execCommand(`git ${args}`, { cwd: this.projectRoot });
65
+ }
66
+ resolvePath(relativePath) {
67
+ return join(this.projectRoot, relativePath);
68
+ }
69
+ getRepoSlug() {
70
+ const result = this.git('remote get-url origin');
71
+ if (result.exitCode !== 0 || !result.stdout)
72
+ return null;
73
+ const match = result.stdout.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
74
+ return match ? match[1] : null;
75
+ }
76
+ getDefaultBranch() {
77
+ const result = this.git('rev-parse --abbrev-ref HEAD');
78
+ return result.exitCode === 0 ? result.stdout : 'main';
79
+ }
80
+ // ---- Detection ----
81
+ /**
82
+ * Detect which CI/CD system(s) are configured in the project
83
+ */
84
+ detectPipeline(projectRoot) {
85
+ const root = projectRoot ?? this.projectRoot;
86
+ const detected = [];
87
+ let primarySystem = 'unknown';
88
+ let primaryConfigFile = '';
89
+ // Check GitHub Actions
90
+ const workflowsDir = join(root, GITHUB_WORKFLOWS_DIR);
91
+ if (existsSync(workflowsDir)) {
92
+ try {
93
+ const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
94
+ if (files.length > 0) {
95
+ detected.push('github-actions');
96
+ if (primarySystem === 'unknown') {
97
+ primarySystem = 'github-actions';
98
+ primaryConfigFile = join(GITHUB_WORKFLOWS_DIR, files[0]);
99
+ }
100
+ }
101
+ }
102
+ catch {
103
+ // Ignore permission errors
104
+ }
105
+ }
106
+ // Check GitLab CI
107
+ if (existsSync(join(root, GITLAB_CI_FILE))) {
108
+ detected.push('gitlab-ci');
109
+ if (primarySystem === 'unknown') {
110
+ primarySystem = 'gitlab-ci';
111
+ primaryConfigFile = GITLAB_CI_FILE;
112
+ }
113
+ }
114
+ // Check Jenkins
115
+ if (existsSync(join(root, JENKINS_FILE))) {
116
+ detected.push('jenkins');
117
+ if (primarySystem === 'unknown') {
118
+ primarySystem = 'jenkins';
119
+ primaryConfigFile = JENKINS_FILE;
120
+ }
121
+ }
122
+ // Check CircleCI
123
+ if (existsSync(join(root, CIRCLECI_CONFIG_FILE))) {
124
+ detected.push('circleci');
125
+ if (primarySystem === 'unknown') {
126
+ primarySystem = 'circleci';
127
+ primaryConfigFile = CIRCLECI_CONFIG_FILE;
128
+ }
129
+ }
130
+ // Cache config
131
+ if (primarySystem !== 'unknown') {
132
+ this.config = {
133
+ system: primarySystem,
134
+ projectRoot: root,
135
+ configFile: primaryConfigFile,
136
+ projectSlug: this.getRepoSlug() ?? undefined,
137
+ defaultBranch: this.getDefaultBranch(),
138
+ remoteUrl: this.git('remote get-url origin').exitCode === 0
139
+ ? this.git('remote get-url origin').stdout
140
+ : undefined,
141
+ };
142
+ }
143
+ return {
144
+ system: primarySystem,
145
+ configFile: primaryConfigFile,
146
+ configExists: primarySystem !== 'unknown',
147
+ multipleSystems: detected,
148
+ };
149
+ }
150
+ /**
151
+ * Get or initialize the CI/CD config
152
+ */
153
+ getConfig() {
154
+ if (!this.config) {
155
+ const detection = this.detectPipeline();
156
+ if (!this.config) {
157
+ this.config = {
158
+ system: detection.system,
159
+ projectRoot: this.projectRoot,
160
+ configFile: detection.configFile,
161
+ projectSlug: this.getRepoSlug() ?? undefined,
162
+ defaultBranch: this.getDefaultBranch(),
163
+ };
164
+ }
165
+ }
166
+ return this.config;
167
+ }
168
+ // ---- Pipeline Execution ----
169
+ /**
170
+ * Trigger a pipeline run
171
+ */
172
+ runPipeline(pipeline, options) {
173
+ const config = this.getConfig();
174
+ const opts = options ?? {};
175
+ switch (config.system) {
176
+ case 'github-actions':
177
+ return this.runGitHubActions(pipeline, opts);
178
+ case 'gitlab-ci':
179
+ return this.runGitLabCI(pipeline, opts);
180
+ case 'jenkins':
181
+ return this.runJenkins(pipeline, opts);
182
+ case 'circleci':
183
+ return this.runCircleCI(pipeline, opts);
184
+ default:
185
+ return { success: false, error: 'No CI/CD system detected. Run detectPipeline() first.' };
186
+ }
187
+ }
188
+ /**
189
+ * Get pipeline status
190
+ */
191
+ getPipelineStatus(runId) {
192
+ const config = this.getConfig();
193
+ switch (config.system) {
194
+ case 'github-actions':
195
+ return this.getGitHubActionsStatus(runId);
196
+ case 'gitlab-ci':
197
+ return this.getGitLabCIStatus(runId);
198
+ case 'jenkins':
199
+ return this.getJenkinsStatus(runId);
200
+ case 'circleci':
201
+ return this.getCircleCIStatus(runId);
202
+ default:
203
+ return { success: false, error: 'No CI/CD system detected.' };
204
+ }
205
+ }
206
+ /**
207
+ * Get pipeline logs
208
+ */
209
+ getPipelineLogs(runId) {
210
+ const config = this.getConfig();
211
+ switch (config.system) {
212
+ case 'github-actions':
213
+ return this.getGitHubActionsLogs(runId);
214
+ case 'gitlab-ci':
215
+ return this.getGitLabCILogs(runId);
216
+ case 'jenkins':
217
+ return this.getJenkinsLogs(runId);
218
+ case 'circleci':
219
+ return this.getCircleCILogs(runId);
220
+ default:
221
+ return { success: false, error: 'No CI/CD system detected.' };
222
+ }
223
+ }
224
+ /**
225
+ * List available pipelines
226
+ */
227
+ listPipelines() {
228
+ const config = this.getConfig();
229
+ switch (config.system) {
230
+ case 'github-actions':
231
+ return this.listGitHubActionsPipelines();
232
+ case 'gitlab-ci':
233
+ return this.listGitLabCIPipelines();
234
+ case 'jenkins':
235
+ return this.listJenkinsPipelines();
236
+ case 'circleci':
237
+ return this.listCircleCIPipelines();
238
+ default:
239
+ return { success: false, error: 'No CI/CD system detected.' };
240
+ }
241
+ }
242
+ /**
243
+ * Generate a CI/CD config file
244
+ */
245
+ createPipelineConfig(type, options) {
246
+ switch (type) {
247
+ case 'github-actions':
248
+ return this.createGitHubActionsConfig(options);
249
+ case 'gitlab-ci':
250
+ return this.createGitLabCIConfig(options);
251
+ case 'jenkins':
252
+ return this.createJenkinsConfig(options);
253
+ case 'circleci':
254
+ return this.createCircleCIConfig(options);
255
+ default:
256
+ return { success: false, error: `Unsupported CI/CD system: ${type}` };
257
+ }
258
+ }
259
+ /**
260
+ * Validate current CI/CD config
261
+ */
262
+ validateConfig() {
263
+ const config = this.getConfig();
264
+ switch (config.system) {
265
+ case 'github-actions':
266
+ return this.validateGitHubActionsConfig();
267
+ case 'gitlab-ci':
268
+ return this.validateGitLabCIConfig();
269
+ case 'jenkins':
270
+ return this.validateJenkinsConfig();
271
+ case 'circleci':
272
+ return this.validateCircleCIConfig();
273
+ default:
274
+ return { success: false, error: 'No CI/CD system detected.' };
275
+ }
276
+ }
277
+ /**
278
+ * Get recent pipeline runs
279
+ */
280
+ getPipelineHistory(limit) {
281
+ const config = this.getConfig();
282
+ const maxResults = limit ?? 10;
283
+ switch (config.system) {
284
+ case 'github-actions':
285
+ return this.getGitHubActionsHistory(maxResults);
286
+ case 'gitlab-ci':
287
+ return this.getGitLabCIHistory(maxResults);
288
+ case 'jenkins':
289
+ return this.getJenkinsHistory(maxResults);
290
+ case 'circleci':
291
+ return this.getCircleCIHistory(maxResults);
292
+ default:
293
+ return { success: false, error: 'No CI/CD system detected.' };
294
+ }
295
+ }
296
+ /**
297
+ * Cancel a running pipeline
298
+ */
299
+ cancelPipeline(runId) {
300
+ const config = this.getConfig();
301
+ switch (config.system) {
302
+ case 'github-actions':
303
+ return this.cancelGitHubActionsRun(runId);
304
+ case 'gitlab-ci':
305
+ return this.cancelGitLabCIRun(runId);
306
+ case 'jenkins':
307
+ return this.cancelJenkinsRun(runId);
308
+ case 'circleci':
309
+ return this.cancelCircleCIRun(runId);
310
+ default:
311
+ return { success: false, error: 'No CI/CD system detected.' };
312
+ }
313
+ }
314
+ /**
315
+ * Re-run a pipeline
316
+ */
317
+ rerunPipeline(runId) {
318
+ const config = this.getConfig();
319
+ switch (config.system) {
320
+ case 'github-actions':
321
+ return this.rerunGitHubActions(runId);
322
+ case 'gitlab-ci':
323
+ return this.rerunGitLabCI(runId);
324
+ case 'jenkins':
325
+ return this.rerunJenkins(runId);
326
+ case 'circleci':
327
+ return this.rerunCircleCI(runId);
328
+ default:
329
+ return { success: false, error: 'No CI/CD system detected.' };
330
+ }
331
+ }
332
+ /**
333
+ * Watch a pipeline in real-time
334
+ */
335
+ watchPipeline(runId, callback, options) {
336
+ const config = this.getConfig();
337
+ const interval = options?.interval ?? 10000;
338
+ const timeoutMs = options?.timeout ?? 600000; // 10 minutes default
339
+ let timer = null;
340
+ let timeoutTimer = null;
341
+ let lastStatus = 'unknown';
342
+ const startTime = Date.now();
343
+ const stop = () => {
344
+ if (timer) {
345
+ clearInterval(timer);
346
+ timer = null;
347
+ }
348
+ if (timeoutTimer) {
349
+ clearTimeout(timeoutTimer);
350
+ timer = null;
351
+ }
352
+ };
353
+ const poll = () => {
354
+ const statusResult = this.getPipelineStatus(runId);
355
+ if (!statusResult.success || !statusResult.run) {
356
+ callback({
357
+ runId,
358
+ status: 'unknown',
359
+ timestamp: Date.now(),
360
+ message: statusResult.error ?? 'Failed to get status',
361
+ });
362
+ return;
363
+ }
364
+ const run = statusResult.run;
365
+ const currentStatus = run.status;
366
+ if (currentStatus !== lastStatus) {
367
+ lastStatus = currentStatus;
368
+ callback({
369
+ runId,
370
+ status: currentStatus,
371
+ timestamp: Date.now(),
372
+ message: `Status changed to: ${currentStatus}`,
373
+ });
374
+ }
375
+ // Check if pipeline is complete
376
+ if (['success', 'failure', 'cancelled', 'skipped'].includes(currentStatus)) {
377
+ callback({
378
+ runId,
379
+ status: currentStatus,
380
+ timestamp: Date.now(),
381
+ message: `Pipeline completed with status: ${currentStatus}`,
382
+ });
383
+ stop();
384
+ }
385
+ };
386
+ // Start polling
387
+ poll(); // Initial check
388
+ timer = setInterval(poll, interval);
389
+ // Set timeout
390
+ timeoutTimer = setTimeout(() => {
391
+ callback({
392
+ runId,
393
+ status: lastStatus,
394
+ timestamp: Date.now(),
395
+ message: 'Watch timed out',
396
+ });
397
+ stop();
398
+ }, timeoutMs);
399
+ return { success: true, stop };
400
+ }
401
+ // ================================================================
402
+ // GitHub Actions Implementation
403
+ // ================================================================
404
+ runGitHubActions(workflow, options) {
405
+ if (!this.ensureGh()) {
406
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
407
+ }
408
+ const slug = this.getRepoSlug();
409
+ const repoFlag = slug ? `--repo ${slug}` : '';
410
+ const ref = options?.ref ?? this.getDefaultBranch();
411
+ if (workflow) {
412
+ const refFlag = ref ? `--ref "${ref}"` : '';
413
+ let paramFlags = '';
414
+ if (options?.parameters) {
415
+ for (const [key, value] of Object.entries(options.parameters)) {
416
+ paramFlags += ` -f ${key}="${value}"`;
417
+ }
418
+ }
419
+ const result = this.gh(`workflow run "${workflow}" ${refFlag}${paramFlags} ${repoFlag}`.trim());
420
+ if (result.exitCode !== 0) {
421
+ return { success: false, error: result.stderr || 'Failed to trigger workflow' };
422
+ }
423
+ // gh workflow run doesn't return a run ID directly; fetch the latest
424
+ return this.getLatestGitHubActionsRun(workflow);
425
+ }
426
+ // No specific workflow - try to trigger all on-push workflows by pushing a dummy commit or listing
427
+ return { success: false, error: 'Specify a workflow name or ID to trigger GitHub Actions' };
428
+ }
429
+ getLatestGitHubActionsRun(workflowName) {
430
+ const slug = this.getRepoSlug();
431
+ const repoFlag = slug ? `--repo ${slug}` : '';
432
+ const workflowFilter = workflowName ? `--workflow="${workflowName}"` : '';
433
+ const result = this.gh(`run list ${workflowFilter} --limit 1 --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
434
+ if (result.exitCode !== 0) {
435
+ return { success: false, error: result.stderr || 'Failed to fetch latest run' };
436
+ }
437
+ const runs = parseJSON(result.stdout);
438
+ if (!runs || runs.length === 0) {
439
+ return { success: false, error: 'No workflow runs found' };
440
+ }
441
+ return {
442
+ success: true,
443
+ run: this.mapGitHubActionsRun(runs[0]),
444
+ };
445
+ }
446
+ getGitHubActionsStatus(runId) {
447
+ if (!this.ensureGh()) {
448
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
449
+ }
450
+ const slug = this.getRepoSlug();
451
+ const repoFlag = slug ? `--repo ${slug}` : '';
452
+ if (runId) {
453
+ const result = this.gh(`run view ${runId} --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
454
+ if (result.exitCode !== 0) {
455
+ return { success: false, error: result.stderr || 'Failed to get run status' };
456
+ }
457
+ const run = parseJSON(result.stdout);
458
+ if (!run)
459
+ return { success: false, error: 'Failed to parse run status' };
460
+ return { success: true, run: this.mapGitHubActionsRun(run) };
461
+ }
462
+ // List recent runs
463
+ const result = this.gh(`run list --limit 10 --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
464
+ if (result.exitCode !== 0) {
465
+ return { success: false, error: result.stderr || 'Failed to list runs' };
466
+ }
467
+ const runs = parseJSON(result.stdout);
468
+ if (!runs)
469
+ return { success: false, error: 'Failed to parse runs' };
470
+ return { success: true, runs: runs.map(r => this.mapGitHubActionsRun(r)) };
471
+ }
472
+ getGitHubActionsLogs(runId) {
473
+ if (!this.ensureGh()) {
474
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
475
+ }
476
+ // Get jobs for this run
477
+ const slug = this.getRepoSlug();
478
+ const repoFlag = slug ? `--repo ${slug}` : '';
479
+ const runResult = this.gh(`run view ${runId} --json jobs ${repoFlag}`.trim());
480
+ if (runResult.exitCode !== 0) {
481
+ return { success: false, error: runResult.stderr || 'Failed to get run details' };
482
+ }
483
+ const runData = parseJSON(runResult.stdout);
484
+ if (!runData)
485
+ return { success: false, error: 'Failed to parse run data' };
486
+ const jobs = [];
487
+ for (const job of runData.jobs ?? []) {
488
+ // Get log for each job
489
+ const logResult = this.gh(`run view ${runId} --job=${job.databaseId ?? job.id} --log ${repoFlag}`.trim());
490
+ jobs.push({
491
+ name: job.name ?? '',
492
+ status: this.mapGitHubStatus(job.status, job.conclusion),
493
+ log: logResult.exitCode === 0 ? logResult.stdout : `Failed to retrieve logs: ${logResult.stderr}`,
494
+ startedAt: job.startedAt,
495
+ completedAt: job.completedAt,
496
+ });
497
+ }
498
+ return {
499
+ success: true,
500
+ logs: {
501
+ runId,
502
+ system: 'github-actions',
503
+ jobs,
504
+ },
505
+ };
506
+ }
507
+ listGitHubActionsPipelines() {
508
+ const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
509
+ const pipelines = [];
510
+ if (!existsSync(workflowsDir)) {
511
+ return { success: false, error: 'No .github/workflows directory found' };
512
+ }
513
+ try {
514
+ const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
515
+ for (const file of files) {
516
+ const filePath = join(workflowsDir, file);
517
+ const content = readFileSync(filePath, 'utf-8');
518
+ const triggers = this.extractYAMLTriggers(content);
519
+ const name = this.extractYAMLWorkflowName(content) ?? basename(file, '.yml').replace(/\.yaml$/, '');
520
+ pipelines.push({
521
+ id: file,
522
+ name,
523
+ system: 'github-actions',
524
+ configFile: join(GITHUB_WORKFLOWS_DIR, file),
525
+ triggers,
526
+ });
527
+ }
528
+ }
529
+ catch (error) {
530
+ return { success: false, error: `Failed to read workflows: ${error instanceof Error ? error.message : String(error)}` };
531
+ }
532
+ return { success: true, pipelines };
533
+ }
534
+ getGitHubActionsHistory(limit) {
535
+ if (!this.ensureGh()) {
536
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
537
+ }
538
+ const slug = this.getRepoSlug();
539
+ const repoFlag = slug ? `--repo ${slug}` : '';
540
+ const result = this.gh(`run list --limit ${limit} --json databaseId,name,status,conclusion,headBranch,headSha,htmlUrl,createdAt,updatedAt,event ${repoFlag}`.trim());
541
+ if (result.exitCode !== 0) {
542
+ return { success: false, error: result.stderr || 'Failed to get run history' };
543
+ }
544
+ const runs = parseJSON(result.stdout);
545
+ if (!runs)
546
+ return { success: false, error: 'Failed to parse run history' };
547
+ return { success: true, runs: runs.map(r => this.mapGitHubActionsRun(r)) };
548
+ }
549
+ cancelGitHubActionsRun(runId) {
550
+ if (!this.ensureGh()) {
551
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
552
+ }
553
+ const slug = this.getRepoSlug();
554
+ const repoFlag = slug ? `--repo ${slug}` : '';
555
+ const result = this.gh(`run cancel ${runId} ${repoFlag}`.trim());
556
+ if (result.exitCode !== 0) {
557
+ return { success: false, error: result.stderr || 'Failed to cancel run' };
558
+ }
559
+ return { success: true };
560
+ }
561
+ rerunGitHubActions(runId) {
562
+ if (!this.ensureGh()) {
563
+ return { success: false, error: 'GitHub CLI (`gh`) is not installed or not authenticated' };
564
+ }
565
+ const slug = this.getRepoSlug();
566
+ const repoFlag = slug ? `--repo ${slug}` : '';
567
+ const result = this.gh(`run rerun ${runId} ${repoFlag}`.trim());
568
+ if (result.exitCode !== 0) {
569
+ return { success: false, error: result.stderr || 'Failed to rerun' };
570
+ }
571
+ // Get the new run (most recent)
572
+ const latestResult = this.gh(`run list --limit 1 --json databaseId ${repoFlag}`.trim());
573
+ if (latestResult.exitCode === 0) {
574
+ const runs = parseJSON(latestResult.stdout);
575
+ if (runs && runs.length > 0) {
576
+ return { success: true, newRunId: String(runs[0].databaseId) };
577
+ }
578
+ }
579
+ return { success: true };
580
+ }
581
+ // ================================================================
582
+ // GitLab CI Implementation
583
+ // ================================================================
584
+ runGitLabCI(pipeline, options) {
585
+ // GitLab CI pipelines are triggered via API; require `glab` CLI
586
+ const ref = options?.ref ?? this.getDefaultBranch();
587
+ const result = execCommand(`glab ci run --ref "${ref}"${pipeline ? ` --pipeline "${pipeline}"` : ''}`, { cwd: this.projectRoot });
588
+ if (result.exitCode !== 0) {
589
+ return { success: false, error: result.stderr || 'Failed to trigger GitLab CI pipeline. Ensure `glab` CLI is installed.' };
590
+ }
591
+ return { success: true };
592
+ }
593
+ getGitLabCIStatus(runId) {
594
+ const result = execCommand(`glab ci status${runId ? ` --pipeline-id ${runId}` : ''}`, { cwd: this.projectRoot });
595
+ if (result.exitCode !== 0) {
596
+ return { success: false, error: result.stderr || 'Failed to get GitLab CI status' };
597
+ }
598
+ return {
599
+ success: true,
600
+ run: {
601
+ id: runId ?? 'latest',
602
+ system: 'gitlab-ci',
603
+ name: 'GitLab CI Pipeline',
604
+ status: 'unknown',
605
+ branch: this.getDefaultBranch(),
606
+ commit: '',
607
+ url: '',
608
+ createdAt: new Date().toISOString(),
609
+ updatedAt: new Date().toISOString(),
610
+ triggeredBy: '',
611
+ },
612
+ };
613
+ }
614
+ getGitLabCILogs(runId) {
615
+ const result = execCommand(`glab ci trace --pipeline-id ${runId}`, { cwd: this.projectRoot });
616
+ if (result.exitCode !== 0) {
617
+ return { success: false, error: result.stderr || 'Failed to get GitLab CI logs' };
618
+ }
619
+ return {
620
+ success: true,
621
+ logs: {
622
+ runId,
623
+ system: 'gitlab-ci',
624
+ jobs: [{
625
+ name: 'pipeline',
626
+ status: 'unknown',
627
+ log: result.stdout,
628
+ }],
629
+ },
630
+ };
631
+ }
632
+ listGitLabCIPipelines() {
633
+ const configFile = this.resolvePath(GITLAB_CI_FILE);
634
+ if (!existsSync(configFile)) {
635
+ return { success: false, error: 'No .gitlab-ci.yml found' };
636
+ }
637
+ const content = readFileSync(configFile, 'utf-8');
638
+ const stages = this.extractYAMLStages(content);
639
+ const pipelines = stages.map(stage => ({
640
+ id: stage,
641
+ name: stage,
642
+ system: 'gitlab-ci',
643
+ configFile: GITLAB_CI_FILE,
644
+ triggers: ['push', 'merge_request'],
645
+ }));
646
+ return { success: true, pipelines };
647
+ }
648
+ getGitLabCIHistory(limit) {
649
+ const result = execCommand(`glab ci list --per-page ${limit}`, { cwd: this.projectRoot });
650
+ if (result.exitCode !== 0) {
651
+ return { success: false, error: result.stderr || 'Failed to get GitLab CI history' };
652
+ }
653
+ return { success: true, runs: [] };
654
+ }
655
+ cancelGitLabCIRun(runId) {
656
+ const result = execCommand(`glab ci cancel --pipeline-id ${runId}`, { cwd: this.projectRoot });
657
+ if (result.exitCode !== 0) {
658
+ return { success: false, error: result.stderr || 'Failed to cancel GitLab CI run' };
659
+ }
660
+ return { success: true };
661
+ }
662
+ rerunGitLabCI(runId) {
663
+ const result = execCommand(`glab ci retry --pipeline-id ${runId}`, { cwd: this.projectRoot });
664
+ if (result.exitCode !== 0) {
665
+ return { success: false, error: result.stderr || 'Failed to rerun GitLab CI pipeline' };
666
+ }
667
+ return { success: true };
668
+ }
669
+ // ================================================================
670
+ // Jenkins Implementation
671
+ // ================================================================
672
+ runJenkins(pipeline, options) {
673
+ // Jenkins builds are triggered via API or `jenkins-cli`
674
+ const jobName = pipeline ?? 'main';
675
+ const parameters = options?.parameters ?? {};
676
+ let paramFlags = '';
677
+ for (const [key, value] of Object.entries(parameters)) {
678
+ paramFlags += ` -p ${key}="${value}"`;
679
+ }
680
+ // Try using jenkins-cli.jar if available
681
+ const result = execCommand(`java -jar jenkins-cli.jar build ${jobName}${paramFlags}`, { cwd: this.projectRoot });
682
+ if (result.exitCode !== 0) {
683
+ return {
684
+ success: false,
685
+ error: 'Failed to trigger Jenkins build. Ensure `jenkins-cli.jar` is available and Jenkins is accessible.',
686
+ };
687
+ }
688
+ return { success: true };
689
+ }
690
+ getJenkinsStatus(runId) {
691
+ // Jenkins status via CLI
692
+ return {
693
+ success: true,
694
+ run: {
695
+ id: runId ?? 'latest',
696
+ system: 'jenkins',
697
+ name: 'Jenkins Build',
698
+ status: 'unknown',
699
+ branch: this.getDefaultBranch(),
700
+ commit: '',
701
+ url: '',
702
+ createdAt: new Date().toISOString(),
703
+ updatedAt: new Date().toISOString(),
704
+ triggeredBy: '',
705
+ },
706
+ };
707
+ }
708
+ getJenkinsLogs(runId) {
709
+ const result = execCommand(`java -jar jenkins-cli.jar console ${runId}`, { cwd: this.projectRoot });
710
+ if (result.exitCode !== 0) {
711
+ return { success: false, error: result.stderr || 'Failed to get Jenkins console output' };
712
+ }
713
+ return {
714
+ success: true,
715
+ logs: {
716
+ runId,
717
+ system: 'jenkins',
718
+ jobs: [{
719
+ name: 'build',
720
+ status: 'unknown',
721
+ log: result.stdout,
722
+ }],
723
+ },
724
+ };
725
+ }
726
+ listJenkinsPipelines() {
727
+ const jenkinsfile = this.resolvePath(JENKINS_FILE);
728
+ if (!existsSync(jenkinsfile)) {
729
+ return { success: false, error: 'No Jenkinsfile found' };
730
+ }
731
+ const content = readFileSync(jenkinsfile, 'utf-8');
732
+ const stages = this.extractJenkinsStages(content);
733
+ return {
734
+ success: true,
735
+ pipelines: [{
736
+ id: 'Jenkinsfile',
737
+ name: 'Jenkins Pipeline',
738
+ system: 'jenkins',
739
+ configFile: JENKINS_FILE,
740
+ triggers: stages,
741
+ }],
742
+ };
743
+ }
744
+ getJenkinsHistory(limit) {
745
+ // Jenkins history via CLI - limited support
746
+ return { success: true, runs: [] };
747
+ }
748
+ cancelJenkinsRun(runId) {
749
+ const result = execCommand(`java -jar jenkins-cli.jar stop ${runId}`, { cwd: this.projectRoot });
750
+ if (result.exitCode !== 0) {
751
+ return { success: false, error: 'Failed to cancel Jenkins build' };
752
+ }
753
+ return { success: true };
754
+ }
755
+ rerunJenkins(runId) {
756
+ const result = execCommand(`java -jar jenkins-cli.jar build ${runId}`, { cwd: this.projectRoot });
757
+ if (result.exitCode !== 0) {
758
+ return { success: false, error: 'Failed to rerun Jenkins build' };
759
+ }
760
+ return { success: true };
761
+ }
762
+ // ================================================================
763
+ // CircleCI Implementation
764
+ // ================================================================
765
+ runCircleCI(pipeline, options) {
766
+ const slug = this.getRepoSlug() ?? '';
767
+ const ref = options?.ref ?? this.getDefaultBranch();
768
+ const slugFlag = slug ? `--repo ${slug}` : '';
769
+ const result = execCommand(`circleci trigger-pipeline --branch "${ref}" ${slugFlag}`, { cwd: this.projectRoot });
770
+ if (result.exitCode !== 0) {
771
+ return {
772
+ success: false,
773
+ error: 'Failed to trigger CircleCI pipeline. Ensure `circleci` CLI is installed and authenticated.',
774
+ };
775
+ }
776
+ return { success: true };
777
+ }
778
+ getCircleCIStatus(runId) {
779
+ const slug = this.getRepoSlug() ?? '';
780
+ const slugFlag = slug ? `--repo ${slug}` : '';
781
+ const result = execCommand(`circleci pipeline status ${slugFlag}${runId ? ` --pipeline-id ${runId}` : ''}`, { cwd: this.projectRoot });
782
+ if (result.exitCode !== 0) {
783
+ return { success: false, error: result.stderr || 'Failed to get CircleCI status' };
784
+ }
785
+ return {
786
+ success: true,
787
+ run: {
788
+ id: runId ?? 'latest',
789
+ system: 'circleci',
790
+ name: 'CircleCI Pipeline',
791
+ status: 'unknown',
792
+ branch: this.getDefaultBranch(),
793
+ commit: '',
794
+ url: '',
795
+ createdAt: new Date().toISOString(),
796
+ updatedAt: new Date().toISOString(),
797
+ triggeredBy: '',
798
+ },
799
+ };
800
+ }
801
+ getCircleCILogs(runId) {
802
+ const result = execCommand(`circleci pipeline show --pipeline-id ${runId}`, { cwd: this.projectRoot });
803
+ if (result.exitCode !== 0) {
804
+ return { success: false, error: result.stderr || 'Failed to get CircleCI logs' };
805
+ }
806
+ return {
807
+ success: true,
808
+ logs: {
809
+ runId,
810
+ system: 'circleci',
811
+ jobs: [{
812
+ name: 'pipeline',
813
+ status: 'unknown',
814
+ log: result.stdout,
815
+ }],
816
+ },
817
+ };
818
+ }
819
+ listCircleCIPipelines() {
820
+ const configPath = this.resolvePath(CIRCLECI_CONFIG_FILE);
821
+ if (!existsSync(configPath)) {
822
+ return { success: false, error: 'No .circleci/config.yml found' };
823
+ }
824
+ const content = readFileSync(configPath, 'utf-8');
825
+ const jobs = this.extractCircleCIJobs(content);
826
+ return {
827
+ success: true,
828
+ pipelines: jobs.map(job => ({
829
+ id: job,
830
+ name: job,
831
+ system: 'circleci',
832
+ configFile: CIRCLECI_CONFIG_FILE,
833
+ triggers: ['push'],
834
+ })),
835
+ };
836
+ }
837
+ getCircleCIHistory(limit) {
838
+ const slug = this.getRepoSlug() ?? '';
839
+ const slugFlag = slug ? `--repo ${slug}` : '';
840
+ const result = execCommand(`circleci pipeline list ${slugFlag} --limit ${limit}`, { cwd: this.projectRoot });
841
+ if (result.exitCode !== 0) {
842
+ return { success: false, error: result.stderr || 'Failed to get CircleCI history' };
843
+ }
844
+ return { success: true, runs: [] };
845
+ }
846
+ cancelCircleCIRun(runId) {
847
+ const result = execCommand(`circleci pipeline cancel --pipeline-id ${runId}`, { cwd: this.projectRoot });
848
+ if (result.exitCode !== 0) {
849
+ return { success: false, error: result.stderr || 'Failed to cancel CircleCI pipeline' };
850
+ }
851
+ return { success: true };
852
+ }
853
+ rerunCircleCI(runId) {
854
+ const result = execCommand(`circleci pipeline rerun --pipeline-id ${runId}`, { cwd: this.projectRoot });
855
+ if (result.exitCode !== 0) {
856
+ return { success: false, error: result.stderr || 'Failed to rerun CircleCI pipeline' };
857
+ }
858
+ return { success: true };
859
+ }
860
+ // ================================================================
861
+ // Config Generation
862
+ // ================================================================
863
+ createGitHubActionsConfig(options) {
864
+ const lang = options.language.toLowerCase();
865
+ const nodeVersion = options.nodeVersion ?? '20';
866
+ const branches = options.branches ?? ['main'];
867
+ const buildCmd = options.buildCommand ?? '';
868
+ const testCmd = options.testCommand ?? '';
869
+ const lintCmd = options.lintCommand ?? '';
870
+ const deployCmd = options.deployCommand ?? '';
871
+ let steps = '';
872
+ // Setup steps based on language
873
+ if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
874
+ steps += `
875
+ - name: Setup Node.js
876
+ uses: actions/setup-node@v4
877
+ with:
878
+ node-version: '${nodeVersion}'
879
+ cache: 'npm'
880
+ - run: npm ci`;
881
+ }
882
+ else if (lang === 'python') {
883
+ const pyVersion = options.pythonVersion ?? '3.11';
884
+ steps += `
885
+ - name: Setup Python
886
+ uses: actions/setup-python@v5
887
+ with:
888
+ python-version: '${pyVersion}'
889
+ cache: 'pip'
890
+ - run: pip install -r requirements.txt`;
891
+ }
892
+ else if (lang === 'go') {
893
+ steps += `
894
+ - name: Setup Go
895
+ uses: actions/setup-go@v5
896
+ with:
897
+ go-version: '1.21'`;
898
+ }
899
+ else {
900
+ steps += `
901
+ - name: Install dependencies
902
+ run: |${buildCmd ? `\n ${buildCmd.split(' ')[0]} install` : ' echo "Add your install step here"'}`;
903
+ }
904
+ if (lintCmd) {
905
+ steps += `
906
+ - name: Lint
907
+ run: ${lintCmd}`;
908
+ }
909
+ if (buildCmd) {
910
+ steps += `
911
+ - name: Build
912
+ run: ${buildCmd}`;
913
+ }
914
+ if (testCmd) {
915
+ steps += `
916
+ - name: Test
917
+ run: ${testCmd}`;
918
+ }
919
+ if (deployCmd) {
920
+ steps += `
921
+ - name: Deploy
922
+ run: ${deployCmd}
923
+ if: github.ref == 'refs/heads/${branches[0]}'`;
924
+ }
925
+ // Add custom stages
926
+ if (options.stages) {
927
+ for (const stage of options.stages) {
928
+ steps += `
929
+ - name: ${stage.name}
930
+ run: ${stage.command}${stage.condition ? `\n if: ${stage.condition}` : ''}`;
931
+ }
932
+ }
933
+ const yaml = `name: CI
934
+
935
+ on:
936
+ push:
937
+ branches: [${branches.map(b => `'${b}'`).join(', ')}]
938
+ pull_request:
939
+ branches: [${branches.map(b => `'${b}'`).join(', ')}]
940
+
941
+ jobs:
942
+ build:
943
+ runs-on: ubuntu-latest
944
+ ${options.dockerImage ? ` container: ${options.dockerImage}` : ''}
945
+ steps:
946
+ - name: Checkout
947
+ uses: actions/checkout@v4${steps}
948
+ `;
949
+ // Write file
950
+ const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
951
+ if (!existsSync(workflowsDir)) {
952
+ mkdirSync(workflowsDir, { recursive: true });
953
+ }
954
+ const filePath = join(workflowsDir, 'ci.yml');
955
+ writeFileSync(filePath, yaml, 'utf-8');
956
+ return { success: true, path: join(GITHUB_WORKFLOWS_DIR, 'ci.yml') };
957
+ }
958
+ createGitLabCIConfig(options) {
959
+ const lang = options.language.toLowerCase();
960
+ const nodeVersion = options.nodeVersion ?? '20';
961
+ const buildCmd = options.buildCommand ?? '';
962
+ const testCmd = options.testCommand ?? '';
963
+ const lintCmd = options.lintCommand ?? '';
964
+ const branches = options.branches ?? ['main'];
965
+ let imageLine = '';
966
+ if (options.dockerImage) {
967
+ imageLine = `image: ${options.dockerImage}\n\n`;
968
+ }
969
+ else if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
970
+ imageLine = `image: node:${nodeVersion}\n\n`;
971
+ }
972
+ else if (lang === 'python') {
973
+ imageLine = `image: python:${options.pythonVersion ?? '3.11'}\n\n`;
974
+ }
975
+ let stages = 'stages:\n - lint\n - build\n - test\n';
976
+ let jobDefs = '';
977
+ if (lintCmd) {
978
+ stages += ' - deploy\n';
979
+ jobDefs += `
980
+ lint:
981
+ stage: lint
982
+ script:
983
+ - ${lintCmd}
984
+ rules:
985
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
986
+ - if: $CI_COMMIT_BRANCH == "${branches[0]}"
987
+
988
+ `;
989
+ }
990
+ if (buildCmd) {
991
+ jobDefs += `build:
992
+ stage: build
993
+ script:
994
+ - ${buildCmd}
995
+ rules:
996
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
997
+ - if: $CI_COMMIT_BRANCH == "${branches[0]}"
998
+
999
+ `;
1000
+ }
1001
+ if (testCmd) {
1002
+ jobDefs += `test:
1003
+ stage: test
1004
+ script:
1005
+ - ${testCmd}
1006
+ rules:
1007
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
1008
+ - if: $CI_COMMIT_BRANCH == "${branches[0]}"
1009
+
1010
+ `;
1011
+ }
1012
+ if (options.deployCommand) {
1013
+ jobDefs += `deploy:
1014
+ stage: deploy
1015
+ script:
1016
+ - ${options.deployCommand}
1017
+ rules:
1018
+ - if: $CI_COMMIT_BRANCH == "${branches[0]}"
1019
+ environment: production
1020
+
1021
+ `;
1022
+ }
1023
+ const yaml = `${imageLine}${stages}\n${jobDefs}`;
1024
+ const filePath = this.resolvePath(GITLAB_CI_FILE);
1025
+ writeFileSync(filePath, yaml, 'utf-8');
1026
+ return { success: true, path: GITLAB_CI_FILE };
1027
+ }
1028
+ createJenkinsConfig(options) {
1029
+ const buildCmd = options.buildCommand ?? 'echo "Build step"';
1030
+ const testCmd = options.testCommand ?? 'echo "Test step"';
1031
+ const lintCmd = options.lintCommand ?? '';
1032
+ let stages = '';
1033
+ if (lintCmd) {
1034
+ stages += `
1035
+ stage('Lint') {
1036
+ steps {
1037
+ sh '${lintCmd}'
1038
+ }
1039
+ }
1040
+ `;
1041
+ }
1042
+ stages += `
1043
+ stage('Build') {
1044
+ steps {
1045
+ sh '${buildCmd}'
1046
+ }
1047
+ }
1048
+ stage('Test') {
1049
+ steps {
1050
+ sh '${testCmd}'
1051
+ }
1052
+ }
1053
+ `;
1054
+ if (options.deployCommand) {
1055
+ stages += `
1056
+ stage('Deploy') {
1057
+ when {
1058
+ branch '${options.branches?.[0] ?? 'main'}'
1059
+ }
1060
+ steps {
1061
+ sh '${options.deployCommand}'
1062
+ }
1063
+ }
1064
+ `;
1065
+ }
1066
+ if (options.stages) {
1067
+ for (const stage of options.stages) {
1068
+ stages += `
1069
+ stage('${stage.name}') {
1070
+ ${stage.condition ? ` when {\n expression { ${stage.condition} }\n }\n` : ''} steps {
1071
+ sh '${stage.command}'
1072
+ }
1073
+ }
1074
+ `;
1075
+ }
1076
+ }
1077
+ const jenkinsfile = `pipeline {
1078
+ agent any
1079
+
1080
+ stages {${stages}
1081
+ }
1082
+
1083
+ post {
1084
+ always {
1085
+ cleanWs()
1086
+ }
1087
+ success {
1088
+ echo 'Pipeline succeeded!'
1089
+ }
1090
+ failure {
1091
+ echo 'Pipeline failed!'
1092
+ }
1093
+ }
1094
+ }
1095
+ `;
1096
+ const filePath = this.resolvePath(JENKINS_FILE);
1097
+ writeFileSync(filePath, jenkinsfile, 'utf-8');
1098
+ return { success: true, path: JENKINS_FILE };
1099
+ }
1100
+ createCircleCIConfig(options) {
1101
+ const lang = options.language.toLowerCase();
1102
+ const nodeVersion = options.nodeVersion ?? '20';
1103
+ const buildCmd = options.buildCommand ?? '';
1104
+ const testCmd = options.testCommand ?? '';
1105
+ const lintCmd = options.lintCommand ?? '';
1106
+ let orbs = '';
1107
+ let setupSteps = '';
1108
+ let dockerImage = options.dockerImage ?? 'cimg/base:stable';
1109
+ if (lang === 'typescript' || lang === 'javascript' || lang === 'node') {
1110
+ dockerImage = `cimg/node:${nodeVersion}`;
1111
+ setupSteps = ` - run:\n name: Install dependencies\n command: npm ci\n`;
1112
+ }
1113
+ else if (lang === 'python') {
1114
+ dockerImage = `cimg/python:${options.pythonVersion ?? '3.11'}`;
1115
+ setupSteps = ` - run:\n name: Install dependencies\n command: pip install -r requirements.txt\n`;
1116
+ }
1117
+ let jobSteps = setupSteps;
1118
+ if (lintCmd) {
1119
+ jobSteps += ` - run:\n name: Lint\n command: ${lintCmd}\n`;
1120
+ }
1121
+ if (buildCmd) {
1122
+ jobSteps += ` - run:\n name: Build\n command: ${buildCmd}\n`;
1123
+ }
1124
+ if (testCmd) {
1125
+ jobSteps += ` - run:\n name: Test\n command: ${testCmd}\n`;
1126
+ }
1127
+ if (options.deployCommand) {
1128
+ jobSteps += ` - run:\n name: Deploy\n command: ${options.deployCommand}\n`;
1129
+ }
1130
+ if (options.stages) {
1131
+ for (const stage of options.stages) {
1132
+ jobSteps += ` - run:\n name: ${stage.name}\n command: ${stage.command}\n`;
1133
+ }
1134
+ }
1135
+ const yaml = `version: 2.1
1136
+
1137
+ ${orbs}jobs:
1138
+ build-and-test:
1139
+ docker:
1140
+ - image: ${dockerImage}
1141
+ steps:
1142
+ - checkout
1143
+ ${jobSteps}
1144
+ workflows:
1145
+ ci:
1146
+ jobs:
1147
+ - build-and-test
1148
+ `;
1149
+ const circleDir = this.resolvePath(CIRCLECI_DIR);
1150
+ if (!existsSync(circleDir)) {
1151
+ mkdirSync(circleDir, { recursive: true });
1152
+ }
1153
+ const filePath = join(circleDir, 'config.yml');
1154
+ writeFileSync(filePath, yaml, 'utf-8');
1155
+ return { success: true, path: join(CIRCLECI_DIR, 'config.yml') };
1156
+ }
1157
+ // ================================================================
1158
+ // Config Validation
1159
+ // ================================================================
1160
+ validateGitHubActionsConfig() {
1161
+ const workflowsDir = this.resolvePath(GITHUB_WORKFLOWS_DIR);
1162
+ const issues = [];
1163
+ if (!existsSync(workflowsDir)) {
1164
+ return {
1165
+ success: true,
1166
+ result: {
1167
+ valid: false,
1168
+ issues: [{ severity: 'error', message: 'No .github/workflows directory found', file: GITHUB_WORKFLOWS_DIR }],
1169
+ system: 'github-actions',
1170
+ },
1171
+ };
1172
+ }
1173
+ try {
1174
+ const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
1175
+ if (files.length === 0) {
1176
+ issues.push({ severity: 'warning', message: 'No workflow files found in .github/workflows', file: GITHUB_WORKFLOWS_DIR });
1177
+ }
1178
+ for (const file of files) {
1179
+ const filePath = join(workflowsDir, file);
1180
+ const content = readFileSync(filePath, 'utf-8');
1181
+ const lines = content.split('\n');
1182
+ // Basic YAML structure checks
1183
+ let hasOn = false;
1184
+ let hasJobs = false;
1185
+ let hasRunsOn = false;
1186
+ let hasCheckout = false;
1187
+ for (let i = 0; i < lines.length; i++) {
1188
+ const line = lines[i];
1189
+ if (/^on\s*:/.test(line.trim()))
1190
+ hasOn = true;
1191
+ if (/^jobs\s*:/.test(line.trim()))
1192
+ hasJobs = true;
1193
+ if (/runs-on\s*:/.test(line))
1194
+ hasRunsOn = true;
1195
+ if (/actions\/checkout/.test(line))
1196
+ hasCheckout = true;
1197
+ }
1198
+ if (!hasOn) {
1199
+ issues.push({ severity: 'error', message: 'Missing "on" trigger definition', file, line: 1 });
1200
+ }
1201
+ if (!hasJobs) {
1202
+ issues.push({ severity: 'error', message: 'Missing "jobs" definition', file, line: 1 });
1203
+ }
1204
+ if (hasJobs && !hasRunsOn) {
1205
+ issues.push({ severity: 'warning', message: 'No "runs-on" found - jobs may not specify runner', file });
1206
+ }
1207
+ if (hasJobs && !hasCheckout) {
1208
+ issues.push({ severity: 'info', message: 'Consider adding "actions/checkout" step', file });
1209
+ }
1210
+ // Check for common YAML issues
1211
+ if (content.includes('\t')) {
1212
+ issues.push({ severity: 'warning', message: 'File contains tab characters (YAML requires spaces)', file });
1213
+ }
1214
+ // Check for trailing spaces on key lines
1215
+ for (let i = 0; i < lines.length; i++) {
1216
+ if (lines[i].endsWith(' ') && lines[i].trim().endsWith(':')) {
1217
+ issues.push({ severity: 'info', message: 'Trailing whitespace after key', file, line: i + 1 });
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ catch (error) {
1223
+ return { success: false, error: `Validation failed: ${error instanceof Error ? error.message : String(error)}` };
1224
+ }
1225
+ const hasErrors = issues.some(i => i.severity === 'error');
1226
+ return {
1227
+ success: true,
1228
+ result: {
1229
+ valid: !hasErrors,
1230
+ issues,
1231
+ system: 'github-actions',
1232
+ },
1233
+ };
1234
+ }
1235
+ validateGitLabCIConfig() {
1236
+ const configPath = this.resolvePath(GITLAB_CI_FILE);
1237
+ const issues = [];
1238
+ if (!existsSync(configPath)) {
1239
+ return {
1240
+ success: true,
1241
+ result: {
1242
+ valid: false,
1243
+ issues: [{ severity: 'error', message: 'No .gitlab-ci.yml found', file: GITLAB_CI_FILE }],
1244
+ system: 'gitlab-ci',
1245
+ },
1246
+ };
1247
+ }
1248
+ const content = readFileSync(configPath, 'utf-8');
1249
+ if (!content.includes('stages:') && !content.includes('stage:')) {
1250
+ issues.push({ severity: 'warning', message: 'No "stages" definition found', file: GITLAB_CI_FILE });
1251
+ }
1252
+ if (content.includes('\t')) {
1253
+ issues.push({ severity: 'warning', message: 'File contains tab characters', file: GITLAB_CI_FILE });
1254
+ }
1255
+ const hasErrors = issues.some(i => i.severity === 'error');
1256
+ return {
1257
+ success: true,
1258
+ result: { valid: !hasErrors, issues, system: 'gitlab-ci' },
1259
+ };
1260
+ }
1261
+ validateJenkinsConfig() {
1262
+ const configPath = this.resolvePath(JENKINS_FILE);
1263
+ const issues = [];
1264
+ if (!existsSync(configPath)) {
1265
+ return {
1266
+ success: true,
1267
+ result: {
1268
+ valid: false,
1269
+ issues: [{ severity: 'error', message: 'No Jenkinsfile found', file: JENKINS_FILE }],
1270
+ system: 'jenkins',
1271
+ },
1272
+ };
1273
+ }
1274
+ const content = readFileSync(configPath, 'utf-8');
1275
+ if (!content.includes('pipeline') && !content.includes('node')) {
1276
+ issues.push({ severity: 'error', message: 'No "pipeline" or "node" block found', file: JENKINS_FILE });
1277
+ }
1278
+ if (!content.includes('stage')) {
1279
+ issues.push({ severity: 'warning', message: 'No "stage" definitions found', file: JENKINS_FILE });
1280
+ }
1281
+ if (!content.includes('agent')) {
1282
+ issues.push({ severity: 'info', message: 'No "agent" definition found - will use "any"', file: JENKINS_FILE });
1283
+ }
1284
+ const hasErrors = issues.some(i => i.severity === 'error');
1285
+ return {
1286
+ success: true,
1287
+ result: { valid: !hasErrors, issues, system: 'jenkins' },
1288
+ };
1289
+ }
1290
+ validateCircleCIConfig() {
1291
+ const configPath = this.resolvePath(CIRCLECI_CONFIG_FILE);
1292
+ const issues = [];
1293
+ if (!existsSync(configPath)) {
1294
+ return {
1295
+ success: true,
1296
+ result: {
1297
+ valid: false,
1298
+ issues: [{ severity: 'error', message: 'No .circleci/config.yml found', file: CIRCLECI_CONFIG_FILE }],
1299
+ system: 'circleci',
1300
+ },
1301
+ };
1302
+ }
1303
+ const content = readFileSync(configPath, 'utf-8');
1304
+ if (!content.includes('version:')) {
1305
+ issues.push({ severity: 'error', message: 'Missing "version" key', file: CIRCLECI_CONFIG_FILE });
1306
+ }
1307
+ if (!content.includes('jobs:')) {
1308
+ issues.push({ severity: 'error', message: 'Missing "jobs" definition', file: CIRCLECI_CONFIG_FILE });
1309
+ }
1310
+ if (!content.includes('workflows:')) {
1311
+ issues.push({ severity: 'error', message: 'Missing "workflows" definition', file: CIRCLECI_CONFIG_FILE });
1312
+ }
1313
+ if (content.includes('\t')) {
1314
+ issues.push({ severity: 'warning', message: 'File contains tab characters', file: CIRCLECI_CONFIG_FILE });
1315
+ }
1316
+ const hasErrors = issues.some(i => i.severity === 'error');
1317
+ return {
1318
+ success: true,
1319
+ result: { valid: !hasErrors, issues, system: 'circleci' },
1320
+ };
1321
+ }
1322
+ // ================================================================
1323
+ // YAML / Config Parsing Helpers
1324
+ // ================================================================
1325
+ mapGitHubActionsRun(r) {
1326
+ return {
1327
+ id: String(r.databaseId ?? r.id ?? ''),
1328
+ system: 'github-actions',
1329
+ name: r.name ?? '',
1330
+ status: this.mapGitHubStatus(r.status, r.conclusion),
1331
+ branch: r.headBranch ?? '',
1332
+ commit: r.headSha ?? '',
1333
+ url: r.htmlUrl ?? '',
1334
+ createdAt: r.createdAt ?? '',
1335
+ updatedAt: r.updatedAt ?? '',
1336
+ triggeredBy: r.event ?? '',
1337
+ };
1338
+ }
1339
+ mapGitHubStatus(status, conclusion) {
1340
+ if (conclusion === 'success')
1341
+ return 'success';
1342
+ if (conclusion === 'failure')
1343
+ return 'failure';
1344
+ if (conclusion === 'cancelled')
1345
+ return 'cancelled';
1346
+ if (conclusion === 'skipped')
1347
+ return 'skipped';
1348
+ switch (status) {
1349
+ case 'queued': return 'queued';
1350
+ case 'in_progress':
1351
+ case 'running': return 'in_progress';
1352
+ case 'waiting': return 'waiting';
1353
+ case 'completed':
1354
+ return conclusion ? this.mapGitHubStatus('', conclusion) : 'unknown';
1355
+ default: return 'unknown';
1356
+ }
1357
+ }
1358
+ extractYAMLTriggers(content) {
1359
+ const triggers = [];
1360
+ const onMatch = content.match(/^on\s*:\s*$/m);
1361
+ if (onMatch) {
1362
+ const afterOn = content.slice(onMatch.index + onMatch[0].length);
1363
+ const triggerBlock = afterOn.split(/^[\w_-]+\s*:/m)[0];
1364
+ const pushMatch = triggerBlock.match(/push\s*:/);
1365
+ const prMatch = triggerBlock.match(/pull_request\s*:/);
1366
+ const scheduleMatch = triggerBlock.match(/schedule\s*:/);
1367
+ const workflowDispatchMatch = triggerBlock.match(/workflow_dispatch\s*:/);
1368
+ if (pushMatch)
1369
+ triggers.push('push');
1370
+ if (prMatch)
1371
+ triggers.push('pull_request');
1372
+ if (scheduleMatch)
1373
+ triggers.push('schedule');
1374
+ if (workflowDispatchMatch)
1375
+ triggers.push('workflow_dispatch');
1376
+ }
1377
+ // Also check for single-line on: [push, pull_request]
1378
+ const inlineMatch = content.match(/^on\s*:\s*\[(.+?)\]/m);
1379
+ if (inlineMatch) {
1380
+ const items = inlineMatch[1].split(',').map(s => s.trim());
1381
+ triggers.push(...items);
1382
+ }
1383
+ // Check for simple on: push
1384
+ const simpleMatch = content.match(/^on\s*:\s*(\w+)\s*$/m);
1385
+ if (simpleMatch && triggers.length === 0) {
1386
+ triggers.push(simpleMatch[1]);
1387
+ }
1388
+ return triggers.length > 0 ? triggers : ['unknown'];
1389
+ }
1390
+ extractYAMLWorkflowName(content) {
1391
+ const match = content.match(/^name\s*:\s*['"]?(.+?)['"]?\s*$/m);
1392
+ return match ? match[1].trim() : null;
1393
+ }
1394
+ extractYAMLStages(content) {
1395
+ const stages = [];
1396
+ const match = content.match(/^stages\s*:\s*$/m);
1397
+ if (match) {
1398
+ const afterStages = content.slice(match.index + match[0].length);
1399
+ const lines = afterStages.split('\n');
1400
+ for (const line of lines) {
1401
+ const stageMatch = line.match(/^\s+-\s*['"]?(.+?)['"]?\s*$/);
1402
+ if (stageMatch) {
1403
+ stages.push(stageMatch[1].trim());
1404
+ }
1405
+ else if (line.trim() && !line.trim().startsWith('#') && !line.match(/^\s+-/)) {
1406
+ break;
1407
+ }
1408
+ }
1409
+ }
1410
+ return stages.length > 0 ? stages : ['build', 'test', 'deploy'];
1411
+ }
1412
+ extractJenkinsStages(content) {
1413
+ const stages = [];
1414
+ const stageRegex = /stage\s*\(\s*['"](.+?)['"]\s*\)/g;
1415
+ let match;
1416
+ while ((match = stageRegex.exec(content)) !== null) {
1417
+ stages.push(match[1]);
1418
+ }
1419
+ return stages;
1420
+ }
1421
+ extractCircleCIJobs(content) {
1422
+ const jobs = [];
1423
+ const lines = content.split('\n');
1424
+ let inJobs = false;
1425
+ for (const line of lines) {
1426
+ if (/^jobs\s*:/.test(line.trim())) {
1427
+ inJobs = true;
1428
+ continue;
1429
+ }
1430
+ if (inJobs) {
1431
+ const jobMatch = line.match(/^\s{2}(\w[\w-]*)\s*:/);
1432
+ if (jobMatch) {
1433
+ jobs.push(jobMatch[1]);
1434
+ }
1435
+ else if (line.trim() && !line.trim().startsWith('#') && !line.match(/^\s{4,}/)) {
1436
+ inJobs = false;
1437
+ }
1438
+ }
1439
+ }
1440
+ return jobs.length > 0 ? jobs : ['build'];
1441
+ }
1442
+ }
1443
+ //# sourceMappingURL=cicd.js.map