icopilot 2.2.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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,624 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { theme } from '../ui/theme.js';
4
+ const DEFAULT_BRANCHES = ['main', 'master'];
5
+ const WORKFLOW_DIRECTORY = ['.github', 'workflows'];
6
+ const TEMPLATE_LIBRARY = [
7
+ {
8
+ name: 'CI',
9
+ test: (description) => /\b(ci|build|test|unit test|integration test|pull request|pr|push)\b/u.test(description),
10
+ template: {
11
+ name: 'CI',
12
+ triggers: {
13
+ push: { branches: DEFAULT_BRANCHES },
14
+ pull_request: { branches: DEFAULT_BRANCHES },
15
+ },
16
+ jobs: [
17
+ {
18
+ name: 'Build and test',
19
+ runsOn: 'ubuntu-latest',
20
+ steps: [
21
+ {
22
+ comment: 'Check out the repository before running build and test steps.',
23
+ uses: 'actions/checkout@v4',
24
+ },
25
+ {
26
+ name: 'Setup Node.js',
27
+ comment: 'Use a current Node.js LTS runtime with npm caching enabled.',
28
+ uses: 'actions/setup-node@v4',
29
+ with: {
30
+ 'node-version': 20,
31
+ cache: 'npm',
32
+ },
33
+ },
34
+ {
35
+ name: 'Install dependencies',
36
+ run: 'npm ci',
37
+ },
38
+ {
39
+ name: 'Build',
40
+ run: 'npm run build --if-present',
41
+ },
42
+ {
43
+ name: 'Run tests',
44
+ run: 'npm test --if-present',
45
+ },
46
+ ],
47
+ },
48
+ ],
49
+ },
50
+ },
51
+ {
52
+ name: 'CD',
53
+ test: (description) => /\b(cd|deploy|deployment|publish|tag|release)\b/u.test(description),
54
+ template: {
55
+ name: 'CD',
56
+ triggers: {
57
+ push: { tags: ['v*'] },
58
+ release: { types: ['published'] },
59
+ },
60
+ jobs: [
61
+ {
62
+ name: 'Deploy',
63
+ runsOn: 'ubuntu-latest',
64
+ steps: [
65
+ {
66
+ comment: 'Fetch the code that is being deployed.',
67
+ uses: 'actions/checkout@v4',
68
+ },
69
+ {
70
+ name: 'Setup Node.js',
71
+ uses: 'actions/setup-node@v4',
72
+ with: {
73
+ 'node-version': 20,
74
+ cache: 'npm',
75
+ },
76
+ },
77
+ {
78
+ name: 'Install dependencies',
79
+ run: 'npm ci',
80
+ },
81
+ {
82
+ name: 'Build release artifacts',
83
+ run: 'npm run build --if-present',
84
+ },
85
+ {
86
+ name: 'Deploy application',
87
+ comment: 'Replace this placeholder with the deployment command for your environment.',
88
+ run: 'npm run deploy --if-present',
89
+ },
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ },
95
+ {
96
+ name: 'Lint',
97
+ test: (description) => /\b(lint|eslint|prettier|format)\b/u.test(description),
98
+ template: {
99
+ name: 'Lint',
100
+ triggers: {
101
+ pull_request: { branches: DEFAULT_BRANCHES },
102
+ },
103
+ jobs: [
104
+ {
105
+ name: 'Lint and format',
106
+ runsOn: 'ubuntu-latest',
107
+ steps: [
108
+ {
109
+ comment: 'Check out the repository before static analysis.',
110
+ uses: 'actions/checkout@v4',
111
+ },
112
+ {
113
+ name: 'Setup Node.js',
114
+ uses: 'actions/setup-node@v4',
115
+ with: {
116
+ 'node-version': 20,
117
+ cache: 'npm',
118
+ },
119
+ },
120
+ {
121
+ name: 'Install dependencies',
122
+ run: 'npm ci',
123
+ },
124
+ {
125
+ name: 'Run ESLint',
126
+ run: 'npm run lint --if-present',
127
+ },
128
+ {
129
+ name: 'Check formatting',
130
+ run: 'npm run format:check --if-present',
131
+ },
132
+ ],
133
+ },
134
+ ],
135
+ },
136
+ },
137
+ {
138
+ name: 'Security',
139
+ test: (description) => /\b(security|audit|dependency audit|vulnerability|dependencies)\b/u.test(description),
140
+ template: {
141
+ name: 'Security',
142
+ triggers: {
143
+ pull_request: { branches: DEFAULT_BRANCHES },
144
+ schedule: { cron: ['0 6 * * 1'] },
145
+ },
146
+ jobs: [
147
+ {
148
+ name: 'Dependency audit',
149
+ runsOn: 'ubuntu-latest',
150
+ steps: [
151
+ {
152
+ comment: 'Use the repository state from the triggering commit or pull request.',
153
+ uses: 'actions/checkout@v4',
154
+ },
155
+ {
156
+ name: 'Setup Node.js',
157
+ uses: 'actions/setup-node@v4',
158
+ with: {
159
+ 'node-version': 20,
160
+ cache: 'npm',
161
+ },
162
+ },
163
+ {
164
+ name: 'Install dependencies',
165
+ run: 'npm ci',
166
+ },
167
+ {
168
+ name: 'Run npm audit',
169
+ run: 'npm audit --audit-level=high',
170
+ },
171
+ ],
172
+ },
173
+ ],
174
+ },
175
+ },
176
+ {
177
+ name: 'Release',
178
+ test: (description) => /\b(semantic-release|semantic release|release automation|automated release)\b/u.test(description),
179
+ template: {
180
+ name: 'Release',
181
+ triggers: {
182
+ push: { branches: ['main'] },
183
+ workflow_dispatch: {},
184
+ },
185
+ jobs: [
186
+ {
187
+ name: 'Semantic release',
188
+ runsOn: 'ubuntu-latest',
189
+ permissions: {
190
+ contents: 'write',
191
+ issues: 'write',
192
+ 'pull-requests': 'write',
193
+ },
194
+ steps: [
195
+ {
196
+ comment: 'Check out the full git history so semantic-release can inspect tags and commits.',
197
+ uses: 'actions/checkout@v4',
198
+ with: {
199
+ 'fetch-depth': 0,
200
+ },
201
+ },
202
+ {
203
+ name: 'Setup Node.js',
204
+ uses: 'actions/setup-node@v4',
205
+ with: {
206
+ 'node-version': 20,
207
+ cache: 'npm',
208
+ },
209
+ },
210
+ {
211
+ name: 'Install dependencies',
212
+ run: 'npm ci',
213
+ },
214
+ {
215
+ name: 'Build',
216
+ run: 'npm run build --if-present',
217
+ },
218
+ {
219
+ name: 'Run semantic-release',
220
+ env: {
221
+ GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}',
222
+ },
223
+ run: 'npx semantic-release',
224
+ },
225
+ ],
226
+ },
227
+ ],
228
+ },
229
+ },
230
+ ];
231
+ export function actionsCommand(args, cwd) {
232
+ const [subcommand, ...rest] = args;
233
+ const normalized = subcommand?.toLowerCase();
234
+ if (!subcommand) {
235
+ return usage();
236
+ }
237
+ if (normalized === 'list') {
238
+ return listWorkflows(cwd);
239
+ }
240
+ if (normalized === 'validate') {
241
+ return validateWorkflows(cwd);
242
+ }
243
+ const description = args.join(' ').trim();
244
+ if (!description) {
245
+ return usage();
246
+ }
247
+ const fileName = suggestWorkflowFileName(description);
248
+ const yaml = generateWorkflow(description);
249
+ return [
250
+ `${theme.brand('Generated workflow')} ${theme.dim(`save as .github/workflows/${fileName}`)}`,
251
+ '',
252
+ yaml,
253
+ '',
254
+ ].join('\n');
255
+ }
256
+ export function generateWorkflow(description) {
257
+ const normalized = description.trim().toLowerCase();
258
+ const selectedTemplates = selectTemplates(normalized);
259
+ const workflow = mergeTemplates(selectedTemplates, description.trim());
260
+ return renderWorkflow(workflow, description.trim(), selectedTemplates.map((entry) => entry.name));
261
+ }
262
+ export function validateWorkflowYaml(content) {
263
+ const lines = content.split(/\r?\n/u);
264
+ const errors = [];
265
+ const meaningfulLines = lines.filter((line) => {
266
+ const trimmed = line.trim();
267
+ return trimmed.length > 0 && !trimmed.startsWith('#');
268
+ });
269
+ if (meaningfulLines.length === 0) {
270
+ return ['workflow file is empty'];
271
+ }
272
+ if (lines.some((line) => line.includes('\t'))) {
273
+ errors.push('tab indentation is not supported; use spaces only');
274
+ }
275
+ if (!meaningfulLines.some((line) => /^name:\s+\S/u.test(line))) {
276
+ errors.push('missing top-level "name" field');
277
+ }
278
+ const hasOnBlock = meaningfulLines.some((line) => line === 'on:' || /^on:\s+\S/u.test(line)) ||
279
+ meaningfulLines.some((line) => /^"on":\s+\S/u.test(line));
280
+ if (!hasOnBlock) {
281
+ errors.push('missing top-level "on" trigger block');
282
+ }
283
+ const jobsIndex = meaningfulLines.findIndex((line) => line === 'jobs:' || /^jobs:\s*$/u.test(line));
284
+ if (jobsIndex === -1) {
285
+ errors.push('missing top-level "jobs" block');
286
+ return errors;
287
+ }
288
+ const jobLines = meaningfulLines.slice(jobsIndex + 1);
289
+ const jobKeys = jobLines.filter((line) => /^ {2}[A-Za-z0-9_-]+:\s*$/u.test(line));
290
+ if (jobKeys.length === 0) {
291
+ errors.push('jobs block must define at least one job');
292
+ }
293
+ for (const jobKey of jobKeys) {
294
+ const match = /^ {2}([A-Za-z0-9_-]+):\s*$/u.exec(jobKey);
295
+ const jobId = match?.[1];
296
+ if (!jobId)
297
+ continue;
298
+ const jobSection = sliceJobSection(meaningfulLines, jobId);
299
+ if (!jobSection.some((line) => /^ {4}runs-on:\s+\S/u.test(line))) {
300
+ errors.push(`job "${jobId}" is missing "runs-on"`);
301
+ }
302
+ if (!jobSection.some((line) => /^ {4}steps:\s*$/u.test(line))) {
303
+ errors.push(`job "${jobId}" is missing "steps"`);
304
+ continue;
305
+ }
306
+ if (!jobSection.some((line) => /^ {6}-(?:\s|$)/u.test(line))) {
307
+ errors.push(`job "${jobId}" must include at least one step`);
308
+ }
309
+ }
310
+ return errors;
311
+ }
312
+ function usage() {
313
+ return [
314
+ theme.brand('Actions command'),
315
+ ` ${theme.hl('/actions <description>')} ${theme.dim('generate a GitHub Actions workflow from natural language')}`,
316
+ ` ${theme.hl('/actions list')} ${theme.dim('list existing workflow files in .github/workflows')}`,
317
+ ` ${theme.hl('/actions validate')} ${theme.dim('run a lightweight validation pass on existing workflow YAML')}`,
318
+ '',
319
+ ].join('\n');
320
+ }
321
+ function workflowDir(cwd) {
322
+ return path.join(cwd, ...WORKFLOW_DIRECTORY);
323
+ }
324
+ function listWorkflows(cwd) {
325
+ const files = getWorkflowFiles(cwd);
326
+ if (files.length === 0) {
327
+ return `${theme.warn('No workflow files found in .github/workflows.')}\n`;
328
+ }
329
+ const lines = files.map((file) => {
330
+ const filePath = path.join(workflowDir(cwd), file);
331
+ const lineCount = readFile(filePath).split(/\r?\n/u).length;
332
+ return ` ${theme.ok(file)} ${theme.dim(`(${lineCount} lines)`)}`;
333
+ });
334
+ return `${theme.brand('Existing workflows')}\n${lines.join('\n')}\n`;
335
+ }
336
+ function validateWorkflows(cwd) {
337
+ const files = getWorkflowFiles(cwd);
338
+ if (files.length === 0) {
339
+ return `${theme.warn('No workflow files found to validate.')}\n`;
340
+ }
341
+ const results = files.map((file) => {
342
+ const filePath = path.join(workflowDir(cwd), file);
343
+ return {
344
+ file,
345
+ errors: validateWorkflowYaml(readFile(filePath)),
346
+ };
347
+ });
348
+ const validCount = results.filter((result) => result.errors.length === 0).length;
349
+ const invalidCount = results.length - validCount;
350
+ const lines = results.flatMap((result) => {
351
+ if (result.errors.length === 0) {
352
+ return [` ${theme.ok('valid')} ${result.file}`];
353
+ }
354
+ return [
355
+ ` ${theme.err('invalid')} ${result.file}`,
356
+ ...result.errors.map((error) => ` - ${error}`),
357
+ ];
358
+ });
359
+ return [
360
+ theme.brand('Workflow validation'),
361
+ ` checked: ${theme.hl(String(results.length))} valid: ${theme.ok(String(validCount))} invalid: ${invalidCount > 0 ? theme.err(String(invalidCount)) : theme.ok('0')}`,
362
+ '',
363
+ ...lines,
364
+ '',
365
+ ].join('\n');
366
+ }
367
+ function readFile(filePath) {
368
+ try {
369
+ return fs.readFileSync(filePath, 'utf8');
370
+ }
371
+ catch {
372
+ return '';
373
+ }
374
+ }
375
+ function getWorkflowFiles(cwd) {
376
+ const directory = workflowDir(cwd);
377
+ try {
378
+ return fs
379
+ .readdirSync(directory, { withFileTypes: true })
380
+ .filter((entry) => entry.isFile() && /\.(?:ya?ml)$/iu.test(entry.name))
381
+ .map((entry) => entry.name)
382
+ .sort((left, right) => left.localeCompare(right));
383
+ }
384
+ catch {
385
+ return [];
386
+ }
387
+ }
388
+ function selectTemplates(description) {
389
+ const matches = TEMPLATE_LIBRARY.filter((entry) => entry.test(description));
390
+ return matches.length > 0 ? matches : [TEMPLATE_LIBRARY[0]];
391
+ }
392
+ function mergeTemplates(selectedTemplates, description) {
393
+ const workflow = {
394
+ name: buildWorkflowName(selectedTemplates, description),
395
+ triggers: {},
396
+ jobs: [],
397
+ };
398
+ for (const entry of selectedTemplates) {
399
+ for (const [triggerName, config] of Object.entries(entry.template.triggers)) {
400
+ if (!config)
401
+ continue;
402
+ workflow.triggers[triggerName] = mergeTriggerConfig(workflow.triggers[triggerName], config);
403
+ }
404
+ for (const job of entry.template.jobs) {
405
+ workflow.jobs.push(cloneJob(job));
406
+ }
407
+ }
408
+ return workflow;
409
+ }
410
+ function mergeTriggerConfig(current, next) {
411
+ const merged = {
412
+ branches: mergeStringLists(current?.branches, next?.branches),
413
+ tags: mergeStringLists(current?.tags, next?.tags),
414
+ types: mergeStringLists(current?.types, next?.types),
415
+ cron: mergeStringLists(current?.cron, next?.cron),
416
+ };
417
+ return Object.fromEntries(Object.entries(merged).filter(([, value]) => Array.isArray(value) && value.length > 0));
418
+ }
419
+ function mergeStringLists(left, right) {
420
+ const values = [...(left ?? []), ...(right ?? [])];
421
+ if (values.length === 0)
422
+ return undefined;
423
+ return [...new Set(values)];
424
+ }
425
+ function cloneJob(job) {
426
+ return {
427
+ ...job,
428
+ needs: job.needs ? [...job.needs] : undefined,
429
+ permissions: job.permissions ? { ...job.permissions } : undefined,
430
+ env: job.env ? { ...job.env } : undefined,
431
+ steps: job.steps.map((step) => ({
432
+ ...step,
433
+ with: step.with ? { ...step.with } : undefined,
434
+ env: step.env ? { ...step.env } : undefined,
435
+ })),
436
+ };
437
+ }
438
+ function buildWorkflowName(selectedTemplates, description) {
439
+ if (selectedTemplates.length === 1) {
440
+ return `${selectedTemplates[0].name} workflow`;
441
+ }
442
+ if (description.length <= 72) {
443
+ return toTitleCase(description);
444
+ }
445
+ return `${selectedTemplates.map((entry) => entry.name).join(' + ')} workflow`;
446
+ }
447
+ function suggestWorkflowFileName(description) {
448
+ const normalized = description
449
+ .toLowerCase()
450
+ .replace(/[^a-z0-9]+/gu, '-')
451
+ .replace(/^-+|-+$/gu, '')
452
+ .slice(0, 48);
453
+ return `${normalized || 'workflow'}.yml`;
454
+ }
455
+ function renderWorkflow(workflow, description, selectedTemplateNames) {
456
+ const lines = [
457
+ '# Generated by iCopilot /actions',
458
+ `# Description: ${description}`,
459
+ `# Templates: ${selectedTemplateNames.join(', ')}`,
460
+ `name: ${yamlScalar(workflow.name)}`,
461
+ '',
462
+ 'on:',
463
+ ];
464
+ lines.push(...renderTriggers(workflow.triggers));
465
+ lines.push('', 'jobs:');
466
+ lines.push(...renderJobs(workflow.jobs));
467
+ return lines.join('\n');
468
+ }
469
+ function renderTriggers(triggers) {
470
+ const lines = [
471
+ ' # Adjust branches, tags, or schedules to match your release strategy.',
472
+ ];
473
+ const orderedTriggers = [
474
+ 'push',
475
+ 'pull_request',
476
+ 'release',
477
+ 'workflow_dispatch',
478
+ 'schedule',
479
+ ];
480
+ for (const triggerName of orderedTriggers) {
481
+ const config = triggers[triggerName];
482
+ if (!config)
483
+ continue;
484
+ if (triggerName === 'workflow_dispatch') {
485
+ lines.push(' workflow_dispatch:');
486
+ continue;
487
+ }
488
+ if (triggerName === 'schedule') {
489
+ lines.push(' schedule:');
490
+ for (const cron of config.cron ?? []) {
491
+ lines.push(' - cron: ' + yamlScalar(cron));
492
+ }
493
+ continue;
494
+ }
495
+ const hasBody = Boolean((config.branches && config.branches.length > 0) ||
496
+ (config.tags && config.tags.length > 0) ||
497
+ (config.types && config.types.length > 0));
498
+ if (!hasBody) {
499
+ lines.push(` ${triggerName}:`);
500
+ continue;
501
+ }
502
+ lines.push(` ${triggerName}:`);
503
+ if (config.branches?.length) {
504
+ lines.push(' branches:');
505
+ for (const branch of config.branches) {
506
+ lines.push(` - ${yamlScalar(branch)}`);
507
+ }
508
+ }
509
+ if (config.tags?.length) {
510
+ lines.push(' tags:');
511
+ for (const tag of config.tags) {
512
+ lines.push(` - ${yamlScalar(tag)}`);
513
+ }
514
+ }
515
+ if (config.types?.length) {
516
+ lines.push(' types:');
517
+ for (const type of config.types) {
518
+ lines.push(` - ${yamlScalar(type)}`);
519
+ }
520
+ }
521
+ }
522
+ return lines;
523
+ }
524
+ function renderJobs(jobs) {
525
+ const usedIds = new Set();
526
+ const lines = [];
527
+ for (const job of jobs) {
528
+ const jobId = uniqueJobId(job.name, usedIds);
529
+ lines.push(` # ${job.name}`);
530
+ lines.push(` ${jobId}:`);
531
+ lines.push(` name: ${yamlScalar(job.name)}`);
532
+ lines.push(` runs-on: ${yamlScalar(job.runsOn)}`);
533
+ if (job.needs?.length) {
534
+ lines.push(` needs: ${job.needs.length === 1 ? yamlScalar(job.needs[0]) : `[${job.needs.map(yamlScalar).join(', ')}]`}`);
535
+ }
536
+ if (job.permissions && Object.keys(job.permissions).length > 0) {
537
+ lines.push(' permissions:');
538
+ for (const [key, value] of Object.entries(job.permissions)) {
539
+ lines.push(` ${key}: ${yamlScalar(value)}`);
540
+ }
541
+ }
542
+ if (job.env && Object.keys(job.env).length > 0) {
543
+ lines.push(' env:');
544
+ for (const [key, value] of Object.entries(job.env)) {
545
+ lines.push(` ${key}: ${yamlScalar(value)}`);
546
+ }
547
+ }
548
+ lines.push(' steps:');
549
+ for (const step of job.steps) {
550
+ if (step.comment) {
551
+ lines.push(` # ${step.comment}`);
552
+ }
553
+ lines.push(...renderStep(step));
554
+ }
555
+ }
556
+ return lines;
557
+ }
558
+ function renderStep(step) {
559
+ const lines = [' -'];
560
+ if (step.name)
561
+ lines.push(` name: ${yamlScalar(step.name)}`);
562
+ if (step.uses)
563
+ lines.push(` uses: ${yamlScalar(step.uses)}`);
564
+ if (step.run)
565
+ lines.push(` run: ${yamlScalar(step.run)}`);
566
+ if (step.shell)
567
+ lines.push(` shell: ${yamlScalar(step.shell)}`);
568
+ if (step.with && Object.keys(step.with).length > 0) {
569
+ lines.push(' with:');
570
+ for (const [key, value] of Object.entries(step.with)) {
571
+ lines.push(` ${key}: ${yamlScalar(value)}`);
572
+ }
573
+ }
574
+ if (step.env && Object.keys(step.env).length > 0) {
575
+ lines.push(' env:');
576
+ for (const [key, value] of Object.entries(step.env)) {
577
+ lines.push(` ${key}: ${yamlScalar(value)}`);
578
+ }
579
+ }
580
+ return lines;
581
+ }
582
+ function yamlScalar(value) {
583
+ const text = String(value);
584
+ if (/^\$\{\{.+\}\}$/u.test(text))
585
+ return `'${text}'`;
586
+ if (/^[A-Za-z0-9._/@:-]+$/u.test(text))
587
+ return text;
588
+ return `'${text.replace(/'/gu, `''`)}'`;
589
+ }
590
+ function uniqueJobId(name, usedIds) {
591
+ const baseId = name
592
+ .toLowerCase()
593
+ .replace(/[^a-z0-9]+/gu, '_')
594
+ .replace(/^_+|_+$/gu, '') || 'job';
595
+ let candidate = baseId;
596
+ let index = 2;
597
+ while (usedIds.has(candidate)) {
598
+ candidate = `${baseId}_${index}`;
599
+ index += 1;
600
+ }
601
+ usedIds.add(candidate);
602
+ return candidate;
603
+ }
604
+ function sliceJobSection(lines, jobId) {
605
+ const start = lines.findIndex((line) => line === ` ${jobId}:`);
606
+ if (start === -1)
607
+ return [];
608
+ const jobLines = [];
609
+ for (let index = start; index < lines.length; index += 1) {
610
+ const line = lines[index];
611
+ if (index !== start && /^ {2}[A-Za-z0-9_-]+:\s*$/u.test(line)) {
612
+ break;
613
+ }
614
+ jobLines.push(line);
615
+ }
616
+ return jobLines;
617
+ }
618
+ function toTitleCase(value) {
619
+ return value
620
+ .split(/\s+/u)
621
+ .filter(Boolean)
622
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
623
+ .join(' ');
624
+ }