self-healing-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +130 -0
  2. package/dist/cli.js +273 -0
  3. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # self-healing-cli
2
+
3
+ A CI auto-fix CLI tool powered by [GitHub Copilot CLI](https://github.com/github/copilot). When your CI fails, it captures the error logs, asks Copilot to fix them, verifies the fix, and creates a PR — or leaves a comment if the fix doesn't work.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ CI command fails
9
+ |
10
+ v
11
+ +---------+ +-----------+ +----------+
12
+ | collect |---->| heal |---->| Copilot |
13
+ | (logs) | | (analyze) | | (fix) |
14
+ +---------+ +-----------+ +----------+
15
+ |
16
+ v
17
+ Has changes?
18
+ / \
19
+ yes no
20
+ / \
21
+ Verify Comment:
22
+ command "no changes"
23
+ / \
24
+ pass fail
25
+ / \
26
+ Create PR Comment:
27
+ "needs review"
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pnpm install
34
+ pnpm build
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### `collect` — Capture CI logs
40
+
41
+ Run a command and save its output to a file. Exits with the command's exit code.
42
+
43
+ ```bash
44
+ self-healing collect --command <cmd> --output <file>
45
+ ```
46
+
47
+ | Option | Description |
48
+ |---|---|
49
+ | `--command` | Command to run |
50
+ | `--output` | Output log file path |
51
+
52
+ Example:
53
+
54
+ ```bash
55
+ self-healing collect --command "pnpm run check" --output check.log
56
+ ```
57
+
58
+ ### `heal` — Auto-fix with Copilot
59
+
60
+ Read a log file, send it to Copilot CLI for analysis, verify the fix, and create a PR or comment.
61
+
62
+ ```bash
63
+ self-healing heal --log <file> --verify <cmd> [--model <model>] [--tail <lines>]
64
+ ```
65
+
66
+ | Option | Description | Default |
67
+ |---|---|---|
68
+ | `--log` | Input log file path | (required) |
69
+ | `--verify` | Command to verify the fix | (required) |
70
+ | `--model` | Copilot model to use | Copilot default |
71
+ | `--tail` | Number of log lines to send | `100` |
72
+
73
+ Example:
74
+
75
+ ```bash
76
+ self-healing heal --log check.log --verify "pnpm run check" --model gpt-5.2
77
+ ```
78
+
79
+ ## GitHub Actions
80
+
81
+ ### Auto-heal on lint/test failure
82
+
83
+ ```yaml
84
+ # .github/workflows/lint.yml
85
+ - name: Lint
86
+ id: lint
87
+ continue-on-error: true
88
+ run: npx self-healing collect --command "pnpm run check" --output lint.log
89
+
90
+ - name: Heal lint
91
+ if: steps.lint.outcome == 'failure'
92
+ env:
93
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
94
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95
+ run: |
96
+ npm install -g @github/copilot
97
+ npx self-healing heal --log lint.log --verify "pnpm run check"
98
+ ```
99
+
100
+ ### Manual dispatch
101
+
102
+ The `copilot.yml` workflow supports `workflow_dispatch` with custom `command` and `verify` inputs, so you can trigger a heal run from the GitHub Actions UI for any command.
103
+
104
+ ## Prerequisites
105
+
106
+ - Node.js >= 22
107
+ - [GitHub Copilot CLI](https://github.com/github/copilot) (`npm install -g @github/copilot`)
108
+ - [GitHub CLI](https://cli.github.com/) (`gh`) — for creating PRs and comments
109
+ - `COPILOT_GITHUB_TOKEN` secret in your repo
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ pnpm install # install dependencies
115
+ pnpm dev # watch mode
116
+ pnpm build # build
117
+ pnpm test # run tests
118
+ pnpm run check # lint + format (biome)
119
+ ```
120
+
121
+ ## Tech Stack
122
+
123
+ - TypeScript (ESM)
124
+ - [Rslib](https://rslib.rs/) — build
125
+ - [Rstest](https://rstest.rs/) — test
126
+ - [Biome](https://biomejs.dev/) — lint & format
127
+
128
+ ## License
129
+
130
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ import node_fs from "node:fs";
3
+ import node_path from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ const exec = (command, options)=>{
6
+ try {
7
+ const stdout = execSync(command, {
8
+ cwd: options?.cwd,
9
+ env: options?.env ? {
10
+ ...process.env,
11
+ ...options.env
12
+ } : void 0,
13
+ encoding: 'utf-8',
14
+ stdio: [
15
+ 'pipe',
16
+ 'pipe',
17
+ 'pipe'
18
+ ]
19
+ });
20
+ return {
21
+ ok: true,
22
+ stdout,
23
+ stderr: '',
24
+ exitCode: 0
25
+ };
26
+ } catch (error) {
27
+ const err = error;
28
+ return {
29
+ ok: false,
30
+ stdout: err.stdout ?? '',
31
+ stderr: err.stderr ?? '',
32
+ exitCode: err.status ?? 1
33
+ };
34
+ }
35
+ };
36
+ const prefix = '[self-healing]';
37
+ const logger = {
38
+ info: (...args)=>console.log(prefix, ...args),
39
+ error: (...args)=>console.error(prefix, 'ERROR:', ...args),
40
+ success: (...args)=>console.log(prefix, '✓', ...args)
41
+ };
42
+ const collect = (options)=>{
43
+ const { command, outputFile } = options;
44
+ logger.info(`Running: ${command}`);
45
+ const result = exec(`${command} 2>&1`);
46
+ const output = result.stdout + result.stderr;
47
+ const dir = node_path.dirname(outputFile);
48
+ if (!node_fs.existsSync(dir)) node_fs.mkdirSync(dir, {
49
+ recursive: true
50
+ });
51
+ node_fs.writeFileSync(outputFile, output, 'utf-8');
52
+ logger.info(`Log saved to ${outputFile} (exit code: ${result.exitCode})`);
53
+ return {
54
+ exitCode: result.exitCode,
55
+ logFile: outputFile
56
+ };
57
+ };
58
+ const getCurrentBranch = (cwd)=>{
59
+ const result = exec('git branch --show-current', {
60
+ cwd
61
+ });
62
+ return result.stdout.trim();
63
+ };
64
+ const hasChanges = (cwd)=>{
65
+ const result = exec('git status --porcelain', {
66
+ cwd
67
+ });
68
+ return result.stdout.trim().length > 0;
69
+ };
70
+ const createBranch = (name, cwd)=>{
71
+ exec(`git checkout -b ${name}`, {
72
+ cwd
73
+ });
74
+ };
75
+ const commitAll = (message, cwd)=>{
76
+ exec('git add -A', {
77
+ cwd
78
+ });
79
+ exec(`git commit -m "${message}"`, {
80
+ cwd
81
+ });
82
+ };
83
+ const push = (branch, cwd)=>{
84
+ exec(`git push origin ${branch}`, {
85
+ cwd
86
+ });
87
+ };
88
+ const configBot = (cwd)=>{
89
+ exec('git config user.name "github-actions[bot]"', {
90
+ cwd
91
+ });
92
+ exec('git config user.email "github-actions[bot]@users.noreply.github.com"', {
93
+ cwd
94
+ });
95
+ };
96
+ const buildPrBody = (options)=>`## Auto-generated Fix
97
+
98
+ This PR was automatically created to fix CI failures on **${options.baseBranch}**.
99
+
100
+ ### Error Log
101
+ \`\`\`
102
+ ${options.logSnippet}
103
+ \`\`\`
104
+
105
+ ---
106
+ *Generated by self-healing-cli*`;
107
+ const createPullRequest = (options)=>{
108
+ const result = exec(`gh pr create --title "${options.title}" --body "${options.body.replace(/"/g, '\\"')}" --base "${options.base}" --head "${options.head}"`, {
109
+ cwd: options.cwd
110
+ });
111
+ if (!result.ok) logger.error('Failed to create PR:', result.stderr);
112
+ return result.stdout.trim();
113
+ };
114
+ const commentOnCommit = (options)=>{
115
+ const result = exec(`gh api repos/{owner}/{repo}/commits/${options.sha}/comments -f body="${options.body.replace(/"/g, '\\"')}"`, {
116
+ cwd: options.cwd
117
+ });
118
+ if (!result.ok) logger.error('Failed to comment on commit:', result.stderr);
119
+ };
120
+ const readLogTail = (logFile, lines)=>{
121
+ const content = node_fs.readFileSync(logFile, 'utf-8');
122
+ const allLines = content.split('\n');
123
+ return allLines.slice(-lines).join('\n');
124
+ };
125
+ const buildCopilotPrompt = (options)=>[
126
+ 'Analyze this CI failure and fix it.',
127
+ 'After applying fixes, verify by running:',
128
+ ` ${options.verifyCommand}`,
129
+ '',
130
+ 'Error log:',
131
+ options.errorLog
132
+ ].join('\n');
133
+ const heal = (options)=>{
134
+ const { logFile, verifyCommand, tailLines = 100, model } = options;
135
+ const errorLog = readLogTail(logFile, tailLines);
136
+ logger.info(`Read ${tailLines} lines from ${logFile}`);
137
+ const prompt = buildCopilotPrompt({
138
+ errorLog,
139
+ verifyCommand
140
+ });
141
+ const baseBranch = getCurrentBranch();
142
+ const shaResult = exec('git rev-parse HEAD');
143
+ const commitSha = shaResult.stdout.trim();
144
+ const fixBranch = `auto-fix/self-healing-${Date.now()}`;
145
+ configBot();
146
+ createBranch(fixBranch);
147
+ logger.info(`Created branch: ${fixBranch}`);
148
+ logger.info('Asking Copilot to analyze and fix...');
149
+ const modelFlag = model ? ` --model ${model}` : '';
150
+ const escapedPrompt = prompt.replace(/"/g, '\\"');
151
+ const copilotResult = exec(`copilot --prompt "${escapedPrompt}" --yolo${modelFlag}`);
152
+ if (!copilotResult.ok) {
153
+ logger.error('Copilot CLI failed:', copilotResult.stderr);
154
+ const body = `## Self-Healing CLI\n\nCopilot CLI failed to run.\n\n\`\`\`\n${copilotResult.stderr}\n\`\`\``;
155
+ commentOnCommit({
156
+ sha: commitSha,
157
+ body
158
+ });
159
+ return {
160
+ fixed: false,
161
+ comment: body
162
+ };
163
+ }
164
+ if (!hasChanges()) {
165
+ logger.info('Copilot made no file changes');
166
+ const body = '## Self-Healing CLI\n\nCopilot analyzed the failure but did not produce any file changes.';
167
+ commentOnCommit({
168
+ sha: commitSha,
169
+ body
170
+ });
171
+ return {
172
+ fixed: false,
173
+ comment: body
174
+ };
175
+ }
176
+ logger.info(`Verifying fix with: ${verifyCommand}`);
177
+ const verifyResult = exec(verifyCommand);
178
+ if (verifyResult.ok) {
179
+ logger.success('Fix verified!');
180
+ commitAll('fix: auto-fix CI failure\n\nApplied by self-healing-cli via Copilot.');
181
+ push(fixBranch);
182
+ const prBody = buildPrBody({
183
+ baseBranch,
184
+ logSnippet: errorLog.slice(-2000)
185
+ });
186
+ const prUrl = createPullRequest({
187
+ title: `fix: auto-heal CI failure on ${baseBranch}`,
188
+ body: prBody,
189
+ base: baseBranch,
190
+ head: fixBranch
191
+ });
192
+ logger.success(`PR created: ${prUrl}`);
193
+ return {
194
+ fixed: true,
195
+ prUrl
196
+ };
197
+ }
198
+ logger.error('Fix did not pass verification');
199
+ const body = `## Self-Healing CLI\n\nCopilot suggested a fix but it did not pass verification (\`${verifyCommand}\`).\n\nPlease review manually.\n\n### Verification output\n\`\`\`\n${verifyResult.stdout + verifyResult.stderr}\n\`\`\``;
200
+ commentOnCommit({
201
+ sha: commitSha,
202
+ body
203
+ });
204
+ return {
205
+ fixed: false,
206
+ comment: body
207
+ };
208
+ };
209
+ const cli_args = process.argv.slice(2);
210
+ const cli_command = cli_args[0];
211
+ const getFlag = (name)=>{
212
+ const index = cli_args.indexOf(`--${name}`);
213
+ if (-1 === index || index + 1 >= cli_args.length) return;
214
+ return cli_args[index + 1];
215
+ };
216
+ const printHelp = ()=>{
217
+ console.log(`
218
+ self-healing-cli
219
+
220
+ Usage:
221
+ self-healing collect --command <cmd> --output <file>
222
+ self-healing heal --log <file> --verify <cmd> [--model <model>] [--tail <lines>]
223
+
224
+ Commands:
225
+ collect Run a command and save output to a log file
226
+ heal Feed log to Copilot CLI, verify fix, create PR or comment
227
+
228
+ Options:
229
+ --command Command to run (collect)
230
+ --output Output log file path (collect)
231
+ --log Input log file path (heal)
232
+ --verify Verification command (heal)
233
+ --model Copilot model to use (heal, optional)
234
+ --tail Number of log lines to send (heal, default: 100)
235
+ --help Show this help message
236
+ `);
237
+ };
238
+ if (!cli_command || '--help' === cli_command || '-h' === cli_command) {
239
+ printHelp();
240
+ process.exit(0);
241
+ }
242
+ if ('collect' === cli_command) {
243
+ const cmd = getFlag('command');
244
+ const output = getFlag('output');
245
+ if (!cmd || !output) {
246
+ logger.error('collect requires --command and --output');
247
+ process.exit(1);
248
+ }
249
+ const result = collect({
250
+ command: cmd,
251
+ outputFile: output
252
+ });
253
+ process.exit(result.exitCode);
254
+ }
255
+ if ('heal' === cli_command) {
256
+ const logFile = getFlag('log');
257
+ const verify = getFlag('verify');
258
+ if (!logFile || !verify) {
259
+ logger.error('heal requires --log and --verify');
260
+ process.exit(1);
261
+ }
262
+ const tailStr = getFlag('tail');
263
+ const result = heal({
264
+ logFile,
265
+ verifyCommand: verify,
266
+ model: getFlag('model'),
267
+ tailLines: tailStr ? Number(tailStr) : void 0
268
+ });
269
+ process.exit(result.fixed ? 0 : 1);
270
+ }
271
+ logger.error(`Unknown command: ${cli_command}`);
272
+ printHelp();
273
+ process.exit(1);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "self-healing-cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "self-healing": "./dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "devDependencies": {
12
+ "@biomejs/biome": "2.3.8",
13
+ "@rslib/core": "^0.19.5",
14
+ "@rstest/adapter-rslib": "^0.2.0",
15
+ "@rstest/core": "^0.8.1",
16
+ "@types/node": "^24.10.9",
17
+ "typescript": "^5.9.3"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "scripts": {
24
+ "build": "rslib build",
25
+ "check": "biome check --write",
26
+ "dev": "rslib build --watch",
27
+ "format": "biome format --write",
28
+ "test": "rstest",
29
+ "test:watch": "rstest --watch"
30
+ }
31
+ }