@lazyagent/lazy-agents 0.0.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/bin/lazy.js +200 -0
- package/package.json +40 -0
- package/src/cli.test.js +135 -0
- package/src/doctor.js +70 -0
- package/src/launch.js +132 -0
- package/src/launch.test.js +175 -0
- package/src/setup.js +280 -0
- package/src/setup.test.js +218 -0
- package/src/wrapper.integration.test.js +138 -0
- package/src/wrapper.js +266 -0
- package/src/wrapper.test.js +128 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseAgentSpec, buildWtCommand, buildTerminalCommand, buildMacCommand, buildLinuxCommand } = require('./launch');
|
|
4
|
+
|
|
5
|
+
describe('parseAgentSpec', () => {
|
|
6
|
+
test('parses numeric spec into default roles', () => {
|
|
7
|
+
const agents = parseAgentSpec('3');
|
|
8
|
+
expect(agents).toHaveLength(3);
|
|
9
|
+
expect(agents[0]).toEqual({ name: 'architect-1', role: 'architect' });
|
|
10
|
+
expect(agents[1]).toEqual({ name: 'developer-2', role: 'developer' });
|
|
11
|
+
expect(agents[2]).toEqual({ name: 'tester-3', role: 'tester' });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('fills extra agents beyond 3 defaults with generic', () => {
|
|
15
|
+
const agents = parseAgentSpec('5');
|
|
16
|
+
expect(agents).toHaveLength(5);
|
|
17
|
+
expect(agents[3].role).toBe('generic');
|
|
18
|
+
expect(agents[4].role).toBe('generic');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('parses comma-separated role list', () => {
|
|
22
|
+
const agents = parseAgentSpec('architect,developer,tester');
|
|
23
|
+
expect(agents).toHaveLength(3);
|
|
24
|
+
expect(agents[0]).toEqual({ name: 'architect-1', role: 'architect' });
|
|
25
|
+
expect(agents[1]).toEqual({ name: 'developer-2', role: 'developer' });
|
|
26
|
+
expect(agents[2]).toEqual({ name: 'tester-3', role: 'tester' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('parses single role', () => {
|
|
30
|
+
const agents = parseAgentSpec('devops');
|
|
31
|
+
expect(agents).toHaveLength(1);
|
|
32
|
+
expect(agents[0]).toEqual({ name: 'devops-1', role: 'devops' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('trims whitespace from roles', () => {
|
|
36
|
+
const agents = parseAgentSpec(' architect , developer ');
|
|
37
|
+
expect(agents[0].role).toBe('architect');
|
|
38
|
+
expect(agents[1].role).toBe('developer');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('parses "1" as one agent', () => {
|
|
42
|
+
const agents = parseAgentSpec('1');
|
|
43
|
+
expect(agents).toHaveLength(1);
|
|
44
|
+
expect(agents[0].role).toBe('architect');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const fs = require('fs');
|
|
49
|
+
|
|
50
|
+
describe('buildWtCommand', () => {
|
|
51
|
+
const WRAPPER = '/path/to/wrapper.js';
|
|
52
|
+
|
|
53
|
+
test('produces a wt command with one tab', () => {
|
|
54
|
+
const agents = [{ name: 'architect-1', role: 'architect' }];
|
|
55
|
+
const cmd = buildWtCommand(agents, 'test-session', WRAPPER, 'C:/projects/foo');
|
|
56
|
+
expect(cmd).toMatch(/^wt /);
|
|
57
|
+
expect(cmd).toMatch(/new-tab/);
|
|
58
|
+
expect(cmd).toMatch(/architect-1/);
|
|
59
|
+
// session is written to a temp file, not inline
|
|
60
|
+
expect(cmd).toMatch(/lazy-prompt/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('uses PowerShell and passes prompt via temp file to avoid truncation', () => {
|
|
64
|
+
const agents = [{ name: 'architect-1', role: 'architect' }];
|
|
65
|
+
const cmd = buildWtCommand(agents, 'swift-oak-17', WRAPPER, 'C:/projects/foo');
|
|
66
|
+
expect(cmd).toMatch(/powershell/);
|
|
67
|
+
// Extract the temp file path from the command and verify its contents
|
|
68
|
+
const match = cmd.match(/[A-Za-z]:\/[^']*lazy-prompt[^']+\.txt/);
|
|
69
|
+
expect(match).not.toBeNull();
|
|
70
|
+
const content = fs.readFileSync(match[0], 'utf8');
|
|
71
|
+
expect(content).toBe('Register as architect-1, role architect, session code: swift-oak-17');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('includes all agents as tabs', () => {
|
|
75
|
+
const agents = [
|
|
76
|
+
{ name: 'architect-1', role: 'architect' },
|
|
77
|
+
{ name: 'developer-2', role: 'developer' },
|
|
78
|
+
];
|
|
79
|
+
const cmd = buildWtCommand(agents, 'my-session', WRAPPER, 'C:/projects/foo');
|
|
80
|
+
expect(cmd).toMatch(/architect-1/);
|
|
81
|
+
expect(cmd).toMatch(/developer-2/);
|
|
82
|
+
expect(cmd).toMatch(/; new-tab/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('omits session from temp file when not provided', () => {
|
|
86
|
+
const agents = [{ name: 'dev-1', role: 'developer' }];
|
|
87
|
+
const cmd = buildWtCommand(agents, null, WRAPPER, 'C:/projects/foo');
|
|
88
|
+
const match = cmd.match(/[A-Za-z]:\/[^']*lazy-prompt[^']+\.txt/);
|
|
89
|
+
expect(match).not.toBeNull();
|
|
90
|
+
const content = fs.readFileSync(match[0], 'utf8');
|
|
91
|
+
expect(content).not.toMatch(/session code/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('includes session code in temp file', () => {
|
|
95
|
+
const agents = [{ name: 'dev-1', role: 'developer' }];
|
|
96
|
+
const cmd = buildWtCommand(agents, 'swift-oak-17', WRAPPER, 'C:/projects/foo');
|
|
97
|
+
const match = cmd.match(/[A-Za-z]:\/[^']*lazy-prompt[^']+\.txt/);
|
|
98
|
+
const content = fs.readFileSync(match[0], 'utf8');
|
|
99
|
+
expect(content).toMatch(/swift-oak-17/);
|
|
100
|
+
expect(content).toMatch(/session code: swift-oak-17/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('includes role in tab title', () => {
|
|
104
|
+
const agents = [{ name: 'dev-1', role: 'developer' }];
|
|
105
|
+
const cmd = buildWtCommand(agents, null, WRAPPER, 'C:/projects/foo');
|
|
106
|
+
expect(cmd).toMatch(/dev-1 \(developer\)/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('references node and wrapper path', () => {
|
|
110
|
+
const agents = [{ name: 'arch-1', role: 'architect' }];
|
|
111
|
+
const cmd = buildWtCommand(agents, null, WRAPPER, 'C:/projects/foo');
|
|
112
|
+
expect(cmd).toMatch(/node/);
|
|
113
|
+
expect(cmd).toMatch(/wrapper\.js/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('includes --startingDirectory with the provided cwd', () => {
|
|
117
|
+
const agents = [{ name: 'dev-1', role: 'developer' }];
|
|
118
|
+
const cmd = buildWtCommand(agents, null, WRAPPER, 'C:/projects/myrepo');
|
|
119
|
+
expect(cmd).toMatch(/--startingDirectory/);
|
|
120
|
+
expect(cmd).toMatch(/myrepo/);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('buildTerminalCommand (cross-platform)', () => {
|
|
125
|
+
const WRAPPER = '/path/to/wrapper.js';
|
|
126
|
+
const agents = [{ name: 'architect-1', role: 'architect' }];
|
|
127
|
+
const session = 'swift-oak-17';
|
|
128
|
+
const cwd = '/home/user/projects';
|
|
129
|
+
|
|
130
|
+
test('windows platform delegates to buildWtCommand', () => {
|
|
131
|
+
const cmd = buildTerminalCommand('windows', agents, session, WRAPPER, 'C:/projects/foo');
|
|
132
|
+
expect(cmd).toMatch(/^wt /);
|
|
133
|
+
expect(cmd).toMatch(/powershell/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('mac platform uses osascript', () => {
|
|
137
|
+
const cmd = buildTerminalCommand('mac', agents, session, WRAPPER, cwd);
|
|
138
|
+
expect(cmd).toMatch(/osascript/);
|
|
139
|
+
expect(cmd).toMatch(/Terminal/);
|
|
140
|
+
expect(cmd).toMatch(/architect-1/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('linux platform uses gnome-terminal', () => {
|
|
144
|
+
const cmd = buildTerminalCommand('linux', agents, session, WRAPPER, cwd);
|
|
145
|
+
expect(cmd).toMatch(/gnome-terminal/);
|
|
146
|
+
expect(cmd).toMatch(/--tab/);
|
|
147
|
+
expect(cmd).toMatch(/architect-1/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('mac command includes session in prompt', () => {
|
|
151
|
+
const cmd = buildMacCommand(agents, 'my-session', WRAPPER, cwd);
|
|
152
|
+
expect(cmd).toMatch(/my-session/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('linux command includes session in prompt', () => {
|
|
156
|
+
const cmd = buildLinuxCommand(agents, 'my-session', WRAPPER, cwd);
|
|
157
|
+
expect(cmd).toMatch(/my-session/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('mac command includes cwd', () => {
|
|
161
|
+
const cmd = buildMacCommand(agents, null, WRAPPER, '/my/special/dir');
|
|
162
|
+
expect(cmd).toMatch(/\/my\/special\/dir/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('linux command includes cwd via --working-directory', () => {
|
|
166
|
+
const cmd = buildLinuxCommand(agents, null, WRAPPER, '/my/special/dir');
|
|
167
|
+
expect(cmd).toMatch(/--working-directory/);
|
|
168
|
+
expect(cmd).toMatch(/\/my\/special\/dir/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('unknown platform falls back to windows (wt)', () => {
|
|
172
|
+
const cmd = buildTerminalCommand('unknown-os', agents, session, WRAPPER, 'C:/projects/foo');
|
|
173
|
+
expect(cmd).toMatch(/^wt /);
|
|
174
|
+
});
|
|
175
|
+
});
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
// ──────────────────────────────────────────────
|
|
8
|
+
// Agent templates — written to .claude/agents/<role>.md
|
|
9
|
+
// Each becomes a Claude Code sub-agent via the agents/ directory.
|
|
10
|
+
// ──────────────────────────────────────────────
|
|
11
|
+
const AGENT_TEMPLATES = {
|
|
12
|
+
architect: {
|
|
13
|
+
description: 'System architect for design decisions, technical planning, and code structure. Use when you need high-level architectural guidance or breaking down complex systems.',
|
|
14
|
+
tools: 'Read, Grep, Glob, Bash',
|
|
15
|
+
color: 'blue',
|
|
16
|
+
system: `You are an expert software architect. Your responsibilities:
|
|
17
|
+
- Design scalable, maintainable system architectures
|
|
18
|
+
- Make high-level technical decisions and trade-off analyses
|
|
19
|
+
- Define coding standards, patterns, and project structure
|
|
20
|
+
- Break complex systems into well-bounded components
|
|
21
|
+
- Review and validate architectural decisions
|
|
22
|
+
|
|
23
|
+
Think long-term. Prioritise clarity, scalability, and maintainability over cleverness.`,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
developer: {
|
|
27
|
+
description: 'Full-stack developer for implementing features, writing production code, and integrating components. Use for any coding task.',
|
|
28
|
+
tools: 'Read, Write, Edit, Bash, Grep, Glob',
|
|
29
|
+
color: 'green',
|
|
30
|
+
system: `You are an expert full-stack developer. Your responsibilities:
|
|
31
|
+
- Implement features according to specifications and architecture
|
|
32
|
+
- Write clean, readable, well-tested, production-ready code
|
|
33
|
+
- Follow the project's existing patterns and conventions
|
|
34
|
+
- Debug and fix issues efficiently with root-cause analysis
|
|
35
|
+
- Optimise for correctness first, then performance
|
|
36
|
+
|
|
37
|
+
Always read existing code before modifying it. Never add unnecessary abstractions.`,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
tester: {
|
|
41
|
+
description: 'QA engineer for writing tests, verifying coverage, and ensuring correctness. Use when adding tests or validating behaviour.',
|
|
42
|
+
tools: 'Read, Write, Edit, Bash, Grep, Glob',
|
|
43
|
+
color: 'yellow',
|
|
44
|
+
system: `You are an expert QA engineer and test automation specialist. Your responsibilities:
|
|
45
|
+
- Write comprehensive unit, integration, and end-to-end tests
|
|
46
|
+
- Identify edge cases, boundary conditions, and failure modes
|
|
47
|
+
- Ensure test coverage meets quality standards
|
|
48
|
+
- Keep tests deterministic, isolated, and fast
|
|
49
|
+
- Review code for testability issues
|
|
50
|
+
|
|
51
|
+
Prefer tests that catch real bugs over tests that just hit coverage numbers.`,
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
reviewer: {
|
|
55
|
+
description: 'Code reviewer for quality, security, correctness, and best practices. Use when reviewing diffs or PRs.',
|
|
56
|
+
tools: 'Read, Grep, Glob, Bash',
|
|
57
|
+
color: 'purple',
|
|
58
|
+
system: `You are an expert code reviewer. Your responsibilities:
|
|
59
|
+
- Review code for correctness, clarity, and maintainability
|
|
60
|
+
- Identify security vulnerabilities, performance issues, and logic errors
|
|
61
|
+
- Ensure adherence to project standards and best practices
|
|
62
|
+
- Check for missing error handling, edge cases, and tests
|
|
63
|
+
- Provide specific, actionable, constructive feedback
|
|
64
|
+
|
|
65
|
+
Be thorough. A missed bug in review becomes a production incident.`,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
researcher: {
|
|
69
|
+
description: 'Technical researcher for exploring libraries, evaluating approaches, and gathering context before implementation.',
|
|
70
|
+
tools: 'Read, Bash, Grep, Glob, WebSearch, WebFetch',
|
|
71
|
+
color: 'cyan',
|
|
72
|
+
system: `You are an expert technical researcher. Your responsibilities:
|
|
73
|
+
- Research and evaluate libraries, frameworks, and technical approaches
|
|
74
|
+
- Gather relevant context, documentation, and prior art
|
|
75
|
+
- Compare alternatives with clear trade-off analysis
|
|
76
|
+
- Validate assumptions before committing to an approach
|
|
77
|
+
- Document findings concisely with evidence
|
|
78
|
+
|
|
79
|
+
Provide evidence-based, objective analysis. Cite sources. Avoid bias toward familiarity.`,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
debugger: {
|
|
83
|
+
description: 'Debugging specialist for diagnosing root causes of bugs and failures systematically. Use when something is broken and the cause is unclear.',
|
|
84
|
+
tools: 'Read, Bash, Grep, Glob',
|
|
85
|
+
color: 'red',
|
|
86
|
+
system: `You are an expert debugging specialist. Your responsibilities:
|
|
87
|
+
- Diagnose root causes of bugs and failures using the scientific method
|
|
88
|
+
- Form hypotheses, design experiments, verify results
|
|
89
|
+
- Analyse logs, stack traces, error messages, and reproduction steps
|
|
90
|
+
- Identify patterns across failures
|
|
91
|
+
- Propose targeted, minimal fixes — not workarounds
|
|
92
|
+
|
|
93
|
+
Understand WHY something is broken before touching any code.`,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
planner: {
|
|
97
|
+
description: 'Technical project planner for task decomposition, dependency mapping, and implementation strategy. Use at the start of complex tasks.',
|
|
98
|
+
tools: 'Read, Grep, Glob',
|
|
99
|
+
color: 'orange',
|
|
100
|
+
system: `You are an expert technical project planner. Your responsibilities:
|
|
101
|
+
- Decompose complex tasks into ordered, actionable steps
|
|
102
|
+
- Identify dependencies, blockers, and critical paths
|
|
103
|
+
- Create phased implementation plans with clear milestones
|
|
104
|
+
- Estimate effort and flag high-risk areas
|
|
105
|
+
- Coordinate multi-agent workflows efficiently
|
|
106
|
+
|
|
107
|
+
Plans must be specific and executable, not vague intentions.`,
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
security: {
|
|
111
|
+
description: 'Security auditor for identifying vulnerabilities, reviewing auth flows, and hardening code. Use before shipping security-sensitive changes.',
|
|
112
|
+
tools: 'Read, Grep, Glob, Bash',
|
|
113
|
+
color: 'pink',
|
|
114
|
+
system: `You are an expert security engineer and auditor. Your responsibilities:
|
|
115
|
+
- Identify vulnerabilities: injection, broken auth, data exposure, IDOR, SSRF, XSS, etc.
|
|
116
|
+
- Review authentication, authorisation, and session management
|
|
117
|
+
- Audit dependencies for known CVEs
|
|
118
|
+
- Recommend specific mitigations and secure coding patterns
|
|
119
|
+
- Validate input handling at all system boundaries
|
|
120
|
+
|
|
121
|
+
Think like an attacker. Never assume internal data is safe.`,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ──────────────────────────────────────────────
|
|
126
|
+
// MCP server definitions
|
|
127
|
+
// ──────────────────────────────────────────────
|
|
128
|
+
function buildMcpConfig(githubToken, cwd) {
|
|
129
|
+
return {
|
|
130
|
+
mcpServers: {
|
|
131
|
+
github: {
|
|
132
|
+
command: 'npx',
|
|
133
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
134
|
+
env: {
|
|
135
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken || '${GITHUB_TOKEN}',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
filesystem: {
|
|
139
|
+
command: 'npx',
|
|
140
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', cwd],
|
|
141
|
+
},
|
|
142
|
+
memory: {
|
|
143
|
+
command: 'npx',
|
|
144
|
+
args: ['-y', '@modelcontextprotocol/server-memory'],
|
|
145
|
+
},
|
|
146
|
+
context7: {
|
|
147
|
+
command: 'npx',
|
|
148
|
+
args: ['-y', '--package=@upstash/context7-mcp', 'context7-mcp'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ──────────────────────────────────────────────
|
|
155
|
+
// Build the markdown content for an agent file
|
|
156
|
+
// ──────────────────────────────────────────────
|
|
157
|
+
function buildAgentFile(name, template) {
|
|
158
|
+
return [
|
|
159
|
+
'---',
|
|
160
|
+
`name: ${name}`,
|
|
161
|
+
`description: ${template.description}`,
|
|
162
|
+
`tools: ${template.tools}`,
|
|
163
|
+
'---',
|
|
164
|
+
'',
|
|
165
|
+
template.system,
|
|
166
|
+
'',
|
|
167
|
+
].join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ──────────────────────────────────────────────
|
|
171
|
+
// Interactive prompt helper
|
|
172
|
+
// ──────────────────────────────────────────────
|
|
173
|
+
function prompt(question) {
|
|
174
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
rl.question(question, (answer) => {
|
|
177
|
+
rl.close();
|
|
178
|
+
resolve(answer.trim());
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ──────────────────────────────────────────────
|
|
184
|
+
// setup()
|
|
185
|
+
// ──────────────────────────────────────────────
|
|
186
|
+
async function setup({
|
|
187
|
+
githubToken,
|
|
188
|
+
agents: agentSpec,
|
|
189
|
+
mcpOnly = false,
|
|
190
|
+
agentsOnly = false,
|
|
191
|
+
force = false,
|
|
192
|
+
cwd: cwdArg,
|
|
193
|
+
interactive = true,
|
|
194
|
+
}) {
|
|
195
|
+
const cwd = cwdArg || process.cwd();
|
|
196
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
197
|
+
const agentsDir = path.join(claudeDir, 'agents');
|
|
198
|
+
const mcpFile = path.join(claudeDir, 'mcp.json');
|
|
199
|
+
|
|
200
|
+
// Resolve which roles to create
|
|
201
|
+
const allRoles = Object.keys(AGENT_TEMPLATES);
|
|
202
|
+
let rolesToCreate = allRoles;
|
|
203
|
+
if (agentSpec) {
|
|
204
|
+
const requested = agentSpec.split(',').map(r => r.trim());
|
|
205
|
+
const unknown = requested.filter(r => !AGENT_TEMPLATES[r]);
|
|
206
|
+
if (unknown.length) {
|
|
207
|
+
console.warn(`[lazy] Unknown agent roles (skipped): ${unknown.join(', ')}`);
|
|
208
|
+
console.warn(`[lazy] Available: ${allRoles.join(', ')}`);
|
|
209
|
+
}
|
|
210
|
+
rolesToCreate = requested.filter(r => AGENT_TEMPLATES[r]);
|
|
211
|
+
if (!rolesToCreate.length) {
|
|
212
|
+
console.error('[lazy] No valid agent roles to create. Aborting.');
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Resolve GitHub token
|
|
218
|
+
let resolvedToken = githubToken || process.env.GITHUB_TOKEN || '';
|
|
219
|
+
if (!agentsOnly && !resolvedToken && interactive) {
|
|
220
|
+
resolvedToken = await prompt(
|
|
221
|
+
'[lazy] GitHub Personal Access Token (Enter to use $GITHUB_TOKEN env var): '
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Ensure .claude/ dir exists
|
|
226
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
let created = 0;
|
|
229
|
+
let skipped = 0;
|
|
230
|
+
|
|
231
|
+
// ── Create agent files ──
|
|
232
|
+
if (!mcpOnly) {
|
|
233
|
+
if (!fs.existsSync(agentsDir)) fs.mkdirSync(agentsDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
for (const role of rolesToCreate) {
|
|
236
|
+
const filePath = path.join(agentsDir, `${role}.md`);
|
|
237
|
+
if (fs.existsSync(filePath) && !force) {
|
|
238
|
+
console.log(`[lazy] skip .claude/agents/${role}.md (--force to overwrite)`);
|
|
239
|
+
skipped++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
fs.writeFileSync(filePath, buildAgentFile(role, AGENT_TEMPLATES[role]), 'utf8');
|
|
243
|
+
console.log(`[lazy] wrote .claude/agents/${role}.md`);
|
|
244
|
+
created++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Create mcp.json ──
|
|
249
|
+
if (!agentsOnly) {
|
|
250
|
+
if (fs.existsSync(mcpFile) && !force) {
|
|
251
|
+
console.log('[lazy] skip .claude/mcp.json (--force to overwrite)');
|
|
252
|
+
skipped++;
|
|
253
|
+
} else {
|
|
254
|
+
const config = buildMcpConfig(resolvedToken, cwd);
|
|
255
|
+
fs.writeFileSync(mcpFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
256
|
+
console.log('[lazy] wrote .claude/mcp.json');
|
|
257
|
+
created++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Summary ──
|
|
262
|
+
console.log(`\n[lazy] Setup complete — ${created} created, ${skipped} skipped.\n`);
|
|
263
|
+
|
|
264
|
+
if (!mcpOnly && rolesToCreate.length) {
|
|
265
|
+
console.log(`Agents (${rolesToCreate.length}):${rolesToCreate.map(r => `\n ${r.padEnd(12)} ${AGENT_TEMPLATES[r].description.split('.')[0]}`).join('')}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!agentsOnly) {
|
|
269
|
+
const tokenNote = resolvedToken
|
|
270
|
+
? '(token saved)'
|
|
271
|
+
: '(set GITHUB_TOKEN env var or re-run with --github-token)';
|
|
272
|
+
console.log(`\nMCP servers:
|
|
273
|
+
github @modelcontextprotocol/server-github ${tokenNote}
|
|
274
|
+
filesystem @modelcontextprotocol/server-filesystem
|
|
275
|
+
memory @modelcontextprotocol/server-memory
|
|
276
|
+
context7 @upstash/context7-mcp`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { setup, buildAgentFile, buildMcpConfig, AGENT_TEMPLATES };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
const { setup, buildAgentFile, buildMcpConfig, AGENT_TEMPLATES } = require('./setup');
|
|
8
|
+
|
|
9
|
+
const LAZY_BIN = path.join(__dirname, '..', 'bin', 'lazy.js');
|
|
10
|
+
|
|
11
|
+
function runLazy(args, opts = {}) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
execFile(process.execPath, [LAZY_BIN, ...args], opts, (err, stdout, stderr) => {
|
|
14
|
+
resolve({ code: err ? err.code : 0, stdout: stdout || '', stderr: stderr || '' });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tmpDir() {
|
|
20
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'lazy-setup-'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
// buildAgentFile
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
describe('buildAgentFile', () => {
|
|
27
|
+
test('produces valid frontmatter with name, description, tools', () => {
|
|
28
|
+
const out = buildAgentFile('developer', AGENT_TEMPLATES.developer);
|
|
29
|
+
expect(out).toMatch(/^---\n/);
|
|
30
|
+
expect(out).toMatch(/name: developer/);
|
|
31
|
+
expect(out).toMatch(/description:/);
|
|
32
|
+
expect(out).toMatch(/tools:/);
|
|
33
|
+
expect(out).toMatch(/---/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('includes the system prompt body after frontmatter', () => {
|
|
37
|
+
const out = buildAgentFile('architect', AGENT_TEMPLATES.architect);
|
|
38
|
+
const parts = out.split('---');
|
|
39
|
+
// parts[0] = '', parts[1] = frontmatter, parts[2] = body
|
|
40
|
+
expect(parts.length).toBeGreaterThanOrEqual(3);
|
|
41
|
+
expect(parts[2].trim().length).toBeGreaterThan(20);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('all eight agent roles produce non-empty files', () => {
|
|
45
|
+
const roles = Object.keys(AGENT_TEMPLATES);
|
|
46
|
+
expect(roles.length).toBe(8);
|
|
47
|
+
for (const role of roles) {
|
|
48
|
+
const out = buildAgentFile(role, AGENT_TEMPLATES[role]);
|
|
49
|
+
expect(out.length).toBeGreaterThan(50);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ──────────────────────────────────────────────
|
|
55
|
+
// buildMcpConfig
|
|
56
|
+
// ──────────────────────────────────────────────
|
|
57
|
+
describe('buildMcpConfig', () => {
|
|
58
|
+
test('returns object with mcpServers key', () => {
|
|
59
|
+
const cfg = buildMcpConfig('ghp_test123', '/tmp');
|
|
60
|
+
expect(cfg).toHaveProperty('mcpServers');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('includes github server with provided token', () => {
|
|
64
|
+
const cfg = buildMcpConfig('ghp_mytoken', '/tmp');
|
|
65
|
+
expect(cfg.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('ghp_mytoken');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('falls back to placeholder when no token given', () => {
|
|
69
|
+
const cfg = buildMcpConfig('', '/tmp');
|
|
70
|
+
expect(cfg.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('${GITHUB_TOKEN}');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('includes filesystem server with cwd', () => {
|
|
74
|
+
const cfg = buildMcpConfig('', '/my/project');
|
|
75
|
+
expect(cfg.mcpServers.filesystem.args).toContain('/my/project');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('includes memory and context7 servers', () => {
|
|
79
|
+
const cfg = buildMcpConfig('', '/tmp');
|
|
80
|
+
expect(cfg.mcpServers).toHaveProperty('memory');
|
|
81
|
+
expect(cfg.mcpServers).toHaveProperty('context7');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('config is JSON-serialisable', () => {
|
|
85
|
+
const cfg = buildMcpConfig('ghp_abc', '/tmp');
|
|
86
|
+
expect(() => JSON.stringify(cfg)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ──────────────────────────────────────────────
|
|
91
|
+
// setup() — unit
|
|
92
|
+
// ──────────────────────────────────────────────
|
|
93
|
+
describe('setup() unit', () => {
|
|
94
|
+
let dir;
|
|
95
|
+
beforeEach(() => { dir = tmpDir(); });
|
|
96
|
+
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
97
|
+
|
|
98
|
+
test('creates .claude/agents/ directory', async () => {
|
|
99
|
+
await setup({ cwd: dir, interactive: false });
|
|
100
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'agents'))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('creates all 8 agent markdown files', async () => {
|
|
104
|
+
await setup({ cwd: dir, interactive: false });
|
|
105
|
+
const files = fs.readdirSync(path.join(dir, '.claude', 'agents'));
|
|
106
|
+
expect(files.length).toBe(8);
|
|
107
|
+
expect(files.every(f => f.endsWith('.md'))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('creates .claude/mcp.json', async () => {
|
|
111
|
+
await setup({ cwd: dir, interactive: false });
|
|
112
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'mcp.json'))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('mcp.json is valid JSON with mcpServers', async () => {
|
|
116
|
+
await setup({ cwd: dir, interactive: false });
|
|
117
|
+
const raw = fs.readFileSync(path.join(dir, '.claude', 'mcp.json'), 'utf8');
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
expect(parsed).toHaveProperty('mcpServers');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('mcp.json includes github token when provided', async () => {
|
|
123
|
+
await setup({ cwd: dir, githubToken: 'ghp_mytoken', interactive: false });
|
|
124
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dir, '.claude', 'mcp.json'), 'utf8'));
|
|
125
|
+
expect(cfg.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('ghp_mytoken');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('--agents-only skips mcp.json', async () => {
|
|
129
|
+
await setup({ cwd: dir, agentsOnly: true, interactive: false });
|
|
130
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'mcp.json'))).toBe(false);
|
|
131
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'agents'))).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('--mcp-only skips agent files', async () => {
|
|
135
|
+
await setup({ cwd: dir, mcpOnly: true, interactive: false });
|
|
136
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'agents'))).toBe(false);
|
|
137
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'mcp.json'))).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('specific --agents creates only those roles', async () => {
|
|
141
|
+
await setup({ cwd: dir, agents: 'architect,developer', interactive: false });
|
|
142
|
+
const files = fs.readdirSync(path.join(dir, '.claude', 'agents'));
|
|
143
|
+
expect(files).toContain('architect.md');
|
|
144
|
+
expect(files).toContain('developer.md');
|
|
145
|
+
expect(files.length).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('does not overwrite existing files without --force', async () => {
|
|
149
|
+
await setup({ cwd: dir, interactive: false });
|
|
150
|
+
const agentFile = path.join(dir, '.claude', 'agents', 'architect.md');
|
|
151
|
+
fs.writeFileSync(agentFile, 'SENTINEL', 'utf8');
|
|
152
|
+
|
|
153
|
+
await setup({ cwd: dir, interactive: false });
|
|
154
|
+
expect(fs.readFileSync(agentFile, 'utf8')).toBe('SENTINEL');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('--force overwrites existing files', async () => {
|
|
158
|
+
await setup({ cwd: dir, interactive: false });
|
|
159
|
+
const agentFile = path.join(dir, '.claude', 'agents', 'architect.md');
|
|
160
|
+
fs.writeFileSync(agentFile, 'SENTINEL', 'utf8');
|
|
161
|
+
|
|
162
|
+
await setup({ cwd: dir, force: true, interactive: false });
|
|
163
|
+
expect(fs.readFileSync(agentFile, 'utf8')).not.toBe('SENTINEL');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('unknown role in --agents is skipped with warning', async () => {
|
|
167
|
+
// Should not throw; valid roles still created
|
|
168
|
+
await setup({ cwd: dir, agents: 'developer,nonexistent', interactive: false });
|
|
169
|
+
const files = fs.readdirSync(path.join(dir, '.claude', 'agents'));
|
|
170
|
+
expect(files).toContain('developer.md');
|
|
171
|
+
expect(files).not.toContain('nonexistent.md');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ──────────────────────────────────────────────
|
|
176
|
+
// lazy setup — CLI integration
|
|
177
|
+
// ──────────────────────────────────────────────
|
|
178
|
+
describe('lazy setup CLI', () => {
|
|
179
|
+
let dir;
|
|
180
|
+
beforeEach(() => { dir = tmpDir(); });
|
|
181
|
+
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
182
|
+
|
|
183
|
+
test('exits 0 in non-interactive mode', async () => {
|
|
184
|
+
const result = await runLazy(['setup', '--no-interactive', '--cwd', dir], { timeout: 15000 });
|
|
185
|
+
expect(result.code).toBe(0);
|
|
186
|
+
}, 18000);
|
|
187
|
+
|
|
188
|
+
test('creates agent files', async () => {
|
|
189
|
+
await runLazy(['setup', '--no-interactive', '--cwd', dir], { timeout: 15000 });
|
|
190
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'agents', 'architect.md'))).toBe(true);
|
|
191
|
+
}, 18000);
|
|
192
|
+
|
|
193
|
+
test('creates mcp.json', async () => {
|
|
194
|
+
await runLazy(['setup', '--no-interactive', '--cwd', dir], { timeout: 15000 });
|
|
195
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'mcp.json'))).toBe(true);
|
|
196
|
+
}, 18000);
|
|
197
|
+
|
|
198
|
+
test('--github-token flag is written into mcp.json', async () => {
|
|
199
|
+
await runLazy(['setup', '--no-interactive', '--github-token', 'ghp_cli_test', '--cwd', dir], { timeout: 15000 });
|
|
200
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dir, '.claude', 'mcp.json'), 'utf8'));
|
|
201
|
+
expect(cfg.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe('ghp_cli_test');
|
|
202
|
+
}, 18000);
|
|
203
|
+
|
|
204
|
+
test('--agents-only does not create mcp.json', async () => {
|
|
205
|
+
await runLazy(['setup', '--no-interactive', '--agents-only', '--cwd', dir], { timeout: 15000 });
|
|
206
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'mcp.json'))).toBe(false);
|
|
207
|
+
}, 18000);
|
|
208
|
+
|
|
209
|
+
test('--mcp-only does not create agent files', async () => {
|
|
210
|
+
await runLazy(['setup', '--no-interactive', '--mcp-only', '--cwd', dir], { timeout: 15000 });
|
|
211
|
+
expect(fs.existsSync(path.join(dir, '.claude', 'agents'))).toBe(false);
|
|
212
|
+
}, 18000);
|
|
213
|
+
|
|
214
|
+
test('prints "Setup complete" in stdout', async () => {
|
|
215
|
+
const result = await runLazy(['setup', '--no-interactive', '--cwd', dir], { timeout: 15000 });
|
|
216
|
+
expect(result.stdout).toMatch(/setup complete/i);
|
|
217
|
+
}, 18000);
|
|
218
|
+
});
|