@slahon/lazykit 1.0.7 → 1.1.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.
- package/README.md +1 -1
- package/git.js +10 -1
- package/init.js +89 -29
- package/package.json +3 -3
- package/workflow.js +20 -7
package/README.md
CHANGED
package/git.js
CHANGED
|
@@ -45,6 +45,15 @@ function isGhCliAvailable() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isGhAuthenticated() {
|
|
49
|
+
try {
|
|
50
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
function detectStack() {
|
|
49
58
|
const cwd = process.cwd();
|
|
50
59
|
const stack = [];
|
|
@@ -82,4 +91,4 @@ function detectStack() {
|
|
|
82
91
|
return stack.length > 0 ? stack.join(' + ') : null;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable, detectStack };
|
|
94
|
+
module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable, isGhAuthenticated, detectStack };
|
package/init.js
CHANGED
|
@@ -8,7 +8,7 @@ const ora = require('ora');
|
|
|
8
8
|
|
|
9
9
|
const { log } = require('./logger');
|
|
10
10
|
const { ask, confirm, select } = require('./prompt');
|
|
11
|
-
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, detectStack } = require('./git');
|
|
11
|
+
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, isGhAuthenticated, detectStack } = require('./git');
|
|
12
12
|
const { generateWorkflow } = require('./workflow');
|
|
13
13
|
const { generateClaudeMd } = require('./claude-md');
|
|
14
14
|
|
|
@@ -43,31 +43,56 @@ async function init() {
|
|
|
43
43
|
process.exit(1);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// ─── Step 2: Check gh CLI and auth ──────────────────────────────────────
|
|
47
|
+
const ghAvailable = isGhCliAvailable();
|
|
48
|
+
let ghReady = false;
|
|
49
|
+
|
|
50
|
+
if (!ghAvailable) {
|
|
51
|
+
log.warn('GitHub CLI (gh) not found.');
|
|
52
|
+
console.log(chalk.gray('\n LazyKit uses gh to create labels, set secrets, and enable PR permissions.'));
|
|
53
|
+
console.log(chalk.gray(' Without it, those steps will be skipped and you will need to do them manually.\n'));
|
|
54
|
+
console.log(chalk.gray(' Install gh from: ') + chalk.cyan('https://cli.github.com'));
|
|
55
|
+
console.log(chalk.gray(' Then re-run: ') + chalk.cyan('npx @slahon/lazykit@latest init\n'));
|
|
56
|
+
const cont = await confirm('Continue without gh? (some steps will be manual)', false);
|
|
57
|
+
if (!cont) process.exit(0);
|
|
58
|
+
} else if (!isGhAuthenticated()) {
|
|
59
|
+
log.warn('GitHub CLI is not authenticated.');
|
|
60
|
+
console.log(chalk.gray('\n Run the following to log in, then re-run LazyKit:\n'));
|
|
61
|
+
console.log(chalk.cyan(' gh auth login'));
|
|
62
|
+
console.log(chalk.gray('\n Then re-run: ') + chalk.cyan('npx @slahon/lazykit@latest init\n'));
|
|
63
|
+
const cont = await confirm('Continue without gh auth? (some steps will be manual)', false);
|
|
64
|
+
if (!cont) process.exit(0);
|
|
65
|
+
} else {
|
|
66
|
+
ghReady = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
log.blank();
|
|
47
70
|
|
|
48
|
-
// ─── Step
|
|
71
|
+
// ─── Step 3: Ask questions ───────────────────────────────────────────────
|
|
49
72
|
log.title('Configure LazyKit');
|
|
50
73
|
log.blank();
|
|
51
74
|
|
|
52
75
|
const label = await ask('Label name to trigger Claude', 'lazykit');
|
|
53
76
|
|
|
77
|
+
const autoTrigger = await confirm('Trigger Claude on every new issue automatically? (no = only when you apply the label)', true);
|
|
78
|
+
|
|
54
79
|
const wantClaudeMd = await confirm('Generate CLAUDE.md project guide?', true);
|
|
55
80
|
|
|
56
81
|
log.blank();
|
|
57
82
|
|
|
58
|
-
// ─── Step
|
|
83
|
+
// ─── Step 4: Create workflow file ────────────────────────────────────────
|
|
59
84
|
const spinner = ora({ text: 'Creating workflow file...', color: 'cyan' }).start();
|
|
60
85
|
|
|
61
86
|
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
62
87
|
fs.mkdirSync(workflowDir, { recursive: true });
|
|
63
88
|
|
|
64
89
|
const workflowPath = path.join(workflowDir, 'lazykit.yml');
|
|
65
|
-
const workflowContent = generateWorkflow({ label });
|
|
90
|
+
const workflowContent = generateWorkflow({ label, autoTrigger });
|
|
66
91
|
fs.writeFileSync(workflowPath, workflowContent);
|
|
67
92
|
|
|
68
93
|
spinner.succeed(chalk.green('Created .github/workflows/lazykit.yml'));
|
|
69
94
|
|
|
70
|
-
// ─── Step
|
|
95
|
+
// ─── Step 5: Create CLAUDE.md ────────────────────────────────────────────
|
|
71
96
|
if (wantClaudeMd) {
|
|
72
97
|
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
73
98
|
const claudeMdContent = generateClaudeMd({ stack });
|
|
@@ -86,10 +111,8 @@ async function init() {
|
|
|
86
111
|
}
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
// ─── Step
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (ghAvailable && repoInfo) {
|
|
114
|
+
// ─── Step 6: Create GitHub label ─────────────────────────────────────────
|
|
115
|
+
if (ghReady) {
|
|
93
116
|
const labelSpinner = ora({ text: `Creating '${label}' label on GitHub...`, color: 'cyan' }).start();
|
|
94
117
|
try {
|
|
95
118
|
execSync(
|
|
@@ -106,11 +129,11 @@ async function init() {
|
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
} else {
|
|
109
|
-
log.warn(`
|
|
132
|
+
log.warn(`Create the '${label}' label manually at: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`)}`);
|
|
110
133
|
}
|
|
111
134
|
|
|
112
|
-
// ─── Step
|
|
113
|
-
if (
|
|
135
|
+
// ─── Step 7: Enable Actions to create PRs ────────────────────────────────
|
|
136
|
+
if (ghReady) {
|
|
114
137
|
const prSpinner = ora({ text: 'Enabling Actions PR creation permission...', color: 'cyan' }).start();
|
|
115
138
|
try {
|
|
116
139
|
execSync(
|
|
@@ -118,12 +141,20 @@ async function init() {
|
|
|
118
141
|
{ stdio: 'pipe' }
|
|
119
142
|
);
|
|
120
143
|
prSpinner.succeed(chalk.green('Enabled: Actions can create and approve pull requests'));
|
|
121
|
-
} catch {
|
|
122
|
-
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = err.stderr?.toString() || '';
|
|
146
|
+
if (msg.includes('403') || msg.includes('Must have admin rights')) {
|
|
147
|
+
prSpinner.warn(chalk.yellow('Permission denied — you need repo admin rights to change this setting.'));
|
|
148
|
+
console.log(chalk.gray(`\n Enable it manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)}`));
|
|
149
|
+
console.log(chalk.gray(' Actions → General → Workflow permissions → Enable "Allow GitHub Actions to create and approve pull requests"\n'));
|
|
150
|
+
} else {
|
|
151
|
+
prSpinner.warn(chalk.yellow('Could not enable PR creation automatically.'));
|
|
152
|
+
console.log(chalk.gray(` Enable manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)}\n`));
|
|
153
|
+
}
|
|
123
154
|
}
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
// ─── Step
|
|
157
|
+
// ─── Step 8: Generate and set Claude token ───────────────────────────────
|
|
127
158
|
console.log('\n' + chalk.bold.green(' ✨ Almost there — setting up your Claude token...\n'));
|
|
128
159
|
|
|
129
160
|
let tokenSet = false;
|
|
@@ -133,14 +164,16 @@ async function init() {
|
|
|
133
164
|
console.log();
|
|
134
165
|
|
|
135
166
|
const result = spawnSync('claude', ['setup-token'], {
|
|
136
|
-
stdio: ['inherit', 'pipe', '
|
|
167
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
137
168
|
encoding: 'utf8',
|
|
138
169
|
});
|
|
139
170
|
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
172
|
+
|
|
173
|
+
const output = (result.stdout || '') + (result.stderr || '');
|
|
174
|
+
const tokenMatch = output.match(/sk-ant-oat[^\s]+/);
|
|
142
175
|
|
|
143
|
-
if (tokenMatch &&
|
|
176
|
+
if (tokenMatch && ghReady) {
|
|
144
177
|
const token = tokenMatch[0];
|
|
145
178
|
const secretSpinner = ora({ text: 'Adding CLAUDE_CODE_OAUTH_TOKEN to GitHub secrets...', color: 'cyan' }).start();
|
|
146
179
|
execSync(
|
|
@@ -149,7 +182,7 @@ async function init() {
|
|
|
149
182
|
);
|
|
150
183
|
secretSpinner.succeed(chalk.green('CLAUDE_CODE_OAUTH_TOKEN added to GitHub secrets'));
|
|
151
184
|
tokenSet = true;
|
|
152
|
-
} else if (tokenMatch && !
|
|
185
|
+
} else if (tokenMatch && !ghReady) {
|
|
153
186
|
log.warn('Token generated but gh CLI not found — add it manually:');
|
|
154
187
|
console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
|
|
155
188
|
console.log(chalk.gray(' Value: ') + chalk.cyan(tokenMatch[0]));
|
|
@@ -157,19 +190,46 @@ async function init() {
|
|
|
157
190
|
} else {
|
|
158
191
|
throw new Error('Token not found in output');
|
|
159
192
|
}
|
|
160
|
-
} catch {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const isNotFound = err.status === 'ENOENT' || (err.message || '').includes('ENOENT');
|
|
195
|
+
if (isNotFound) {
|
|
196
|
+
log.warn('Claude Code CLI not found — install it first:');
|
|
197
|
+
console.log(chalk.cyan('\n npm install -g @anthropic-ai/claude-code'));
|
|
198
|
+
} else {
|
|
199
|
+
log.warn('Could not set token automatically.');
|
|
200
|
+
}
|
|
201
|
+
console.log(chalk.gray('\n Add it manually:'));
|
|
202
|
+
console.log(chalk.gray(' 1. Run: ') + chalk.cyan('claude setup-token'));
|
|
203
|
+
console.log(chalk.gray(' 2. Go to: ') + chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
|
|
204
|
+
console.log(chalk.gray(' 3. Add secret:') + chalk.cyan(' CLAUDE_CODE_OAUTH_TOKEN') + chalk.gray(' = the token you copied\n'));
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
log.divider();
|
|
168
208
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
209
|
+
// ─── Step 9: Commit and push ──────────────────────────────────────────────
|
|
210
|
+
const autoPush = await confirm('Commit and push the generated files now?', true);
|
|
211
|
+
|
|
212
|
+
if (autoPush) {
|
|
213
|
+
const pushSpinner = ora({ text: 'Committing and pushing...', color: 'cyan' }).start();
|
|
214
|
+
try {
|
|
215
|
+
const files = ['.github/workflows/lazykit.yml', ...(wantClaudeMd ? ['CLAUDE.md'] : [])].join(' ');
|
|
216
|
+
execSync(`git add ${files}`, { stdio: 'pipe' });
|
|
217
|
+
execSync(`git commit -m "Add LazyKit automation"`, { stdio: 'pipe' });
|
|
218
|
+
execSync(`git push`, { stdio: 'pipe' });
|
|
219
|
+
pushSpinner.succeed(chalk.green('Committed and pushed — workflow is live'));
|
|
220
|
+
} catch (err) {
|
|
221
|
+
pushSpinner.fail(chalk.red('Push failed.'));
|
|
222
|
+
console.log(chalk.gray('\n Push it manually:\n'));
|
|
223
|
+
console.log(chalk.cyan(' git add .github/workflows/lazykit.yml' + (wantClaudeMd ? ' CLAUDE.md' : '')));
|
|
224
|
+
console.log(chalk.cyan(' git commit -m "Add LazyKit automation"'));
|
|
225
|
+
console.log(chalk.cyan(' git push\n'));
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
console.log(chalk.bold('\n Push the files yourself when ready:\n'));
|
|
229
|
+
console.log(chalk.cyan(' git add .github/workflows/lazykit.yml' + (wantClaudeMd ? ' CLAUDE.md' : '')));
|
|
230
|
+
console.log(chalk.cyan(' git commit -m "Add LazyKit automation"'));
|
|
231
|
+
console.log(chalk.cyan(' git push'));
|
|
232
|
+
}
|
|
173
233
|
|
|
174
234
|
log.divider();
|
|
175
235
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slahon/lazykit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Drop an issue, get a PR. AI-powered issue-to-PR automation using Claude.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lazykit": "./index.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"start": "node
|
|
9
|
+
"start": "node index.js"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"claude",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"cli",
|
|
17
17
|
"issue-to-pr"
|
|
18
18
|
],
|
|
19
|
-
"author": "",
|
|
19
|
+
"author": "slahon",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"chalk": "^4.1.2",
|
package/workflow.js
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function generateWorkflow({ label }) {
|
|
3
|
+
function generateWorkflow({ label, autoTrigger }) {
|
|
4
4
|
const checkList = ` 1. Review your changes and make sure no existing functionality is broken.
|
|
5
5
|
2. If you encounter errors you cannot fix, post a comment on the issue explaining what went wrong instead of committing broken code.`;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const trigger = autoTrigger
|
|
8
|
+
? `on:
|
|
9
|
+
issues:
|
|
10
|
+
types: [opened]
|
|
11
|
+
issue_comment:
|
|
12
|
+
types: [created]`
|
|
13
|
+
: `on:
|
|
10
14
|
issues:
|
|
11
15
|
types: [opened, labeled]
|
|
12
16
|
issue_comment:
|
|
13
|
-
types: [created]
|
|
17
|
+
types: [created]`;
|
|
18
|
+
|
|
19
|
+
const condition = autoTrigger
|
|
20
|
+
? `github.event.action == 'opened' ||
|
|
21
|
+
contains(github.event.comment.body, '@claude')`
|
|
22
|
+
: `contains(github.event.issue.labels.*.name, '${label}') ||
|
|
23
|
+
contains(github.event.comment.body, '@claude')`;
|
|
24
|
+
|
|
25
|
+
return `name: LazyKit
|
|
26
|
+
|
|
27
|
+
${trigger}
|
|
14
28
|
|
|
15
29
|
concurrency:
|
|
16
30
|
group: lazykit-\${{ github.event.issue.number }}
|
|
@@ -19,8 +33,7 @@ concurrency:
|
|
|
19
33
|
jobs:
|
|
20
34
|
lazykit:
|
|
21
35
|
if: >
|
|
22
|
-
|
|
23
|
-
contains(github.event.comment.body, '@claude')
|
|
36
|
+
${condition}
|
|
24
37
|
runs-on: ubuntu-latest
|
|
25
38
|
timeout-minutes: 30
|
|
26
39
|
permissions:
|