@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
package/bin/lazy.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { program } = require('commander');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { launch } = require('../src/launch');
|
|
8
|
+
|
|
9
|
+
// ──────────────────────────────────────────────
|
|
10
|
+
// Legacy backwards-compat: lazy --agents 3 [--session x]
|
|
11
|
+
// Detect this form BEFORE commander parses anything, so it doesn't
|
|
12
|
+
// interfere with subcommand option parsing.
|
|
13
|
+
// ──────────────────────────────────────────────
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const isLegacy =
|
|
16
|
+
argv.length > 0 &&
|
|
17
|
+
!['launch', 'init', 'start', 'register', 'doctor', 'setup', '--help', '-h', '--version', '-V'].includes(argv[0]) &&
|
|
18
|
+
argv.some(a => a === '--agents' || a.startsWith('--agents='));
|
|
19
|
+
|
|
20
|
+
if (isLegacy) {
|
|
21
|
+
// Parse --agents and --session from raw argv manually
|
|
22
|
+
let agents = null;
|
|
23
|
+
let session = null;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
if (argv[i] === '--agents' && argv[i + 1]) agents = argv[++i];
|
|
26
|
+
else if (argv[i].startsWith('--agents=')) agents = argv[i].split('=')[1];
|
|
27
|
+
else if (argv[i] === '--session' && argv[i + 1]) session = argv[++i];
|
|
28
|
+
else if (argv[i].startsWith('--session=')) session = argv[i].split('=')[1];
|
|
29
|
+
}
|
|
30
|
+
if (agents) {
|
|
31
|
+
console.warn('[lazy] Warning: top-level --agents is deprecated. Use: lazy launch --agents');
|
|
32
|
+
launch({ agents, session: session || null, cwd: process.cwd(), platform: null });
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.name('lazy')
|
|
39
|
+
.description('Launch Claude Code agents with automatic rate limit handling');
|
|
40
|
+
|
|
41
|
+
// ──────────────────────────────────────────────
|
|
42
|
+
// lazy launch
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
program
|
|
45
|
+
.command('launch')
|
|
46
|
+
.description('Launch agents in terminal tabs')
|
|
47
|
+
.option('--agents <spec>', 'Number of agents or comma-separated roles (e.g. "3" or "architect,developer,tester")')
|
|
48
|
+
.option('--session <code>', 'Workspace session code (e.g. "swift-oak-17")')
|
|
49
|
+
.option('--cwd <path>', 'Working directory (default: current directory)')
|
|
50
|
+
.option('--platform <platform>', 'Terminal platform override: windows | mac | linux')
|
|
51
|
+
.action((opts) => {
|
|
52
|
+
if (!opts.agents) {
|
|
53
|
+
console.error('Error: --agents is required.\nExamples:\n lazy launch --agents 3 --session swift-oak-17\n lazy launch --agents architect,developer,tester');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
launch({
|
|
57
|
+
agents: opts.agents,
|
|
58
|
+
session: opts.session || null,
|
|
59
|
+
cwd: opts.cwd || process.cwd(),
|
|
60
|
+
platform: opts.platform || null,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ──────────────────────────────────────────────
|
|
65
|
+
// lazy init
|
|
66
|
+
// ──────────────────────────────────────────────
|
|
67
|
+
program
|
|
68
|
+
.command('init')
|
|
69
|
+
.description('Create a .lazy/config.json in the current directory')
|
|
70
|
+
.option('--agents <spec>', 'Number of agents or comma-separated roles')
|
|
71
|
+
.option('--session <code>', 'Workspace session code')
|
|
72
|
+
.action((opts) => {
|
|
73
|
+
const configDir = path.join(process.cwd(), '.lazy');
|
|
74
|
+
const configFile = path.join(configDir, 'config.json');
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(configDir)) {
|
|
77
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const config = {
|
|
81
|
+
agents: opts.agents || '3',
|
|
82
|
+
session: opts.session || null,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
86
|
+
|
|
87
|
+
console.log('[lazy] Created .lazy/config.json');
|
|
88
|
+
console.log('[lazy] Config:', JSON.stringify(config));
|
|
89
|
+
console.log('[lazy] Next steps:');
|
|
90
|
+
console.log('[lazy] lazy setup — create .claude/agents/ and .claude/mcp.json');
|
|
91
|
+
console.log('[lazy] lazy start — launch agents');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ──────────────────────────────────────────────
|
|
95
|
+
// lazy start
|
|
96
|
+
// ──────────────────────────────────────────────
|
|
97
|
+
program
|
|
98
|
+
.command('start')
|
|
99
|
+
.description('Launch agents using .lazy/config.json (falls back to flags)')
|
|
100
|
+
.option('--agents <spec>', 'Fallback agent spec if no config file found')
|
|
101
|
+
.option('--session <code>', 'Fallback session code if no config file found')
|
|
102
|
+
.option('--cwd <path>', 'Working directory (default: current directory)')
|
|
103
|
+
.option('--platform <platform>', 'Terminal platform override: windows | mac | linux')
|
|
104
|
+
.action((opts) => {
|
|
105
|
+
const configFile = path.join(process.cwd(), '.lazy', 'config.json');
|
|
106
|
+
let config = {};
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(configFile)) {
|
|
109
|
+
try {
|
|
110
|
+
config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
111
|
+
console.log('[lazy] Loaded config from .lazy/config.json');
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('[lazy] Failed to parse .lazy/config.json:', err.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log('[lazy] No .lazy/config.json found — falling back to flags');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const agents = config.agents || opts.agents;
|
|
121
|
+
if (!agents) {
|
|
122
|
+
console.error('Error: --agents is required when no .lazy/config.json exists.');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
launch({
|
|
127
|
+
agents,
|
|
128
|
+
session: config.session || opts.session || null,
|
|
129
|
+
cwd: opts.cwd || process.cwd(),
|
|
130
|
+
platform: opts.platform || null,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ──────────────────────────────────────────────
|
|
135
|
+
// lazy register
|
|
136
|
+
// ──────────────────────────────────────────────
|
|
137
|
+
program
|
|
138
|
+
.command('register')
|
|
139
|
+
.description('Show the claude command that registers an agent in the backend workspace')
|
|
140
|
+
.option('--name <name>', 'Agent name (e.g. "architect-1")')
|
|
141
|
+
.option('--role <role>', 'Agent role (e.g. "architect")')
|
|
142
|
+
.option('--session <code>', 'Workspace session code')
|
|
143
|
+
.option('--server <url>', 'Backend server URL (e.g. "http://localhost:3000")')
|
|
144
|
+
.action((opts) => {
|
|
145
|
+
const missing = ['name', 'role', 'session', 'server'].filter(k => !opts[k]);
|
|
146
|
+
if (missing.length) {
|
|
147
|
+
console.error(`Error: missing required flags: ${missing.map(k => '--' + k).join(', ')}`);
|
|
148
|
+
console.error('Usage: lazy register --name architect-1 --role architect --session swift-oak-17 --server http://localhost:3000');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const prompt = [
|
|
153
|
+
`Register as ${opts.name}, role ${opts.role}`,
|
|
154
|
+
`session code: ${opts.session}`,
|
|
155
|
+
`server: ${opts.server}`,
|
|
156
|
+
].join(', ');
|
|
157
|
+
|
|
158
|
+
const cmd = `claude '${prompt}'`;
|
|
159
|
+
console.log('[lazy] Command to run:');
|
|
160
|
+
console.log(cmd);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ──────────────────────────────────────────────
|
|
164
|
+
// lazy setup
|
|
165
|
+
// ──────────────────────────────────────────────
|
|
166
|
+
program
|
|
167
|
+
.command('setup')
|
|
168
|
+
.description('Create .claude/agents/ sub-agents and .claude/mcp.json for Claude Code')
|
|
169
|
+
.option('--github-token <token>', 'GitHub Personal Access Token (falls back to $GITHUB_TOKEN)')
|
|
170
|
+
.option('--agents <roles>', 'Comma-separated roles to create (default: all). Options: architect,developer,tester,reviewer,researcher,debugger,planner,security')
|
|
171
|
+
.option('--mcp-only', 'Only create .claude/mcp.json, skip agent files')
|
|
172
|
+
.option('--agents-only', 'Only create .claude/agents/, skip mcp.json')
|
|
173
|
+
.option('--force', 'Overwrite existing files')
|
|
174
|
+
.option('--cwd <path>', 'Target directory (default: current directory)')
|
|
175
|
+
.option('--no-interactive', 'Skip interactive prompts (CI-friendly)')
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
const { setup } = require('../src/setup');
|
|
178
|
+
await setup({
|
|
179
|
+
githubToken: opts.githubToken || null,
|
|
180
|
+
agents: opts.agents || null,
|
|
181
|
+
mcpOnly: opts.mcpOnly || false,
|
|
182
|
+
agentsOnly: opts.agentsOnly || false,
|
|
183
|
+
force: opts.force || false,
|
|
184
|
+
cwd: opts.cwd || process.cwd(),
|
|
185
|
+
interactive: opts.interactive !== false,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ──────────────────────────────────────────────
|
|
190
|
+
// lazy doctor
|
|
191
|
+
// ──────────────────────────────────────────────
|
|
192
|
+
program
|
|
193
|
+
.command('doctor')
|
|
194
|
+
.description('Check environment: node version, Windows Terminal, claude CLI')
|
|
195
|
+
.action(async () => {
|
|
196
|
+
const { doctor } = require('../src/doctor');
|
|
197
|
+
await doctor();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lazyagent/lazy-agents",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Launch multiple Claude Code agents with automatic rate limit handling",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"claude",
|
|
8
|
+
"ai",
|
|
9
|
+
"agents",
|
|
10
|
+
"cli",
|
|
11
|
+
"automation"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/oshri1997/let-me-being-lazy.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/oshri1997/let-me-being-lazy#readme",
|
|
18
|
+
"bin": {
|
|
19
|
+
"lazy": "./bin/lazy.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./src/launch.js",
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "jest"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.0.0",
|
|
32
|
+
"node-pty": "^1.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"jest": "^29.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/cli.test.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const LAZY_BIN = path.join(__dirname, '..', 'bin', 'lazy.js');
|
|
9
|
+
|
|
10
|
+
// Helper: run lazy CLI, returns { code, stdout, stderr }
|
|
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
|
+
// ──────────────────────────────────────────────
|
|
20
|
+
// lazy init
|
|
21
|
+
// ──────────────────────────────────────────────
|
|
22
|
+
describe('lazy init', () => {
|
|
23
|
+
let tmpDir;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lazy-init-'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('creates .lazy/config.json with default agents=3', async () => {
|
|
34
|
+
const result = await runLazy(['init'], { cwd: tmpDir, timeout: 8000 });
|
|
35
|
+
expect(result.code).toBe(0);
|
|
36
|
+
const configFile = path.join(tmpDir, '.lazy', 'config.json');
|
|
37
|
+
expect(fs.existsSync(configFile)).toBe(true);
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
39
|
+
expect(config.agents).toBe('3');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('creates .lazy/config.json with --agents flag', async () => {
|
|
43
|
+
const result = await runLazy(['init', '--agents', 'architect,developer'], { cwd: tmpDir, timeout: 8000 });
|
|
44
|
+
expect(result.code).toBe(0);
|
|
45
|
+
const configFile = path.join(tmpDir, '.lazy', 'config.json');
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
47
|
+
expect(config.agents).toBe('architect,developer');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('creates .lazy/config.json with --session flag', async () => {
|
|
51
|
+
const result = await runLazy(['init', '--session', 'swift-oak-17'], { cwd: tmpDir, timeout: 8000 });
|
|
52
|
+
expect(result.code).toBe(0);
|
|
53
|
+
const configFile = path.join(tmpDir, '.lazy', 'config.json');
|
|
54
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
55
|
+
expect(config.session).toBe('swift-oak-17');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('prints next step message', async () => {
|
|
59
|
+
const result = await runLazy(['init'], { cwd: tmpDir, timeout: 8000 });
|
|
60
|
+
expect(result.stdout).toMatch(/lazy start/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('config.json is valid JSON', async () => {
|
|
64
|
+
await runLazy(['init'], { cwd: tmpDir, timeout: 8000 });
|
|
65
|
+
const raw = fs.readFileSync(path.join(tmpDir, '.lazy', 'config.json'), 'utf8');
|
|
66
|
+
expect(() => JSON.parse(raw)).not.toThrow();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('overwrites existing config without error', async () => {
|
|
70
|
+
await runLazy(['init', '--agents', '2'], { cwd: tmpDir, timeout: 8000 });
|
|
71
|
+
const result = await runLazy(['init', '--agents', '5'], { cwd: tmpDir, timeout: 8000 });
|
|
72
|
+
expect(result.code).toBe(0);
|
|
73
|
+
const config = JSON.parse(fs.readFileSync(path.join(tmpDir, '.lazy', 'config.json'), 'utf8'));
|
|
74
|
+
expect(config.agents).toBe('5');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ──────────────────────────────────────────────
|
|
79
|
+
// lazy launch
|
|
80
|
+
// ──────────────────────────────────────────────
|
|
81
|
+
describe('lazy launch', () => {
|
|
82
|
+
test('exits non-zero and prints error when --agents is missing', async () => {
|
|
83
|
+
const result = await runLazy(['launch'], { timeout: 5000 });
|
|
84
|
+
expect(result.code).not.toBe(0);
|
|
85
|
+
expect(result.stderr + result.stdout).toMatch(/--agents is required/i);
|
|
86
|
+
}, 8000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ──────────────────────────────────────────────
|
|
90
|
+
// lazy doctor
|
|
91
|
+
// ──────────────────────────────────────────────
|
|
92
|
+
describe('lazy doctor', () => {
|
|
93
|
+
test('exits 0 and prints check results', async () => {
|
|
94
|
+
const result = await runLazy(['doctor'], { timeout: 10000 });
|
|
95
|
+
// Doctor should not crash — exit 0 even if some checks fail
|
|
96
|
+
expect(result.code).toBe(0);
|
|
97
|
+
// Should output tick or cross marks
|
|
98
|
+
expect(result.stdout).toMatch(/[✓✗]/);
|
|
99
|
+
}, 12000);
|
|
100
|
+
|
|
101
|
+
test('reports Node.js version', async () => {
|
|
102
|
+
const result = await runLazy(['doctor'], { timeout: 10000 });
|
|
103
|
+
expect(result.stdout).toMatch(/Node\.js/);
|
|
104
|
+
}, 12000);
|
|
105
|
+
|
|
106
|
+
test('reports claude CLI check', async () => {
|
|
107
|
+
const result = await runLazy(['doctor'], { timeout: 10000 });
|
|
108
|
+
expect(result.stdout).toMatch(/claude/i);
|
|
109
|
+
}, 12000);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ──────────────────────────────────────────────
|
|
113
|
+
// lazy register
|
|
114
|
+
// ──────────────────────────────────────────────
|
|
115
|
+
describe('lazy register', () => {
|
|
116
|
+
test('prints the claude command when all flags provided', async () => {
|
|
117
|
+
const result = await runLazy([
|
|
118
|
+
'register',
|
|
119
|
+
'--name', 'architect-1',
|
|
120
|
+
'--role', 'architect',
|
|
121
|
+
'--session', 'swift-oak-17',
|
|
122
|
+
'--server', 'http://localhost:3000',
|
|
123
|
+
], { timeout: 5000 });
|
|
124
|
+
expect(result.code).toBe(0);
|
|
125
|
+
expect(result.stdout).toMatch(/claude/);
|
|
126
|
+
expect(result.stdout).toMatch(/architect-1/);
|
|
127
|
+
expect(result.stdout).toMatch(/swift-oak-17/);
|
|
128
|
+
}, 8000);
|
|
129
|
+
|
|
130
|
+
test('exits non-zero when flags are missing', async () => {
|
|
131
|
+
const result = await runLazy(['register', '--name', 'arch-1'], { timeout: 5000 });
|
|
132
|
+
expect(result.code).not.toBe(0);
|
|
133
|
+
expect(result.stderr + result.stdout).toMatch(/missing required flags/i);
|
|
134
|
+
}, 8000);
|
|
135
|
+
});
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('child_process');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
function check(label, pass, fixMsg) {
|
|
7
|
+
const icon = pass ? '\u2713' : '\u2717';
|
|
8
|
+
console.log(` ${icon} ${label}`);
|
|
9
|
+
if (!pass && fixMsg) console.log(` ${fixMsg}`);
|
|
10
|
+
return pass;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function which(bin) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const cmd = os.platform() === 'win32' ? 'where' : 'which';
|
|
16
|
+
execFile(cmd, [bin], (err) => resolve(!err));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function doctor() {
|
|
21
|
+
console.log('[lazy] Running environment checks...\n');
|
|
22
|
+
|
|
23
|
+
// Node version
|
|
24
|
+
const nodeVersion = process.versions.node;
|
|
25
|
+
const [major] = nodeVersion.split('.').map(Number);
|
|
26
|
+
check(
|
|
27
|
+
`Node.js ${nodeVersion}`,
|
|
28
|
+
major >= 18,
|
|
29
|
+
'Node.js 18+ is required. Download from https://nodejs.org'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// claude CLI
|
|
33
|
+
const hasClaude = await which('claude');
|
|
34
|
+
check(
|
|
35
|
+
'claude CLI',
|
|
36
|
+
hasClaude,
|
|
37
|
+
'Claude Code CLI not found. Install via: npm install -g @anthropic-ai/claude-code'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Platform-specific terminal
|
|
41
|
+
const platform = os.platform();
|
|
42
|
+
if (platform === 'win32') {
|
|
43
|
+
const hasWt = await which('wt');
|
|
44
|
+
check(
|
|
45
|
+
'Windows Terminal (wt)',
|
|
46
|
+
hasWt,
|
|
47
|
+
'Install Windows Terminal from https://aka.ms/terminal or the Microsoft Store'
|
|
48
|
+
);
|
|
49
|
+
} else if (platform === 'darwin') {
|
|
50
|
+
// Terminal.app always exists on macOS — check for osascript instead
|
|
51
|
+
const hasOsascript = await which('osascript');
|
|
52
|
+
check(
|
|
53
|
+
'osascript (Terminal automation)',
|
|
54
|
+
hasOsascript,
|
|
55
|
+
'osascript is part of macOS — this check should always pass'
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
const hasGnome = await which('gnome-terminal');
|
|
59
|
+
const hasXterm = await which('xterm');
|
|
60
|
+
check(
|
|
61
|
+
'gnome-terminal or xterm',
|
|
62
|
+
hasGnome || hasXterm,
|
|
63
|
+
'Install a terminal emulator: sudo apt install gnome-terminal OR sudo apt install xterm'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('\n[lazy] Doctor done.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { doctor };
|
package/src/launch.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const WRAPPER = path.join(__dirname, 'wrapper.js');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ROLES = ['architect', 'developer', 'tester'];
|
|
11
|
+
|
|
12
|
+
function parseAgentSpec(spec) {
|
|
13
|
+
const num = parseInt(spec, 10);
|
|
14
|
+
if (!isNaN(num) && String(num) === spec.trim()) {
|
|
15
|
+
return Array.from({ length: num }, (_, i) => ({
|
|
16
|
+
name: `${DEFAULT_ROLES[i] || 'generic'}-${i + 1}`,
|
|
17
|
+
role: DEFAULT_ROLES[i] || 'generic',
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
return spec.split(',').map((role, i) => ({
|
|
21
|
+
name: `${role.trim()}-${i + 1}`,
|
|
22
|
+
role: role.trim(),
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ──────────────────────────────────────────────
|
|
27
|
+
// Windows Terminal (wt) command builder
|
|
28
|
+
// ──────────────────────────────────────────────
|
|
29
|
+
function buildWtCommand(agents, session, wrapperPath, cwd = process.cwd()) {
|
|
30
|
+
const fwdWrapper = wrapperPath.replace(/\\/g, '/');
|
|
31
|
+
const tabs = agents.map((a, i) => {
|
|
32
|
+
const prompt = `Register as ${a.name}, role ${a.role}${session ? ', session code: ' + session : ''}`;
|
|
33
|
+
const tmpFile = path.join(os.tmpdir(), `lazy-prompt-${Date.now()}-${i}.txt`).replace(/\\/g, '/');
|
|
34
|
+
fs.writeFileSync(tmpFile, prompt, 'utf8');
|
|
35
|
+
const prefix = i === 0 ? 'new-tab' : '; new-tab';
|
|
36
|
+
return `${prefix} --startingDirectory "${cwd}" --title "${a.name} (${a.role})" -- powershell -NoExit -Command "node '${fwdWrapper}' '${tmpFile}'"`;
|
|
37
|
+
});
|
|
38
|
+
return `wt ${tabs.join('')}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ──────────────────────────────────────────────
|
|
42
|
+
// macOS — open one Terminal.app window per agent via osascript
|
|
43
|
+
// ──────────────────────────────────────────────
|
|
44
|
+
function buildMacCommand(agents, session, wrapperPath, cwd = process.cwd()) {
|
|
45
|
+
const cmds = agents.map((a) => {
|
|
46
|
+
const prompt = `Register as ${a.name}, role ${a.role}${session ? ', session code: ' + session : ''}`;
|
|
47
|
+
const escaped = prompt.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
48
|
+
const nodeCmd = `node '${wrapperPath}' '${escaped}'`;
|
|
49
|
+
// osascript: tell Terminal to open a new window and run the command
|
|
50
|
+
return (
|
|
51
|
+
`osascript -e 'tell application "Terminal" to do script` +
|
|
52
|
+
` "cd ${cwd} && ${nodeCmd}"'`
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
// Chain all osascript calls with &&
|
|
56
|
+
return cmds.join(' && ');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ──────────────────────────────────────────────
|
|
60
|
+
// Linux — try gnome-terminal, fall back to xterm
|
|
61
|
+
// ──────────────────────────────────────────────
|
|
62
|
+
function buildLinuxCommand(agents, session, wrapperPath, cwd = process.cwd()) {
|
|
63
|
+
const tabs = agents.map((a) => {
|
|
64
|
+
const prompt = `Register as ${a.name}, role ${a.role}${session ? ', session code: ' + session : ''}`;
|
|
65
|
+
const escaped = prompt.replace(/"/g, '\\"');
|
|
66
|
+
return `--tab --title="${a.name} (${a.role})" --working-directory="${cwd}" -- bash -c "node '${wrapperPath}' '${escaped}'; exec bash"`;
|
|
67
|
+
});
|
|
68
|
+
return `gnome-terminal ${tabs.join(' ')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ──────────────────────────────────────────────
|
|
72
|
+
// Unified platform dispatcher
|
|
73
|
+
// ──────────────────────────────────────────────
|
|
74
|
+
function detectPlatform() {
|
|
75
|
+
const p = os.platform();
|
|
76
|
+
if (p === 'win32') return 'windows';
|
|
77
|
+
if (p === 'darwin') return 'mac';
|
|
78
|
+
return 'linux';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a terminal command for the given platform.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} platform 'windows' | 'mac' | 'linux'
|
|
85
|
+
* @param {Array} agents parsed agent objects [{name, role}]
|
|
86
|
+
* @param {string} session session code or null
|
|
87
|
+
* @param {string} wrapperPath absolute path to wrapper.js
|
|
88
|
+
* @param {string} cwd working directory
|
|
89
|
+
* @returns {string} shell command to execute
|
|
90
|
+
*/
|
|
91
|
+
function buildTerminalCommand(platform, agents, session, wrapperPath, cwd = process.cwd()) {
|
|
92
|
+
switch (platform) {
|
|
93
|
+
case 'mac': return buildMacCommand(agents, session, wrapperPath, cwd);
|
|
94
|
+
case 'linux': return buildLinuxCommand(agents, session, wrapperPath, cwd);
|
|
95
|
+
case 'windows':
|
|
96
|
+
default: return buildWtCommand(agents, session, wrapperPath, cwd);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ──────────────────────────────────────────────
|
|
101
|
+
// Launch
|
|
102
|
+
// ──────────────────────────────────────────────
|
|
103
|
+
function launch({ agents: spec, session, cwd, platform }) {
|
|
104
|
+
const agents = parseAgentSpec(spec);
|
|
105
|
+
const resolvedCwd = cwd || process.cwd();
|
|
106
|
+
const resolvedPlatform = platform || detectPlatform();
|
|
107
|
+
|
|
108
|
+
console.log('[lazy] Launching agents:');
|
|
109
|
+
agents.forEach(a => console.log(` - ${a.name} (${a.role})`));
|
|
110
|
+
if (session) console.log(`[lazy] Session: ${session}`);
|
|
111
|
+
console.log(`[lazy] Platform: ${resolvedPlatform}`);
|
|
112
|
+
|
|
113
|
+
const cmd = buildTerminalCommand(resolvedPlatform, agents, session, WRAPPER, resolvedCwd);
|
|
114
|
+
|
|
115
|
+
const execCmd = resolvedPlatform === 'windows' ? `start "" ${cmd}` : cmd;
|
|
116
|
+
|
|
117
|
+
exec(execCmd, (err) => {
|
|
118
|
+
if (err) {
|
|
119
|
+
const hints = {
|
|
120
|
+
windows: 'Make sure "wt" (Windows Terminal) is installed.',
|
|
121
|
+
mac: 'Make sure Terminal.app is available (or use --platform linux).',
|
|
122
|
+
linux: 'Make sure gnome-terminal or xterm is installed.',
|
|
123
|
+
};
|
|
124
|
+
console.error('[lazy] Failed to launch terminal:', err.message);
|
|
125
|
+
console.error(`[lazy] ${hints[resolvedPlatform] || ''}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
console.log('[lazy] Terminal launched. Each tab handles rate limits automatically.');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { launch, parseAgentSpec, buildWtCommand, buildTerminalCommand, buildMacCommand, buildLinuxCommand };
|