@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.
@@ -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
+ });