@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 CHANGED
@@ -91,4 +91,13 @@ function detectStack() {
91
91
  return stack.length > 0 ? stack.join(' + ') : null;
92
92
  }
93
93
 
94
- module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable, isGhAuthenticated, detectStack };
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āŒ Setup failed:', err.message);
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 Set up LazyKit in your current GitHub repo
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, select } = require('./prompt');
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
- spinner.succeed(chalk.green('Created .github/workflows/lazykit.yml'));
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
- // ─── Step 5: Create CLAUDE.md ────────────────────────────────────────────
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 (fs.existsSync(claudeMdPath)) {
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, claudeMdContent);
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, claudeMdContent);
124
+ fs.writeFileSync(claudeMdPath, generateClaudeMd({ stack }));
110
125
  log.success('Created CLAUDE.md');
111
126
  }
112
127
  }
113
128
 
114
- // ─── Step 6: Create GitHub label ─────────────────────────────────────────
115
- if (ghReady) {
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 automatically — you'll need to create it manually`));
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 7: Enable Actions to create PRs ────────────────────────────────
136
- if (ghReady) {
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 8: Generate and set Claude token ───────────────────────────────
158
- console.log('\n' + chalk.bold.green(' ✨ Almost there — setting up your Claude token...\n'));
159
-
160
- let tokenSet = false;
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
- const output = (result.stdout || '') + (result.stderr || '');
174
- const tokenMatch = output.match(/sk-ant-oat[^\s]+/);
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
- if (tokenMatch && ghReady) {
177
- const token = tokenMatch[0];
178
- const secretSpinner = ora({ text: 'Adding CLAUDE_CODE_OAUTH_TOKEN to GitHub secrets...', color: 'cyan' }).start();
179
- execSync(
180
- `gh secret set CLAUDE_CODE_OAUTH_TOKEN --body "${token}" --repo ${repoInfo.owner}/${repoInfo.repo}`,
181
- { stdio: 'pipe' }
182
- );
183
- secretSpinner.succeed(chalk.green('CLAUDE_CODE_OAUTH_TOKEN added to GitHub secrets'));
184
- tokenSet = true;
185
- } else if (tokenMatch && !ghReady) {
186
- log.warn('Token generated but gh CLI not found — add it manually:');
187
- console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
188
- console.log(chalk.gray(' Value: ') + chalk.cyan(tokenMatch[0]));
189
- console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
190
- } else {
191
- throw new Error('Token not found in output');
192
- }
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.');
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 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' : '')));
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\n'));
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 describing what you want`);
238
- console.log(` ${chalk.gray('2.')} Apply the ${chalk.bold.magenta(label)} label`);
239
- console.log(` ${chalk.gray('3.')} Watch Claude open a PR with the changes\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slahon/lazykit",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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"
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 };