linear-ai-build 1.0.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/.env.example +3 -0
- package/README.md +220 -0
- package/bin/cli.js +21 -0
- package/bin/cli.ts +51 -0
- package/bin/init.ts +252 -0
- package/commands/build-from-linear.md +193 -0
- package/commands/generate-stories.md +110 -0
- package/dist/bridge/config.d.ts +11 -0
- package/dist/bridge/config.js +24 -0
- package/dist/bridge/executor.d.ts +8 -0
- package/dist/bridge/executor.js +147 -0
- package/dist/bridge/index.d.ts +3 -0
- package/dist/bridge/index.js +137 -0
- package/dist/bridge/linear-helpers.d.ts +6 -0
- package/dist/bridge/linear-helpers.js +43 -0
- package/dist/bridge/reporter.d.ts +7 -0
- package/dist/bridge/reporter.js +79 -0
- package/dist/bridge/state.d.ts +10 -0
- package/dist/bridge/state.js +37 -0
- package/dist/bridge/transformer.d.ts +22 -0
- package/dist/bridge/transformer.js +142 -0
- package/dist/bridge/types.d.ts +44 -0
- package/dist/bridge/types.js +1 -0
- package/dist/bridge/watcher.d.ts +13 -0
- package/dist/bridge/watcher.js +104 -0
- package/dist/generate-sdk.d.ts +1 -0
- package/dist/generate-sdk.js +471 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +232 -0
- package/package.json +61 -0
- package/src/bridge/config.ts +39 -0
- package/src/bridge/executor.ts +167 -0
- package/src/bridge/index.ts +156 -0
- package/src/bridge/linear-helpers.ts +49 -0
- package/src/bridge/reporter.ts +93 -0
- package/src/bridge/state.ts +50 -0
- package/src/bridge/transformer.ts +173 -0
- package/src/bridge/types.ts +45 -0
- package/src/bridge/watcher.ts +122 -0
- package/src/generate-sdk.ts +570 -0
- package/src/index.ts +287 -0
package/.env.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Linear AI Build
|
|
2
|
+
|
|
3
|
+
Generate user stories from feature descriptions, push them to Linear, then have AI agents build the code — all from Claude Code.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
1. **Generates stories** — Asks clarifying questions, creates a PRD with user stories, acceptance criteria, and tasks
|
|
8
|
+
2. **Pushes to Linear** — Creates a project with parent issues (stories) and child issues (tasks), labels, and priorities
|
|
9
|
+
3. **Builds from Linear** — Pulls labeled stories back from Linear and orchestrates Claude Flow agents to plan, implement, test, and review the code
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone <your-repo>
|
|
17
|
+
cd linear-ai-build
|
|
18
|
+
npm install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Configure
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run init
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This will:
|
|
28
|
+
1. Ask for your **Linear API key** (from Linear Settings > Security & Access > Personal API keys)
|
|
29
|
+
2. Ask for your **Anthropic API key** (optional — only needed for standalone CLI)
|
|
30
|
+
3. Ask for your **default Linear team ID** (optional)
|
|
31
|
+
4. Write a `.env` file with your credentials
|
|
32
|
+
5. Configure the **Linear MCP server** for Claude Code (`.mcp.json`)
|
|
33
|
+
6. Install the **Claude Code commands**
|
|
34
|
+
|
|
35
|
+
### Prerequisites for Building
|
|
36
|
+
|
|
37
|
+
Install Claude Flow globally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g claude-flow@alpha
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Generate Stories
|
|
46
|
+
|
|
47
|
+
Create stories and tasks in Linear from a feature description.
|
|
48
|
+
|
|
49
|
+
**CLI:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run generate -- "Add user authentication with OAuth" TEAM-abc123
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Claude Code:**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
/linear:generate-stories "Add user authentication with OAuth" TEAM-abc123
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Options:**
|
|
62
|
+
|
|
63
|
+
| Flag | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `--dry-run` | Preview the generated PRD without creating anything in Linear |
|
|
66
|
+
| `--cycle <id>` | Assign issues to a specific cycle (defaults to active cycle) |
|
|
67
|
+
| `--assignee <id>` | Assign issues to a specific user (defaults to you) |
|
|
68
|
+
| `--no-cycle` | Skip cycle assignment |
|
|
69
|
+
| `--no-assignee` | Skip assignee assignment |
|
|
70
|
+
| `--skip-questions` | Skip clarifying questions, generate PRD directly |
|
|
71
|
+
|
|
72
|
+
### Build From Linear
|
|
73
|
+
|
|
74
|
+
Pull labeled stories from Linear and have Claude Flow agents implement the code.
|
|
75
|
+
|
|
76
|
+
**Claude Code:**
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
/linear:build-from-linear
|
|
80
|
+
/linear:build-from-linear WIC-42
|
|
81
|
+
/linear:build-from-linear --label ai-build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This command:
|
|
85
|
+
1. Uses **Linear MCP** to fetch stories with the `ai-build` label (or a specific issue)
|
|
86
|
+
2. Fetches child tasks and acceptance criteria for each story
|
|
87
|
+
3. Asks you to confirm which issues to process
|
|
88
|
+
4. Uses **Claude Flow MCP** to orchestrate agents:
|
|
89
|
+
- **Planner** — analyzes the story and creates an implementation plan
|
|
90
|
+
- **Implementers** — build each task in parallel
|
|
91
|
+
- **Tester** — writes and runs tests against acceptance criteria
|
|
92
|
+
- **Reviewer** — reviews all changes, fixes issues
|
|
93
|
+
5. Creates a git branch per story (`linear/<issue-identifier>`)
|
|
94
|
+
6. Updates Linear with progress comments and moves issues to "In Review"
|
|
95
|
+
|
|
96
|
+
### Watch Mode (CLI)
|
|
97
|
+
|
|
98
|
+
Alternatively, poll Linear and auto-process issues without Claude Code:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run watch
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or process a single issue:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run build:issue -- WIC-42
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Feature Description
|
|
114
|
+
|
|
|
115
|
+
v
|
|
116
|
+
/linear:generate-stories
|
|
117
|
+
|
|
|
118
|
+
v
|
|
119
|
+
Linear (stories + tasks created)
|
|
120
|
+
|
|
|
121
|
+
Label with "ai-build"
|
|
122
|
+
|
|
|
123
|
+
v
|
|
124
|
+
/linear:build-from-linear
|
|
125
|
+
|
|
|
126
|
+
v
|
|
127
|
+
Claude Flow Agents
|
|
128
|
+
Plan → Implement → Test → Review
|
|
129
|
+
|
|
|
130
|
+
v
|
|
131
|
+
Git branch + PR ready
|
|
132
|
+
Linear issues moved to "In Review"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Environment Variables
|
|
136
|
+
|
|
137
|
+
| Variable | Required | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `LINEAR_API_KEY` | Yes | Linear API key |
|
|
140
|
+
| `LINEAR_TEAM_ID` | Yes | Default team ID or key (e.g., `WIC`) |
|
|
141
|
+
| `ANTHROPIC_API_KEY` | For CLI | Anthropic API key (not needed for Claude Code plugin) |
|
|
142
|
+
| `TARGET_REPO_PATH` | No | Where agents write code (default: current directory) |
|
|
143
|
+
| `TARGET_BRANCH` | No | Base branch for new branches (default: `main`) |
|
|
144
|
+
| `POLL_INTERVAL_MS` | No | Watch mode polling interval (default: `30000`) |
|
|
145
|
+
| `AI_BUILD_LABEL` | No | Linear label to watch for (default: `ai-build`) |
|
|
146
|
+
|
|
147
|
+
## MCP Servers
|
|
148
|
+
|
|
149
|
+
Configured in `.mcp.json`:
|
|
150
|
+
|
|
151
|
+
| Server | Purpose |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `linear` | Linear API — fetch/create/update issues, comments, projects |
|
|
154
|
+
| `claude-flow` | Claude Flow — agent swarm orchestration, task management |
|
|
155
|
+
|
|
156
|
+
## Project Structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
linear-ai-build/
|
|
160
|
+
bin/
|
|
161
|
+
cli.ts # CLI entry point (init, generate, watch, build)
|
|
162
|
+
init.ts # Interactive setup
|
|
163
|
+
commands/
|
|
164
|
+
generate-stories.md # /linear:generate-stories command
|
|
165
|
+
build-from-linear.md # /linear:build-from-linear command
|
|
166
|
+
src/
|
|
167
|
+
generate-sdk.ts # PRD generation + Linear SDK integration
|
|
168
|
+
index.ts # MCP-based alternative
|
|
169
|
+
bridge/
|
|
170
|
+
index.ts # Watch/build entry point
|
|
171
|
+
watcher.ts # Linear polling loop
|
|
172
|
+
executor.ts # Claude Flow workflow executor
|
|
173
|
+
transformer.ts # Issue → workflow builder
|
|
174
|
+
reporter.ts # Linear status updates
|
|
175
|
+
linear-helpers.ts # Shared Linear SDK helpers
|
|
176
|
+
config.ts # Bridge configuration
|
|
177
|
+
state.ts # Deduplication tracking
|
|
178
|
+
types.ts # TypeScript types
|
|
179
|
+
.env.example
|
|
180
|
+
.mcp.json # MCP server config (Linear + Claude Flow)
|
|
181
|
+
package.json
|
|
182
|
+
tsconfig.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
### "Missing LINEAR_API_KEY environment variable"
|
|
188
|
+
|
|
189
|
+
Make sure `.env` exists and contains your key, or export it directly:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
export LINEAR_API_KEY=lin_api_...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### "Team ID not found"
|
|
196
|
+
|
|
197
|
+
Verify your team ID:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
curl -X POST https://api.linear.app/graphql \
|
|
201
|
+
-H "Authorization: lin_api_YOUR_KEY" \
|
|
202
|
+
-H "Content-Type: application/json" \
|
|
203
|
+
-d '{"query": "{ viewer { teams { nodes { id name key } } } }"}'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Claude Flow MCP not connecting
|
|
207
|
+
|
|
208
|
+
Re-register the MCP server:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
claude mcp add claude-flow -- npx -y claude-flow@alpha mcp start
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### "Failed to parse PRD JSON after retry"
|
|
215
|
+
|
|
216
|
+
Claude occasionally produces unparseable output. Try running again or simplifying the feature description.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Use tsx to run the TypeScript CLI
|
|
10
|
+
const tsxBin = join(__dirname, '..', 'node_modules', '.bin', 'tsx');
|
|
11
|
+
const cliTs = join(__dirname, 'cli.ts');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
execFileSync(tsxBin, [cliTs, ...process.argv.slice(2)], {
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// execFileSync throws on non-zero exit — just propagate the exit code
|
|
20
|
+
process.exit(err.status || 1);
|
|
21
|
+
}
|
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
const command = process.argv[2];
|
|
3
|
+
|
|
4
|
+
switch (command) {
|
|
5
|
+
case 'init':
|
|
6
|
+
import('./init.ts');
|
|
7
|
+
break;
|
|
8
|
+
case 'generate':
|
|
9
|
+
// Forward remaining args to generate-sdk
|
|
10
|
+
process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
|
|
11
|
+
import('../src/generate-sdk.ts');
|
|
12
|
+
break;
|
|
13
|
+
case 'watch':
|
|
14
|
+
import('../src/bridge/index.ts').then(m => m.watch());
|
|
15
|
+
break;
|
|
16
|
+
case 'build': {
|
|
17
|
+
const issueId = process.argv[3];
|
|
18
|
+
if (!issueId) {
|
|
19
|
+
console.error('Usage: linear-ai-build build <issue-identifier>');
|
|
20
|
+
console.error('Example: linear-ai-build build WIC-42');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
import('../src/bridge/index.ts').then(m => m.build(issueId));
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
default:
|
|
27
|
+
console.log(`linear-ai-build - Generate user stories and build with AI agents
|
|
28
|
+
|
|
29
|
+
Commands:
|
|
30
|
+
init Interactive setup — credentials, MCP config, Claude Code command
|
|
31
|
+
generate Generate stories and push to Linear (standalone mode)
|
|
32
|
+
watch Watch Linear for ai-build issues and process with Claude Flow
|
|
33
|
+
build One-shot: process a specific Linear issue with Claude Flow
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
npx linear-ai-build init
|
|
37
|
+
npx linear-ai-build generate "Add auth" TEAM-abc123
|
|
38
|
+
npx linear-ai-build watch
|
|
39
|
+
npx linear-ai-build build WIC-42
|
|
40
|
+
|
|
41
|
+
After running init, use the Claude Code command:
|
|
42
|
+
/linear:generate-stories "Add user authentication" TEAM-abc123
|
|
43
|
+
|
|
44
|
+
Environment variables for bridge:
|
|
45
|
+
TARGET_REPO_PATH Path to the repo where agents write code (default: cwd)
|
|
46
|
+
TARGET_BRANCH Base branch for new branches (default: main)
|
|
47
|
+
POLL_INTERVAL_MS Polling interval in ms (default: 30000)
|
|
48
|
+
AI_BUILD_LABEL Linear label to watch for (default: ai-build)
|
|
49
|
+
`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
package/bin/init.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const COMMAND_DIR = 'linear';
|
|
12
|
+
const COMMAND_FILE = 'generate-stories.md';
|
|
13
|
+
|
|
14
|
+
// --- Interactive Prompts ---
|
|
15
|
+
|
|
16
|
+
function prompt(question: string, defaultValue = ''): Promise<string> {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
21
|
+
rl.close();
|
|
22
|
+
resolve(answer.trim() || defaultValue);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function promptSecret(question: string): Promise<string> {
|
|
28
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
return new Promise(resolve => {
|
|
30
|
+
// Note: readline doesn't natively hide input, but we inform the user
|
|
31
|
+
rl.question(`${question}: `, answer => {
|
|
32
|
+
rl.close();
|
|
33
|
+
resolve(answer.trim());
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Credential Collection ---
|
|
39
|
+
|
|
40
|
+
async function collectCredentials(): Promise<{
|
|
41
|
+
linearApiKey: string;
|
|
42
|
+
anthropicApiKey: string;
|
|
43
|
+
teamId: string;
|
|
44
|
+
}> {
|
|
45
|
+
console.log('\nLinear AI Build - Setup\n');
|
|
46
|
+
console.log('This will configure the tool for CLI use and install the Claude Code plugin.\n');
|
|
47
|
+
|
|
48
|
+
// Check for existing .env
|
|
49
|
+
const envFile = path.resolve(process.cwd(), '.env');
|
|
50
|
+
const existing = loadExistingEnv(envFile);
|
|
51
|
+
|
|
52
|
+
// Linear API Key
|
|
53
|
+
console.log('1. Linear API Key');
|
|
54
|
+
console.log(' Get yours at: Linear Settings > Security & Access > Personal API keys');
|
|
55
|
+
let linearApiKey = existing.LINEAR_API_KEY || '';
|
|
56
|
+
if (linearApiKey) {
|
|
57
|
+
const masked = linearApiKey.slice(0, 8) + '...' + linearApiKey.slice(-4);
|
|
58
|
+
const keep = await prompt(` Found existing key (${masked}). Keep it? (y/n)`, 'y');
|
|
59
|
+
if (keep.toLowerCase() !== 'y') {
|
|
60
|
+
linearApiKey = await promptSecret(' Enter your Linear API key');
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
linearApiKey = await promptSecret(' Enter your Linear API key');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!linearApiKey) {
|
|
67
|
+
console.error('\n Linear API key is required. Aborting.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Anthropic API Key (optional — only needed for standalone CLI, not the Claude Code plugin)
|
|
72
|
+
console.log('\n2. Anthropic API Key (optional — only needed for standalone CLI)');
|
|
73
|
+
console.log(' Get yours at: https://console.anthropic.com/ > API Keys');
|
|
74
|
+
console.log(' Skip this if you only plan to use the Claude Code plugin.');
|
|
75
|
+
let anthropicApiKey = existing.ANTHROPIC_API_KEY || '';
|
|
76
|
+
if (anthropicApiKey) {
|
|
77
|
+
const masked = anthropicApiKey.slice(0, 8) + '...' + anthropicApiKey.slice(-4);
|
|
78
|
+
const keep = await prompt(` Found existing key (${masked}). Keep it? (y/n)`, 'y');
|
|
79
|
+
if (keep.toLowerCase() !== 'y') {
|
|
80
|
+
anthropicApiKey = await promptSecret(' Enter your Anthropic API key (or press Enter to skip)');
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
anthropicApiKey = await promptSecret(' Enter your Anthropic API key (or press Enter to skip)');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Team ID
|
|
87
|
+
console.log('\n3. Default Linear Team ID');
|
|
88
|
+
console.log(' Find it in the URL: https://linear.app/YOUR-WORKSPACE/...');
|
|
89
|
+
console.log(' Or query it with: curl -X POST https://api.linear.app/graphql \\');
|
|
90
|
+
console.log(' -H "Authorization: <your-key>" -H "Content-Type: application/json" \\');
|
|
91
|
+
console.log(' -d \'{"query": "{ viewer { teams { nodes { id name } } } }"}\'');
|
|
92
|
+
let teamId = existing.LINEAR_TEAM_ID || '';
|
|
93
|
+
if (teamId) {
|
|
94
|
+
teamId = await prompt(` Default team ID`, teamId);
|
|
95
|
+
} else {
|
|
96
|
+
teamId = await prompt(' Default team ID (optional, can pass per-command)');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { linearApiKey, anthropicApiKey, teamId };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadExistingEnv(envPath: string): Record<string, string> {
|
|
103
|
+
const result: Record<string, string> = {};
|
|
104
|
+
if (!fs.existsSync(envPath)) return result;
|
|
105
|
+
|
|
106
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
107
|
+
for (const line of content.split('\n')) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
110
|
+
const eqIdx = trimmed.indexOf('=');
|
|
111
|
+
if (eqIdx === -1) continue;
|
|
112
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
113
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
114
|
+
result[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- .env File ---
|
|
120
|
+
|
|
121
|
+
function writeEnvFile(credentials: { linearApiKey: string; anthropicApiKey: string; teamId: string }) {
|
|
122
|
+
const envPath = path.resolve(process.cwd(), '.env');
|
|
123
|
+
const existing = loadExistingEnv(envPath);
|
|
124
|
+
|
|
125
|
+
// Merge with existing values (preserve any extra keys)
|
|
126
|
+
existing.LINEAR_API_KEY = credentials.linearApiKey;
|
|
127
|
+
if (credentials.anthropicApiKey) {
|
|
128
|
+
existing.ANTHROPIC_API_KEY = credentials.anthropicApiKey;
|
|
129
|
+
}
|
|
130
|
+
if (credentials.teamId) {
|
|
131
|
+
existing.LINEAR_TEAM_ID = credentials.teamId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lines: string[] = [];
|
|
135
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
136
|
+
lines.push(`${key}=${value}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n');
|
|
140
|
+
console.log(`\n Saved: ${envPath}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Claude Code MCP Configuration ---
|
|
144
|
+
|
|
145
|
+
function configureLinearMcp(linearApiKey: string) {
|
|
146
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
147
|
+
|
|
148
|
+
// Write MCP config to project-level .mcp.json
|
|
149
|
+
// This configures the Linear MCP server so Claude Code can talk to Linear directly
|
|
150
|
+
const mcpConfigPath = path.resolve(process.cwd(), '.mcp.json');
|
|
151
|
+
let mcpConfig: Record<string, any> = {};
|
|
152
|
+
|
|
153
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
154
|
+
try {
|
|
155
|
+
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
156
|
+
} catch {
|
|
157
|
+
// Start fresh if corrupted
|
|
158
|
+
mcpConfig = {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!mcpConfig.mcpServers) {
|
|
163
|
+
mcpConfig.mcpServers = {};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
mcpConfig.mcpServers.linear = {
|
|
167
|
+
type: 'http',
|
|
168
|
+
url: 'https://mcp.linear.app/mcp',
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: linearApiKey,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
|
|
175
|
+
console.log(` Configured Linear MCP: ${mcpConfigPath}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Claude Code Command Installation ---
|
|
179
|
+
|
|
180
|
+
function installCommand() {
|
|
181
|
+
const targetDir = path.join(os.homedir(), '.claude', 'commands', COMMAND_DIR);
|
|
182
|
+
const targetFile = path.join(targetDir, COMMAND_FILE);
|
|
183
|
+
|
|
184
|
+
// Find the command template relative to this script
|
|
185
|
+
let sourceFile = path.resolve(__dirname, '..', 'commands', COMMAND_FILE);
|
|
186
|
+
|
|
187
|
+
if (!fs.existsSync(sourceFile)) {
|
|
188
|
+
// Fallback: try from cwd
|
|
189
|
+
sourceFile = path.resolve(process.cwd(), 'commands', COMMAND_FILE);
|
|
190
|
+
if (!fs.existsSync(sourceFile)) {
|
|
191
|
+
console.error(` Could not find command template at:\n ${path.resolve(__dirname, '..', 'commands', COMMAND_FILE)}\n ${sourceFile}`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const existed = fs.existsSync(targetFile);
|
|
199
|
+
fs.copyFileSync(sourceFile, targetFile);
|
|
200
|
+
|
|
201
|
+
if (existed) {
|
|
202
|
+
console.log(` Updated: ${targetFile}`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` Installed: ${targetFile}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- Main ---
|
|
211
|
+
|
|
212
|
+
async function init() {
|
|
213
|
+
// Collect credentials interactively
|
|
214
|
+
const credentials = await collectCredentials();
|
|
215
|
+
|
|
216
|
+
// Write .env file
|
|
217
|
+
console.log('\nConfiguring environment...');
|
|
218
|
+
writeEnvFile(credentials);
|
|
219
|
+
|
|
220
|
+
// Configure Linear MCP for Claude Code
|
|
221
|
+
console.log('\nConfiguring Claude Code...');
|
|
222
|
+
configureLinearMcp(credentials.linearApiKey);
|
|
223
|
+
|
|
224
|
+
// Install the Claude Code command
|
|
225
|
+
const installed = installCommand();
|
|
226
|
+
|
|
227
|
+
// Summary
|
|
228
|
+
console.log('\n' + '═'.repeat(50));
|
|
229
|
+
console.log('Setup complete!');
|
|
230
|
+
console.log('═'.repeat(50));
|
|
231
|
+
|
|
232
|
+
console.log('\nCLI usage:');
|
|
233
|
+
if (credentials.teamId) {
|
|
234
|
+
console.log(` npm run generate -- "Add user authentication" ${credentials.teamId}`);
|
|
235
|
+
} else {
|
|
236
|
+
console.log(' npm run generate -- "Add user authentication" TEAM-abc123');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (installed) {
|
|
240
|
+
console.log('\nClaude Code usage:');
|
|
241
|
+
if (credentials.teamId) {
|
|
242
|
+
console.log(` /linear:generate-stories "Add user authentication" ${credentials.teamId}`);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(' /linear:generate-stories "Add user authentication" TEAM-abc123');
|
|
245
|
+
}
|
|
246
|
+
console.log('\n Linear MCP is configured — Claude can create issues directly.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log('');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
init();
|