@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 +1 -1
- package/claude-md.js +1 -1
- package/git.js +47 -1
- package/init.js +98 -35
- package/package.json +3 -3
package/README.md
CHANGED
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 || '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
89
|
-
|
|
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(`
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
console.log(chalk.cyan(' claude setup-token'));
|
|
158
|
+
let tokenSet = false;
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
try {
|
|
161
|
+
log.info('Running claude setup-token (a browser window may open to authenticate)...');
|
|
162
|
+
console.log();
|
|
121
163
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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
|
|
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",
|