@slahon/lazykit 1.1.1 ā 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -72
- 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/README.md
CHANGED
|
@@ -1,72 +1,132 @@
|
|
|
1
|
-
# 𦄠LazyKit
|
|
2
|
-
|
|
3
|
-
**Drop an issue, get a PR.**
|
|
4
|
-
|
|
5
|
-
LazyKit wires Claude AI into your GitHub repo so that when you
|
|
6
|
-
|
|
7
|
-
## Quickstart
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx @slahon/lazykit@latest init
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Run this inside your project folder.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
# 𦄠LazyKit
|
|
2
|
+
|
|
3
|
+
**Drop an issue, get a PR.**
|
|
4
|
+
|
|
5
|
+
LazyKit wires Claude AI into your GitHub repo so that when you open an issue, Claude reads it, writes the code, and opens a pull request ā while you do something else.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @slahon/lazykit@latest init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Run this inside your project folder. That's it. LazyKit handles everything else automatically.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Node.js 18+
|
|
18
|
+
- A GitHub repository with a remote set up
|
|
19
|
+
- A Claude Pro or Max subscription ([claude.ai](https://claude.ai))
|
|
20
|
+
- Claude Code installed locally (`npm install -g @anthropic-ai/claude-code`)
|
|
21
|
+
- GitHub CLI (`gh`) ā required for full automation (`brew install gh` / [cli.github.com](https://cli.github.com))
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
1. You open a GitHub issue using the **LazyKit Task** template
|
|
26
|
+
2. Claude starts working on it automatically (or when you apply the label ā your choice)
|
|
27
|
+
3. Claude reads the issue, explores your codebase, writes the changes
|
|
28
|
+
4. Claude opens a pull request with a title and description
|
|
29
|
+
5. You review and merge
|
|
30
|
+
|
|
31
|
+
You can also mention `@claude` in any issue comment to give follow-up instructions or re-trigger Claude mid-thread.
|
|
32
|
+
|
|
33
|
+
## What `init` does
|
|
34
|
+
|
|
35
|
+
Running `npx @slahon/lazykit@latest init` fully sets up your repo ā no manual steps required:
|
|
36
|
+
|
|
37
|
+
| Step | What happens |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| Detects repo | Reads your `git remote` to find your GitHub repo |
|
|
40
|
+
| Detects stack | Auto-detects your tech stack from `package.json`, `go.mod`, `Cargo.toml`, etc. |
|
|
41
|
+
| Creates workflow | `.github/workflows/lazykit.yml` ā the GitHub Actions automation |
|
|
42
|
+
| Creates issue template | `.github/ISSUE_TEMPLATE/lazykit.md` ā auto-applies the trigger label |
|
|
43
|
+
| Creates CLAUDE.md | Project guide so Claude understands your codebase |
|
|
44
|
+
| Creates label | Creates the trigger label on GitHub |
|
|
45
|
+
| Enables PR creation | Grants Actions permission to open pull requests |
|
|
46
|
+
| Sets token | Runs `claude setup-token` and stores it as `CLAUDE_CODE_OAUTH_TOKEN` in your repo secrets |
|
|
47
|
+
| Commits and pushes | Commits all generated files and pushes to GitHub |
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx @slahon/lazykit@latest init # Set up LazyKit in your repo
|
|
53
|
+
npx @slahon/lazykit@latest status # Check if everything is wired up correctly
|
|
54
|
+
npx @slahon/lazykit@latest update # Regenerate workflow and CLAUDE.md
|
|
55
|
+
npx @slahon/lazykit@latest remove # Remove LazyKit from your repo
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Flags
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @slahon/lazykit@latest init --dry-run # Preview what would happen without writing files
|
|
62
|
+
npx @slahon/lazykit@latest update --dry-run # Preview changes without applying them
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `lazykit status`
|
|
66
|
+
|
|
67
|
+
Runs a health check and reports:
|
|
68
|
+
|
|
69
|
+
- Workflow file present
|
|
70
|
+
- Issue template present
|
|
71
|
+
- CLAUDE.md present
|
|
72
|
+
- `CLAUDE_CODE_OAUTH_TOKEN` secret exists (with age warning if over 6 months old)
|
|
73
|
+
- Trigger label exists on GitHub
|
|
74
|
+
- Actions PR creation permission is enabled
|
|
75
|
+
- Branch protection status on `main`
|
|
76
|
+
|
|
77
|
+
### `lazykit update`
|
|
78
|
+
|
|
79
|
+
Re-generates `.github/workflows/lazykit.yml` (and optionally `CLAUDE.md`) without re-doing the full setup. Useful when you want to pull in changes to the workflow template. Reads your existing label name and trigger mode from the current workflow file.
|
|
80
|
+
|
|
81
|
+
### `lazykit remove`
|
|
82
|
+
|
|
83
|
+
Cleanly removes LazyKit from your repo:
|
|
84
|
+
- Deletes the workflow file, issue template, and optionally CLAUDE.md
|
|
85
|
+
- Deletes the trigger label from GitHub
|
|
86
|
+
- Deletes the `CLAUDE_CODE_OAUTH_TOKEN` secret
|
|
87
|
+
- Commits and pushes the removals
|
|
88
|
+
|
|
89
|
+
## Init options
|
|
90
|
+
|
|
91
|
+
During `npx @slahon/lazykit@latest init` you will be asked:
|
|
92
|
+
|
|
93
|
+
| Option | Default | Description |
|
|
94
|
+
|--------|---------|-------------|
|
|
95
|
+
| Label name | `lazykit` | The GitHub label that triggers Claude |
|
|
96
|
+
| Auto-trigger | Yes | Trigger Claude on every new issue, or only when you apply the label |
|
|
97
|
+
| Generate CLAUDE.md | Yes | Create a project guide for Claude |
|
|
98
|
+
|
|
99
|
+
## Trigger modes
|
|
100
|
+
|
|
101
|
+
**Auto (default)** ā Claude fires the moment a new issue is opened. No label needed.
|
|
102
|
+
|
|
103
|
+
**Label-controlled** ā Claude only runs when you apply the trigger label. Use this when you want to review issues before handing them to Claude.
|
|
104
|
+
|
|
105
|
+
## Authentication
|
|
106
|
+
|
|
107
|
+
LazyKit uses your Claude Pro/Max subscription via an OAuth token ā no pay-per-token API billing.
|
|
108
|
+
|
|
109
|
+
`init` handles this automatically: it runs `claude setup-token`, captures the token, and stores it as `CLAUDE_CODE_OAUTH_TOKEN` in your repo secrets via `gh secret set`.
|
|
110
|
+
|
|
111
|
+
If the token can't be set automatically (Claude Code not installed or `gh` not available), you'll see step-by-step instructions to do it manually.
|
|
112
|
+
|
|
113
|
+
**Token expiry:** OAuth tokens can expire. Run `lazykit status` to check the age of your token. If it's expired, re-run `npx @slahon/lazykit@latest init` to generate and store a fresh one.
|
|
114
|
+
|
|
115
|
+
## CLAUDE.md
|
|
116
|
+
|
|
117
|
+
LazyKit creates a `CLAUDE.md` file at your repo root. This is Claude's project guide ā it tells Claude about your stack, coding conventions, and rules to follow. Edit it to match your actual project for best results.
|
|
118
|
+
|
|
119
|
+
## Branch protection
|
|
120
|
+
|
|
121
|
+
If your `main` branch has protection rules enabled, Claude's pull requests will be opened but **cannot be auto-merged** ā they will require manual review and approval. LazyKit detects this during `init` and `status` and warns you.
|
|
122
|
+
|
|
123
|
+
## Tips
|
|
124
|
+
|
|
125
|
+
- **Keep issues small and specific.** "Add a `/health` endpoint that returns `{ status: 'ok' }`" works great. "Rewrite the auth system" does not.
|
|
126
|
+
- **Edit CLAUDE.md** to describe your folder structure, naming conventions, and any rules Claude must follow.
|
|
127
|
+
- **Use `@claude` in comments** to give Claude follow-up instructions or corrections without opening a new issue.
|
|
128
|
+
- **Run `lazykit status`** if something stops working ā it pinpoints exactly what's misconfigured.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
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 };
|