@slahon/lazykit 1.1.1 ā 1.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.
- package/git.js +10 -1
- package/index.js +32 -4
- package/init.js +137 -98
- package/issue-template.js +24 -0
- package/package.json +1 -1
- package/remove.js +104 -0
- package/status.js +113 -0
- package/update.js +89 -0
package/git.js
CHANGED
|
@@ -91,4 +91,13 @@ function detectStack() {
|
|
|
91
91
|
return stack.length > 0 ? stack.join(' + ') : null;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
function hasBranchProtection({ owner, repo }) {
|
|
95
|
+
try {
|
|
96
|
+
execSync(`gh api repos/${owner}/${repo}/branches/main/protection`, { stdio: 'pipe' });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable, isGhAuthenticated, detectStack, hasBranchProtection };
|
package/index.js
CHANGED
|
@@ -3,14 +3,36 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const { init } = require('./init');
|
|
6
|
+
const { update } = require('./update');
|
|
7
|
+
const { status } = require('./status');
|
|
8
|
+
const { remove } = require('./remove');
|
|
6
9
|
|
|
7
|
-
const [,, command] = process.argv;
|
|
10
|
+
const [,, command, ...args] = process.argv;
|
|
11
|
+
const dryRun = args.includes('--dry-run');
|
|
8
12
|
|
|
9
13
|
switch (command) {
|
|
10
14
|
case 'init':
|
|
11
15
|
case undefined:
|
|
12
|
-
init().catch(err => {
|
|
13
|
-
console.error('\n
|
|
16
|
+
init({ dryRun }).catch(err => {
|
|
17
|
+
console.error('\n Setup failed:', err.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
break;
|
|
21
|
+
case 'update':
|
|
22
|
+
update({ dryRun }).catch(err => {
|
|
23
|
+
console.error('\n Update failed:', err.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
break;
|
|
27
|
+
case 'status':
|
|
28
|
+
status().catch(err => {
|
|
29
|
+
console.error('\n Status check failed:', err.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
break;
|
|
33
|
+
case 'remove':
|
|
34
|
+
remove().catch(err => {
|
|
35
|
+
console.error('\n Remove failed:', err.message);
|
|
14
36
|
process.exit(1);
|
|
15
37
|
});
|
|
16
38
|
break;
|
|
@@ -19,6 +41,12 @@ switch (command) {
|
|
|
19
41
|
𦄠LazyKit ā Drop an issue, get a PR.
|
|
20
42
|
|
|
21
43
|
Usage:
|
|
22
|
-
npx lazykit init
|
|
44
|
+
npx @slahon/lazykit@latest init Set up LazyKit in your current GitHub repo
|
|
45
|
+
npx @slahon/lazykit@latest update Regenerate workflow and CLAUDE.md
|
|
46
|
+
npx @slahon/lazykit@latest status Check if everything is wired up correctly
|
|
47
|
+
npx @slahon/lazykit@latest remove Remove LazyKit from your repo
|
|
48
|
+
|
|
49
|
+
Flags:
|
|
50
|
+
--dry-run Preview what would happen without writing files (init, update)
|
|
23
51
|
`);
|
|
24
52
|
}
|
package/init.js
CHANGED
|
@@ -7,28 +7,30 @@ const chalk = require('chalk');
|
|
|
7
7
|
const ora = require('ora');
|
|
8
8
|
|
|
9
9
|
const { log } = require('./logger');
|
|
10
|
-
const { ask, confirm
|
|
11
|
-
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, isGhAuthenticated, detectStack } = require('./git');
|
|
10
|
+
const { ask, confirm } = require('./prompt');
|
|
11
|
+
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, isGhAuthenticated, detectStack, hasBranchProtection } = require('./git');
|
|
12
12
|
const { generateWorkflow } = require('./workflow');
|
|
13
13
|
const { generateClaudeMd } = require('./claude-md');
|
|
14
|
+
const { generateIssueTemplate } = require('./issue-template');
|
|
15
|
+
|
|
16
|
+
async function init({ dryRun = false } = {}) {
|
|
17
|
+
if (dryRun) console.log(chalk.yellow('\n [dry-run] Preview mode ā no files will be written or API calls made.\n'));
|
|
14
18
|
|
|
15
|
-
async function init() {
|
|
16
19
|
console.log(chalk.bold.white('\n𦄠LazyKit ā Drop an issue, get a PR.\n'));
|
|
17
20
|
console.log(chalk.gray(' This will set up AI-powered issue-to-PR automation in your repo.\n'));
|
|
18
21
|
|
|
19
|
-
// āāā Step 1: Verify git repo
|
|
22
|
+
// āāā Step 1: Verify git repo āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
20
23
|
if (!isGitRepo()) {
|
|
21
24
|
log.error('Not inside a git repository.');
|
|
22
25
|
console.log(chalk.gray('\n LazyKit needs a git repo with a GitHub remote. To set one up:\n'));
|
|
23
26
|
console.log(chalk.cyan(' git init'));
|
|
24
27
|
console.log(chalk.cyan(' git remote add origin https://github.com/<you>/<repo>'));
|
|
25
|
-
console.log(chalk.gray('\n Then re-run: ') + chalk.cyan('npx @slahon/lazykit init\n'));
|
|
28
|
+
console.log(chalk.gray('\n Then re-run: ') + chalk.cyan('npx @slahon/lazykit@latest init\n'));
|
|
26
29
|
process.exit(1);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const remote = getGitRemote();
|
|
30
33
|
const repoInfo = parseGitHubRepo(remote);
|
|
31
|
-
|
|
32
34
|
const stack = detectStack();
|
|
33
35
|
|
|
34
36
|
if (repoInfo) {
|
|
@@ -39,11 +41,11 @@ async function init() {
|
|
|
39
41
|
console.log(chalk.gray('\n LazyKit requires a GitHub repository with a remote set up.\n'));
|
|
40
42
|
console.log(chalk.gray(' To fix this, connect your repo to GitHub first:\n'));
|
|
41
43
|
console.log(chalk.cyan(' git remote add origin https://github.com/<you>/<repo>'));
|
|
42
|
-
console.log(chalk.gray('\n Then re-run: ') + chalk.cyan('npx @slahon/lazykit init\n'));
|
|
44
|
+
console.log(chalk.gray('\n Then re-run: ') + chalk.cyan('npx @slahon/lazykit@latest init\n'));
|
|
43
45
|
process.exit(1);
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
// āāā Step 2: Check gh CLI and auth
|
|
48
|
+
// āāā Step 2: Check gh CLI and auth āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
47
49
|
const ghAvailable = isGhCliAvailable();
|
|
48
50
|
let ghReady = false;
|
|
49
51
|
|
|
@@ -68,51 +70,66 @@ async function init() {
|
|
|
68
70
|
|
|
69
71
|
log.blank();
|
|
70
72
|
|
|
71
|
-
// āāā Step 3: Ask questions
|
|
73
|
+
// āāā Step 3: Ask questions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
72
74
|
log.title('Configure LazyKit');
|
|
73
75
|
log.blank();
|
|
74
76
|
|
|
75
77
|
const label = await ask('Label name to trigger Claude', 'lazykit');
|
|
76
|
-
|
|
77
78
|
const autoTrigger = await confirm('Trigger Claude on every new issue automatically? (no = only when you apply the label)', true);
|
|
78
|
-
|
|
79
79
|
const wantClaudeMd = await confirm('Generate CLAUDE.md project guide?', true);
|
|
80
80
|
|
|
81
81
|
log.blank();
|
|
82
82
|
|
|
83
|
-
// āāā Step 4: Create workflow file
|
|
84
|
-
const spinner = ora({ text: 'Creating workflow file...', color: 'cyan' }).start();
|
|
85
|
-
|
|
83
|
+
// āāā Step 4: Create workflow file āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
86
84
|
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
87
|
-
fs.mkdirSync(workflowDir, { recursive: true });
|
|
88
|
-
|
|
89
85
|
const workflowPath = path.join(workflowDir, 'lazykit.yml');
|
|
90
|
-
const workflowContent = generateWorkflow({ label, autoTrigger });
|
|
91
|
-
fs.writeFileSync(workflowPath, workflowContent);
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
log.info('[dry-run] Would create: .github/workflows/lazykit.yml');
|
|
89
|
+
} else {
|
|
90
|
+
const spinner = ora({ text: 'Creating workflow file...', color: 'cyan' }).start();
|
|
91
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(workflowPath, generateWorkflow({ label, autoTrigger }));
|
|
93
|
+
spinner.succeed(chalk.green('Created .github/workflows/lazykit.yml'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// āāā Step 5: Create issue template āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
97
|
+
const templateDir = path.join(process.cwd(), '.github', 'ISSUE_TEMPLATE');
|
|
98
|
+
const templatePath = path.join(templateDir, 'lazykit.md');
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
log.info('[dry-run] Would create: .github/ISSUE_TEMPLATE/lazykit.md');
|
|
102
|
+
} else {
|
|
103
|
+
const spinner = ora({ text: 'Creating issue template...', color: 'cyan' }).start();
|
|
104
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(templatePath, generateIssueTemplate({ label }));
|
|
106
|
+
spinner.succeed(chalk.green('Created .github/ISSUE_TEMPLATE/lazykit.md'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// āāā Step 6: Create CLAUDE.md āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
96
110
|
if (wantClaudeMd) {
|
|
97
111
|
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
98
|
-
const claudeMdContent = generateClaudeMd({ stack });
|
|
99
112
|
|
|
100
|
-
if (
|
|
113
|
+
if (dryRun) {
|
|
114
|
+
log.info('[dry-run] Would create: CLAUDE.md');
|
|
115
|
+
} else if (fs.existsSync(claudeMdPath)) {
|
|
101
116
|
const overwrite = await confirm('CLAUDE.md already exists. Overwrite?', false);
|
|
102
117
|
if (overwrite) {
|
|
103
|
-
fs.writeFileSync(claudeMdPath,
|
|
118
|
+
fs.writeFileSync(claudeMdPath, generateClaudeMd({ stack }));
|
|
104
119
|
log.success('Updated CLAUDE.md');
|
|
105
120
|
} else {
|
|
106
121
|
log.warn('Skipped CLAUDE.md ā keeping existing file.');
|
|
107
122
|
}
|
|
108
123
|
} else {
|
|
109
|
-
fs.writeFileSync(claudeMdPath,
|
|
124
|
+
fs.writeFileSync(claudeMdPath, generateClaudeMd({ stack }));
|
|
110
125
|
log.success('Created CLAUDE.md');
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
// āāā Step
|
|
115
|
-
if (
|
|
129
|
+
// āāā Step 7: Create GitHub label āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
log.info(`[dry-run] Would create GitHub label: '${label}'`);
|
|
132
|
+
} else if (ghReady) {
|
|
116
133
|
const labelSpinner = ora({ text: `Creating '${label}' label on GitHub...`, color: 'cyan' }).start();
|
|
117
134
|
try {
|
|
118
135
|
execSync(
|
|
@@ -125,15 +142,17 @@ async function init() {
|
|
|
125
142
|
if (msg.includes('already exists')) {
|
|
126
143
|
labelSpinner.succeed(chalk.green(`Label '${label}' already exists ā skipping`));
|
|
127
144
|
} else {
|
|
128
|
-
labelSpinner.warn(chalk.yellow(`Could not create label
|
|
145
|
+
labelSpinner.warn(chalk.yellow(`Could not create label ā create it manually at https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`));
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
} else {
|
|
132
149
|
log.warn(`Create the '${label}' label manually at: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`)}`);
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
// āāā Step
|
|
136
|
-
if (
|
|
152
|
+
// āāā Step 8: Enable Actions PR creation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
153
|
+
if (dryRun) {
|
|
154
|
+
log.info('[dry-run] Would enable Actions PR creation permission');
|
|
155
|
+
} else if (ghReady) {
|
|
137
156
|
const prSpinner = ora({ text: 'Enabling Actions PR creation permission...', color: 'cyan' }).start();
|
|
138
157
|
try {
|
|
139
158
|
execSync(
|
|
@@ -154,89 +173,109 @@ async function init() {
|
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
|
|
157
|
-
// āāā Step
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
log.info('Running claude setup-token (a browser window may open to authenticate)...');
|
|
164
|
-
console.log();
|
|
165
|
-
|
|
166
|
-
const result = spawnSync('claude', ['setup-token'], {
|
|
167
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
168
|
-
encoding: 'utf8',
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
176
|
+
// āāā Step 9: Branch protection warning āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
177
|
+
if (!dryRun && ghReady && hasBranchProtection(repoInfo)) {
|
|
178
|
+
log.warn('Branch protection is enabled on main.');
|
|
179
|
+
console.log(chalk.gray(" Claude's PRs will be opened but cannot be auto-merged ā they require manual review.\n"));
|
|
180
|
+
}
|
|
172
181
|
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
// āāā Step 10: Generate and set Claude token āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
183
|
+
if (dryRun) {
|
|
184
|
+
log.info('[dry-run] Would run claude setup-token and set CLAUDE_CODE_OAUTH_TOKEN secret');
|
|
185
|
+
} else {
|
|
186
|
+
console.log('\n' + chalk.bold.green(' ⨠Almost there ā setting up your Claude token...\n'));
|
|
175
187
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
188
|
+
try {
|
|
189
|
+
log.info('Running claude setup-token (a browser window may open to authenticate)...');
|
|
190
|
+
console.log();
|
|
191
|
+
|
|
192
|
+
const result = spawnSync('claude', ['setup-token'], {
|
|
193
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
194
|
+
encoding: 'utf8',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
198
|
+
|
|
199
|
+
const output = (result.stdout || '') + (result.stderr || '');
|
|
200
|
+
const tokenMatch = output.match(/sk-ant-oat[^\s]+/);
|
|
201
|
+
|
|
202
|
+
if (tokenMatch && ghReady) {
|
|
203
|
+
const secretSpinner = ora({ text: 'Adding CLAUDE_CODE_OAUTH_TOKEN to GitHub secrets...', color: 'cyan' }).start();
|
|
204
|
+
execSync(
|
|
205
|
+
`gh secret set CLAUDE_CODE_OAUTH_TOKEN --body "${tokenMatch[0]}" --repo ${repoInfo.owner}/${repoInfo.repo}`,
|
|
206
|
+
{ stdio: 'pipe' }
|
|
207
|
+
);
|
|
208
|
+
secretSpinner.succeed(chalk.green('CLAUDE_CODE_OAUTH_TOKEN added to GitHub secrets'));
|
|
209
|
+
} else if (tokenMatch && !ghReady) {
|
|
210
|
+
log.warn('Token generated but gh CLI not available ā add it manually:');
|
|
211
|
+
console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
|
|
212
|
+
console.log(chalk.gray(' Value: ') + chalk.cyan(tokenMatch[0]));
|
|
213
|
+
console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
|
|
214
|
+
} else {
|
|
215
|
+
throw new Error('Token not found in output');
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const isNotFound = (err.message || '').includes('ENOENT');
|
|
219
|
+
if (isNotFound) {
|
|
220
|
+
log.warn('Claude Code CLI not found ā install it first:');
|
|
221
|
+
console.log(chalk.cyan('\n npm install -g @anthropic-ai/claude-code'));
|
|
222
|
+
} else {
|
|
223
|
+
log.warn('Could not set token automatically.');
|
|
224
|
+
}
|
|
225
|
+
console.log(chalk.gray('\n Add it manually:'));
|
|
226
|
+
console.log(chalk.gray(' 1. Run: ') + chalk.cyan('claude setup-token'));
|
|
227
|
+
console.log(chalk.gray(' 2. Go to: ') + chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
|
|
228
|
+
console.log(chalk.gray(' 3. Add secret: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN') + chalk.gray(' = the token you copied\n'));
|
|
200
229
|
}
|
|
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'));
|
|
205
230
|
}
|
|
206
231
|
|
|
207
232
|
log.divider();
|
|
208
233
|
|
|
209
|
-
// āāā Step
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
// āāā Step 11: Commit and push āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
235
|
+
if (dryRun) {
|
|
236
|
+
log.info('[dry-run] Would commit and push generated files');
|
|
237
|
+
log.blank();
|
|
238
|
+
} else {
|
|
239
|
+
const autoPush = await confirm('Commit and push the generated files now?', true);
|
|
240
|
+
|
|
241
|
+
if (autoPush) {
|
|
242
|
+
const pushSpinner = ora({ text: 'Committing and pushing...', color: 'cyan' }).start();
|
|
243
|
+
try {
|
|
244
|
+
const files = [
|
|
245
|
+
'.github/workflows/lazykit.yml',
|
|
246
|
+
'.github/ISSUE_TEMPLATE/lazykit.md',
|
|
247
|
+
...(wantClaudeMd ? ['CLAUDE.md'] : []),
|
|
248
|
+
].join(' ');
|
|
249
|
+
execSync(`git add ${files}`, { stdio: 'pipe' });
|
|
250
|
+
execSync(`git commit -m "Add LazyKit automation"`, { stdio: 'pipe' });
|
|
251
|
+
execSync(`git push`, { stdio: 'pipe' });
|
|
252
|
+
pushSpinner.succeed(chalk.green('Committed and pushed ā workflow is live'));
|
|
253
|
+
} catch {
|
|
254
|
+
pushSpinner.fail(chalk.red('Push failed.'));
|
|
255
|
+
console.log(chalk.gray('\n Push it manually:\n'));
|
|
256
|
+
console.log(chalk.cyan(` git add .github/workflows/lazykit.yml .github/ISSUE_TEMPLATE/lazykit.md${wantClaudeMd ? ' CLAUDE.md' : ''}`));
|
|
257
|
+
console.log(chalk.cyan(' git commit -m "Add LazyKit automation"'));
|
|
258
|
+
console.log(chalk.cyan(' git push\n'));
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
console.log(chalk.bold('\n Push the files yourself when ready:\n'));
|
|
262
|
+
console.log(chalk.cyan(` git add .github/workflows/lazykit.yml .github/ISSUE_TEMPLATE/lazykit.md${wantClaudeMd ? ' CLAUDE.md' : ''}`));
|
|
224
263
|
console.log(chalk.cyan(' git commit -m "Add LazyKit automation"'));
|
|
225
|
-
console.log(chalk.cyan(' git push
|
|
264
|
+
console.log(chalk.cyan(' git push'));
|
|
226
265
|
}
|
|
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
266
|
}
|
|
233
267
|
|
|
234
268
|
log.divider();
|
|
235
269
|
|
|
236
270
|
console.log(chalk.bold('\n Done! Here is how to use it:\n'));
|
|
237
|
-
console.log(` ${chalk.gray('1.')} Open a GitHub issue
|
|
238
|
-
|
|
239
|
-
|
|
271
|
+
console.log(` ${chalk.gray('1.')} Open a GitHub issue using the ${chalk.bold.magenta('LazyKit Task')} template`);
|
|
272
|
+
if (autoTrigger) {
|
|
273
|
+
console.log(` ${chalk.gray('2.')} Claude starts working on it automatically`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(` ${chalk.gray('2.')} Apply the ${chalk.bold.magenta(label)} label to trigger Claude`);
|
|
276
|
+
}
|
|
277
|
+
console.log(` ${chalk.gray('3.')} Review and merge the pull request`);
|
|
278
|
+
console.log(`\n ${chalk.gray('Tip:')} Mention ${chalk.bold.cyan('@claude')} in any issue comment to give follow-up instructions.\n`);
|
|
240
279
|
|
|
241
280
|
console.log(chalk.bold.white(' 𦄠LazyKit is ready. Go be lazy.\n'));
|
|
242
281
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function generateIssueTemplate({ label }) {
|
|
4
|
+
return `---
|
|
5
|
+
name: LazyKit Task
|
|
6
|
+
about: Describe a task for Claude to implement
|
|
7
|
+
labels: ${label}
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What to build
|
|
11
|
+
|
|
12
|
+
<!-- Describe clearly and specifically what you want Claude to implement. Keep it small and focused. -->
|
|
13
|
+
|
|
14
|
+
## Acceptance criteria
|
|
15
|
+
|
|
16
|
+
<!-- Optional: what does "done" look like? -->
|
|
17
|
+
|
|
18
|
+
## Notes
|
|
19
|
+
|
|
20
|
+
<!-- Files to touch, things to avoid, constraints, or extra context for Claude. -->
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { generateIssueTemplate };
|
package/package.json
CHANGED
package/remove.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
const { log } = require('./logger');
|
|
10
|
+
const { confirm } = require('./prompt');
|
|
11
|
+
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, isGhAuthenticated } = require('./git');
|
|
12
|
+
|
|
13
|
+
async function remove() {
|
|
14
|
+
console.log(chalk.bold.white('\n𦄠LazyKit Remove\n'));
|
|
15
|
+
|
|
16
|
+
if (!isGitRepo()) {
|
|
17
|
+
log.error('Not inside a git repository.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const repoInfo = parseGitHubRepo(getGitRemote());
|
|
22
|
+
if (!repoInfo) {
|
|
23
|
+
log.error('No GitHub remote detected.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const confirmed = await confirm('This will remove LazyKit from your repo. Continue?', false);
|
|
28
|
+
if (!confirmed) {
|
|
29
|
+
console.log(chalk.gray('\n Aborted.\n'));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ghReady = isGhCliAvailable() && isGhAuthenticated();
|
|
34
|
+
const deleted = [];
|
|
35
|
+
|
|
36
|
+
// Read label name before deleting workflow
|
|
37
|
+
let labelName = 'lazykit';
|
|
38
|
+
const workflowPath = path.join(process.cwd(), '.github', 'workflows', 'lazykit.yml');
|
|
39
|
+
if (fs.existsSync(workflowPath)) {
|
|
40
|
+
const wf = fs.readFileSync(workflowPath, 'utf8');
|
|
41
|
+
const m = wf.match(/label_trigger:\s*(\S+)/);
|
|
42
|
+
if (m) labelName = m[1];
|
|
43
|
+
fs.unlinkSync(workflowPath);
|
|
44
|
+
deleted.push('.github/workflows/lazykit.yml');
|
|
45
|
+
log.success('Removed .github/workflows/lazykit.yml');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const templatePath = path.join(process.cwd(), '.github', 'ISSUE_TEMPLATE', 'lazykit.md');
|
|
49
|
+
if (fs.existsSync(templatePath)) {
|
|
50
|
+
fs.unlinkSync(templatePath);
|
|
51
|
+
deleted.push('.github/ISSUE_TEMPLATE/lazykit.md');
|
|
52
|
+
log.success('Removed .github/ISSUE_TEMPLATE/lazykit.md');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
56
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
57
|
+
const removeClaude = await confirm('Remove CLAUDE.md too?', false);
|
|
58
|
+
if (removeClaude) {
|
|
59
|
+
fs.unlinkSync(claudeMdPath);
|
|
60
|
+
deleted.push('CLAUDE.md');
|
|
61
|
+
log.success('Removed CLAUDE.md');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (ghReady) {
|
|
66
|
+
const labelSpinner = ora({ text: `Deleting '${labelName}' label from GitHub...`, color: 'cyan' }).start();
|
|
67
|
+
try {
|
|
68
|
+
execSync(`gh label delete "${labelName}" --repo ${repoInfo.owner}/${repoInfo.repo} --yes`, { stdio: 'pipe' });
|
|
69
|
+
labelSpinner.succeed(chalk.green(`Deleted '${labelName}' label from GitHub`));
|
|
70
|
+
} catch {
|
|
71
|
+
labelSpinner.warn(`Could not delete label ā remove manually at ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const secretSpinner = ora({ text: 'Removing CLAUDE_CODE_OAUTH_TOKEN secret...', color: 'cyan' }).start();
|
|
75
|
+
try {
|
|
76
|
+
execSync(`gh secret delete CLAUDE_CODE_OAUTH_TOKEN --repo ${repoInfo.owner}/${repoInfo.repo}`, { stdio: 'pipe' });
|
|
77
|
+
secretSpinner.succeed(chalk.green('Removed CLAUDE_CODE_OAUTH_TOKEN secret'));
|
|
78
|
+
} catch {
|
|
79
|
+
secretSpinner.warn(`Could not remove secret ā delete manually in repo Settings ā Secrets`);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
log.warn(`Remove the '${labelName}' label manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`)}`);
|
|
83
|
+
log.warn(`Remove the CLAUDE_CODE_OAUTH_TOKEN secret manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (deleted.length > 0) {
|
|
87
|
+
const autoPush = await confirm('Commit and push the removals?', true);
|
|
88
|
+
if (autoPush) {
|
|
89
|
+
const spinner = ora({ text: 'Committing and pushing...', color: 'cyan' }).start();
|
|
90
|
+
try {
|
|
91
|
+
execSync(`git add ${deleted.map(f => `"${f}"`).join(' ')}`, { stdio: 'pipe' });
|
|
92
|
+
execSync(`git commit -m "Remove LazyKit automation"`, { stdio: 'pipe' });
|
|
93
|
+
execSync(`git push`, { stdio: 'pipe' });
|
|
94
|
+
spinner.succeed(chalk.green('Committed and pushed'));
|
|
95
|
+
} catch {
|
|
96
|
+
spinner.fail('Push failed ā commit and push manually');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.bold.white('\n LazyKit has been removed.\n'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { remove };
|
package/status.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
const { log } = require('./logger');
|
|
9
|
+
const { isGitRepo, getGitRemote, parseGitHubRepo, isGhCliAvailable, isGhAuthenticated } = require('./git');
|
|
10
|
+
|
|
11
|
+
async function status() {
|
|
12
|
+
console.log(chalk.bold.white('\n𦄠LazyKit Status\n'));
|
|
13
|
+
|
|
14
|
+
if (!isGitRepo()) {
|
|
15
|
+
log.error('Not inside a git repository.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const repoInfo = parseGitHubRepo(getGitRemote());
|
|
20
|
+
if (!repoInfo) {
|
|
21
|
+
log.error('No GitHub remote detected.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
log.info(`Repo: ${chalk.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
|
|
26
|
+
log.blank();
|
|
27
|
+
|
|
28
|
+
// Local files
|
|
29
|
+
const workflowPath = path.join(process.cwd(), '.github', 'workflows', 'lazykit.yml');
|
|
30
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
31
|
+
const templatePath = path.join(process.cwd(), '.github', 'ISSUE_TEMPLATE', 'lazykit.md');
|
|
32
|
+
|
|
33
|
+
fs.existsSync(workflowPath)
|
|
34
|
+
? log.success('Workflow file found')
|
|
35
|
+
: log.error('Workflow file missing ā run npx @slahon/lazykit@latest init');
|
|
36
|
+
|
|
37
|
+
fs.existsSync(templatePath)
|
|
38
|
+
? log.success('Issue template found')
|
|
39
|
+
: log.warn('Issue template missing ā run npx @slahon/lazykit@latest init');
|
|
40
|
+
|
|
41
|
+
fs.existsSync(claudeMdPath)
|
|
42
|
+
? log.success('CLAUDE.md found')
|
|
43
|
+
: log.warn('CLAUDE.md not found ā Claude has no project context');
|
|
44
|
+
|
|
45
|
+
// Remote checks require gh
|
|
46
|
+
const ghReady = isGhCliAvailable() && isGhAuthenticated();
|
|
47
|
+
if (!ghReady) {
|
|
48
|
+
log.blank();
|
|
49
|
+
log.warn('gh CLI not available or not authenticated ā skipping remote checks');
|
|
50
|
+
log.warn('Run: ' + chalk.cyan('gh auth login'));
|
|
51
|
+
console.log();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log.blank();
|
|
56
|
+
|
|
57
|
+
// Secret
|
|
58
|
+
try {
|
|
59
|
+
const secrets = execSync(`gh secret list --repo ${repoInfo.owner}/${repoInfo.repo}`, { stdio: 'pipe' }).toString();
|
|
60
|
+
const match = secrets.match(/CLAUDE_CODE_OAUTH_TOKEN\s+(\S+)/);
|
|
61
|
+
if (match) {
|
|
62
|
+
const updated = new Date(match[1]);
|
|
63
|
+
const monthsOld = (Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24 * 30);
|
|
64
|
+
log.success(`CLAUDE_CODE_OAUTH_TOKEN secret exists ${chalk.gray(`(updated ${match[1]})`)}`);
|
|
65
|
+
if (monthsOld > 6) log.warn('Token is over 6 months old ā consider refreshing it with npx @slahon/lazykit@latest init');
|
|
66
|
+
} else {
|
|
67
|
+
log.error('CLAUDE_CODE_OAUTH_TOKEN secret not found ā run npx @slahon/lazykit@latest init');
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
log.warn('Could not check secrets ā insufficient permissions');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Label
|
|
74
|
+
try {
|
|
75
|
+
let labelName = 'lazykit';
|
|
76
|
+
if (fs.existsSync(workflowPath)) {
|
|
77
|
+
const wf = fs.readFileSync(workflowPath, 'utf8');
|
|
78
|
+
const m = wf.match(/label_trigger:\s*(\S+)/);
|
|
79
|
+
if (m) labelName = m[1];
|
|
80
|
+
}
|
|
81
|
+
const labels = execSync(`gh label list --repo ${repoInfo.owner}/${repoInfo.repo}`, { stdio: 'pipe' }).toString();
|
|
82
|
+
labels.includes(labelName)
|
|
83
|
+
? log.success(`Label '${labelName}' exists on GitHub`)
|
|
84
|
+
: log.error(`Label '${labelName}' not found ā run npx @slahon/lazykit@latest init`);
|
|
85
|
+
} catch {
|
|
86
|
+
log.warn('Could not check labels');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// PR creation permission
|
|
90
|
+
try {
|
|
91
|
+
const perms = JSON.parse(execSync(
|
|
92
|
+
`gh api repos/${repoInfo.owner}/${repoInfo.repo}/actions/permissions/workflow`,
|
|
93
|
+
{ stdio: 'pipe' }
|
|
94
|
+
).toString());
|
|
95
|
+
perms.can_approve_pull_request_reviews
|
|
96
|
+
? log.success('Actions can create and approve pull requests')
|
|
97
|
+
: log.warn(`PR creation not enabled ā ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)}`);
|
|
98
|
+
} catch {
|
|
99
|
+
log.warn('Could not check Actions permissions');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Branch protection
|
|
103
|
+
try {
|
|
104
|
+
execSync(`gh api repos/${repoInfo.owner}/${repoInfo.repo}/branches/main/protection`, { stdio: 'pipe' });
|
|
105
|
+
log.warn("Branch protection is on ā Claude's PRs require manual review before merge");
|
|
106
|
+
} catch {
|
|
107
|
+
log.success("No branch protection on main ā Claude's PRs can be merged directly");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { status };
|
package/update.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
const { log } = require('./logger');
|
|
10
|
+
const { confirm } = require('./prompt');
|
|
11
|
+
const { isGitRepo, getGitRemote, parseGitHubRepo, detectStack } = require('./git');
|
|
12
|
+
const { generateWorkflow } = require('./workflow');
|
|
13
|
+
const { generateClaudeMd } = require('./claude-md');
|
|
14
|
+
|
|
15
|
+
async function update({ dryRun = false } = {}) {
|
|
16
|
+
if (dryRun) console.log(chalk.yellow('\n [dry-run] Preview mode ā no files will be written.\n'));
|
|
17
|
+
console.log(chalk.bold.white('\n𦄠LazyKit Update\n'));
|
|
18
|
+
|
|
19
|
+
if (!isGitRepo()) {
|
|
20
|
+
log.error('Not inside a git repository.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const repoInfo = parseGitHubRepo(getGitRemote());
|
|
25
|
+
if (!repoInfo) {
|
|
26
|
+
log.error('No GitHub remote detected.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const workflowPath = path.join(process.cwd(), '.github', 'workflows', 'lazykit.yml');
|
|
31
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
32
|
+
|
|
33
|
+
// Read existing config from workflow file
|
|
34
|
+
let label = 'lazykit';
|
|
35
|
+
let autoTrigger = true;
|
|
36
|
+
if (fs.existsSync(workflowPath)) {
|
|
37
|
+
const wf = fs.readFileSync(workflowPath, 'utf8');
|
|
38
|
+
const m = wf.match(/label_trigger:\s*(\S+)/);
|
|
39
|
+
if (m) label = m[1];
|
|
40
|
+
autoTrigger = wf.includes("github.event.action == 'opened'");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stack = detectStack();
|
|
44
|
+
const filesToPush = [];
|
|
45
|
+
|
|
46
|
+
// Regenerate workflow
|
|
47
|
+
if (dryRun) {
|
|
48
|
+
log.info('[dry-run] Would update: .github/workflows/lazykit.yml');
|
|
49
|
+
} else {
|
|
50
|
+
const spinner = ora({ text: 'Updating workflow file...', color: 'cyan' }).start();
|
|
51
|
+
fs.mkdirSync(path.dirname(workflowPath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(workflowPath, generateWorkflow({ label, autoTrigger }));
|
|
53
|
+
spinner.succeed(chalk.green('Updated .github/workflows/lazykit.yml'));
|
|
54
|
+
filesToPush.push('.github/workflows/lazykit.yml');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Regenerate CLAUDE.md if it exists
|
|
58
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
59
|
+
const regen = await confirm('Regenerate CLAUDE.md?', true);
|
|
60
|
+
if (regen) {
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
log.info('[dry-run] Would update: CLAUDE.md');
|
|
63
|
+
} else {
|
|
64
|
+
fs.writeFileSync(claudeMdPath, generateClaudeMd({ stack }));
|
|
65
|
+
log.success('Updated CLAUDE.md');
|
|
66
|
+
filesToPush.push('CLAUDE.md');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!dryRun && filesToPush.length > 0) {
|
|
72
|
+
const autoPush = await confirm('Commit and push?', true);
|
|
73
|
+
if (autoPush) {
|
|
74
|
+
const spinner = ora({ text: 'Committing and pushing...', color: 'cyan' }).start();
|
|
75
|
+
try {
|
|
76
|
+
execSync(`git add ${filesToPush.join(' ')}`, { stdio: 'pipe' });
|
|
77
|
+
execSync(`git commit -m "Update LazyKit configuration"`, { stdio: 'pipe' });
|
|
78
|
+
execSync(`git push`, { stdio: 'pipe' });
|
|
79
|
+
spinner.succeed(chalk.green('Committed and pushed'));
|
|
80
|
+
} catch {
|
|
81
|
+
spinner.fail('Push failed ā commit and push manually');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { update };
|