@slahon/lazykit 1.0.5 → 1.0.8

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 CHANGED
@@ -7,7 +7,7 @@ LazyKit wires Claude AI into your GitHub repo so that when you label an issue, C
7
7
  ## Quickstart
8
8
 
9
9
  ```bash
10
- npx @slahon/lazykit init
10
+ npx @slahon/lazykit@latest init
11
11
  ```
12
12
 
13
13
  Run this inside your project folder. It will:
package/claude-md.js CHANGED
@@ -4,7 +4,7 @@ function generateClaudeMd({ stack }) {
4
4
  return `# LazyKit — Project Guide for Claude
5
5
 
6
6
  ## Stack
7
- ${stack || 'Describe your tech stack here (e.g. TypeScript + Next.js, Postgres via Prisma)'}
7
+ ${stack || 'Update this section with your tech stack (e.g. TypeScript + Next.js, Postgres via Prisma)'}
8
8
 
9
9
  ## Conventions
10
10
  - Match the existing code style and patterns in whatever file you are editing.
package/git.js CHANGED
@@ -45,4 +45,50 @@ function isGhCliAvailable() {
45
45
  }
46
46
  }
47
47
 
48
- module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable };
48
+ function isGhAuthenticated() {
49
+ try {
50
+ execSync('gh auth status', { stdio: 'pipe' });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function detectStack() {
58
+ const cwd = process.cwd();
59
+ const stack = [];
60
+
61
+ const pkgPath = require('path').join(cwd, 'package.json');
62
+ if (require('fs').existsSync(pkgPath)) {
63
+ try {
64
+ const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8'));
65
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
66
+
67
+ if (deps['next']) stack.push('Next.js');
68
+ else if (deps['react']) stack.push('React');
69
+ else if (deps['vue']) stack.push('Vue');
70
+ else if (deps['@angular/core']) stack.push('Angular');
71
+ else if (deps['svelte']) stack.push('Svelte');
72
+ else if (deps['express']) stack.push('Express');
73
+ else if (deps['fastify']) stack.push('Fastify');
74
+
75
+ if (deps['typescript'] || require('fs').existsSync(require('path').join(cwd, 'tsconfig.json'))) stack.push('TypeScript');
76
+
77
+ if (deps['@prisma/client'] || deps['prisma']) stack.push('Prisma');
78
+ else if (deps['mongoose']) stack.push('MongoDB');
79
+ else if (deps['pg'] || deps['postgres']) stack.push('PostgreSQL');
80
+ else if (deps['mysql2']) stack.push('MySQL');
81
+ } catch {}
82
+ }
83
+
84
+ if (require('fs').existsSync(require('path').join(cwd, 'requirements.txt')) ||
85
+ require('fs').existsSync(require('path').join(cwd, 'pyproject.toml'))) stack.push('Python');
86
+ if (require('fs').existsSync(require('path').join(cwd, 'go.mod'))) stack.push('Go');
87
+ if (require('fs').existsSync(require('path').join(cwd, 'Cargo.toml'))) stack.push('Rust');
88
+ if (require('fs').existsSync(require('path').join(cwd, 'pom.xml')) ||
89
+ require('fs').existsSync(require('path').join(cwd, 'build.gradle'))) stack.push('Java');
90
+
91
+ return stack.length > 0 ? stack.join(' + ') : null;
92
+ }
93
+
94
+ module.exports = { getGitRemote, parseGitHubRepo, isGitRepo, isGhCliAvailable, isGhAuthenticated, detectStack };
package/init.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { execSync } = require('child_process');
5
+ const { execSync, spawnSync } = require('child_process');
6
6
  const chalk = require('chalk');
7
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 } = 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
 
@@ -29,8 +29,11 @@ async function init() {
29
29
  const remote = getGitRemote();
30
30
  const repoInfo = parseGitHubRepo(remote);
31
31
 
32
+ const stack = detectStack();
33
+
32
34
  if (repoInfo) {
33
35
  log.success(`Detected repo: ${chalk.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
36
+ if (stack) log.success(`Detected stack: ${chalk.cyan(stack)}`);
34
37
  } else {
35
38
  log.error('No GitHub remote detected.');
36
39
  console.log(chalk.gray('\n LazyKit requires a GitHub repository with a remote set up.\n'));
@@ -40,21 +43,42 @@ async function init() {
40
43
  process.exit(1);
41
44
  }
42
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
+
43
69
  log.blank();
44
70
 
45
- // ─── Step 2: Ask questions ───────────────────────────────────────────────
71
+ // ─── Step 3: Ask questions ───────────────────────────────────────────────
46
72
  log.title('Configure LazyKit');
47
73
  log.blank();
48
74
 
49
75
  const label = await ask('Label name to trigger Claude', 'lazykit');
50
76
 
51
- const stack = await ask('What is your stack?', 'e.g. TypeScript + Next.js, Postgres');
52
-
53
77
  const wantClaudeMd = await confirm('Generate CLAUDE.md project guide?', true);
54
78
 
55
79
  log.blank();
56
80
 
57
- // ─── Step 3: Create workflow file ────────────────────────────────────────
81
+ // ─── Step 4: Create workflow file ────────────────────────────────────────
58
82
  const spinner = ora({ text: 'Creating workflow file...', color: 'cyan' }).start();
59
83
 
60
84
  const workflowDir = path.join(process.cwd(), '.github', 'workflows');
@@ -66,7 +90,7 @@ async function init() {
66
90
 
67
91
  spinner.succeed(chalk.green('Created .github/workflows/lazykit.yml'));
68
92
 
69
- // ─── Step 4: Create CLAUDE.md ────────────────────────────────────────────
93
+ // ─── Step 5: Create CLAUDE.md ────────────────────────────────────────────
70
94
  if (wantClaudeMd) {
71
95
  const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
72
96
  const claudeMdContent = generateClaudeMd({ stack });
@@ -85,10 +109,8 @@ async function init() {
85
109
  }
86
110
  }
87
111
 
88
- // ─── Step 5: Create GitHub label ─────────────────────────────────────────
89
- const ghAvailable = isGhCliAvailable();
90
-
91
- if (ghAvailable && repoInfo) {
112
+ // ─── Step 6: Create GitHub label ─────────────────────────────────────────
113
+ if (ghReady) {
92
114
  const labelSpinner = ora({ text: `Creating '${label}' label on GitHub...`, color: 'cyan' }).start();
93
115
  try {
94
116
  execSync(
@@ -105,39 +127,80 @@ async function init() {
105
127
  }
106
128
  }
107
129
  } else {
108
- log.warn(`gh CLI not found — create the '${label}' label manually on GitHub`);
130
+ log.warn(`Create the '${label}' label manually at: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/labels`)}`);
109
131
  }
110
132
 
111
- // ─── Step 6: Done! Print final instructions ───────────────────────────────
112
- console.log('\n' + chalk.bold.green(' ✨ LazyKit is almost ready!\n'));
113
- log.divider();
133
+ // ─── Step 7: Enable Actions to create PRs ────────────────────────────────
134
+ if (ghReady) {
135
+ const prSpinner = ora({ text: 'Enabling Actions PR creation permission...', color: 'cyan' }).start();
136
+ try {
137
+ execSync(
138
+ `gh api repos/${repoInfo.owner}/${repoInfo.repo}/actions/permissions/workflow --method PUT --field default_workflow_permissions=write --field can_approve_pull_request_reviews=true`,
139
+ { stdio: 'pipe' }
140
+ );
141
+ prSpinner.succeed(chalk.green('Enabled: Actions can create and approve pull requests'));
142
+ } catch (err) {
143
+ const msg = err.stderr?.toString() || '';
144
+ if (msg.includes('403') || msg.includes('Must have admin rights')) {
145
+ prSpinner.warn(chalk.yellow('Permission denied — you need repo admin rights to change this setting.'));
146
+ console.log(chalk.gray(`\n Enable it manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)}`));
147
+ console.log(chalk.gray(' Actions → General → Workflow permissions → Enable "Allow GitHub Actions to create and approve pull requests"\n'));
148
+ } else {
149
+ prSpinner.warn(chalk.yellow('Could not enable PR creation automatically.'));
150
+ console.log(chalk.gray(` Enable manually: ${chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`)}\n`));
151
+ }
152
+ }
153
+ }
114
154
 
115
- console.log(chalk.bold('\n One manual step add your Claude token as a repo secret:\n'));
155
+ // ─── Step 8: Generate and set Claude token ───────────────────────────────
156
+ console.log('\n' + chalk.bold.green(' ✨ Almost there — setting up your Claude token...\n'));
116
157
 
117
- console.log(chalk.gray(' 1.') + ' Run in your terminal:');
118
- console.log(chalk.cyan(' claude setup-token'));
158
+ let tokenSet = false;
119
159
 
120
- console.log(chalk.gray('\n 2.') + ' Copy the token it prints ' + chalk.gray('(sk-ant-oat01-...)'));
160
+ try {
161
+ log.info('Running claude setup-token (a browser window may open to authenticate)...');
162
+ console.log();
121
163
 
122
- if (repoInfo) {
123
- console.log(chalk.gray('\n 3.') + ' Go to:');
124
- console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
125
- } else {
126
- console.log(chalk.gray('\n 3.') + ' Go to:');
127
- console.log(chalk.cyan(' Your repo → Settings → Secrets and variables → Actions'));
128
- }
164
+ const result = spawnSync('claude', ['setup-token'], {
165
+ stdio: ['inherit', 'pipe', 'pipe'],
166
+ encoding: 'utf8',
167
+ });
129
168
 
130
- console.log(chalk.gray('\n 4.') + ' Click ' + chalk.bold('"New repository secret"') + ' and add:');
131
- console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
132
- console.log(chalk.gray(' Value: ') + chalk.cyan('the token you copied'));
169
+ if (result.stderr) process.stderr.write(result.stderr);
133
170
 
134
- console.log(chalk.gray('\n 5.') + ' Also enable PR creation:');
135
- if (repoInfo) {
136
- console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/actions`));
137
- } else {
138
- console.log(chalk.cyan(' Repo Settings → Actions → General → Workflow permissions'));
171
+ const output = (result.stdout || '') + (result.stderr || '');
172
+ const tokenMatch = output.match(/sk-ant-oat[^\s]+/);
173
+
174
+ if (tokenMatch && ghReady) {
175
+ const token = tokenMatch[0];
176
+ const secretSpinner = ora({ text: 'Adding CLAUDE_CODE_OAUTH_TOKEN to GitHub secrets...', color: 'cyan' }).start();
177
+ execSync(
178
+ `gh secret set CLAUDE_CODE_OAUTH_TOKEN --body "${token}" --repo ${repoInfo.owner}/${repoInfo.repo}`,
179
+ { stdio: 'pipe' }
180
+ );
181
+ secretSpinner.succeed(chalk.green('CLAUDE_CODE_OAUTH_TOKEN added to GitHub secrets'));
182
+ tokenSet = true;
183
+ } else if (tokenMatch && !ghReady) {
184
+ log.warn('Token generated but gh CLI not found — add it manually:');
185
+ console.log(chalk.gray(' Name: ') + chalk.cyan('CLAUDE_CODE_OAUTH_TOKEN'));
186
+ console.log(chalk.gray(' Value: ') + chalk.cyan(tokenMatch[0]));
187
+ console.log(chalk.cyan(` https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
188
+ } else {
189
+ throw new Error('Token not found in output');
190
+ }
191
+ } catch (err) {
192
+ const isNotFound = err.status === 'ENOENT' || (err.message || '').includes('ENOENT');
193
+ if (isNotFound) {
194
+ log.warn('Claude Code CLI not found — install it first:');
195
+ console.log(chalk.cyan('\n npm install -g @anthropic-ai/claude-code'));
196
+ } else {
197
+ log.warn('Could not set token automatically.');
198
+ }
199
+ console.log(chalk.gray('\n Add it manually:'));
200
+ console.log(chalk.gray(' 1. Run: ') + chalk.cyan('claude setup-token'));
201
+ console.log(chalk.gray(' 2. Go to: ') + chalk.cyan(`https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`));
202
+ console.log(chalk.gray(' 3. Add secret:') + chalk.cyan(' CLAUDE_CODE_OAUTH_TOKEN') + chalk.gray(' = the token you copied\n'));
139
203
  }
140
- console.log(chalk.gray(' Enable: ') + '"Allow GitHub Actions to create and approve pull requests"');
141
204
 
142
205
  log.divider();
143
206
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@slahon/lazykit",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
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 bin/index.js"
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",