@grantx/fleet-cli 0.1.3 → 0.1.5
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/package.json +2 -2
- package/src/agent-templates.js +354 -0
- package/src/agent-wizard.js +228 -0
- package/src/github-repos.js +159 -0
- package/src/init.js +209 -119
- package/src/prompt-utils.js +127 -0
- package/src/setup-agents.js +6 -3
- package/src/slack-setup.js +153 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// github-repos.js — GitHub repo onboarding for fleet setup.
|
|
2
|
+
// Links repos to give agents codebase context.
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { ask, askList, printHeader } from './prompt-utils.js';
|
|
8
|
+
import { scanCodebase } from './generate.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run the GitHub repo onboarding wizard.
|
|
12
|
+
* @param {readline.Interface} rl
|
|
13
|
+
* @param {string} projectRoot
|
|
14
|
+
* @returns {Promise<Array>} Array of repo definitions for fleet.config.json
|
|
15
|
+
*/
|
|
16
|
+
export async function runGitHubRepos(rl, projectRoot) {
|
|
17
|
+
printHeader('GitHub Repositories');
|
|
18
|
+
console.log(' Link the repos your team works on (gives agents codebase context).\n');
|
|
19
|
+
|
|
20
|
+
// Collect repo slugs
|
|
21
|
+
const repoSlugs = [];
|
|
22
|
+
while (true) {
|
|
23
|
+
const input = await ask(rl, 'Repo (owner/name or URL, blank to finish)');
|
|
24
|
+
if (!input) break;
|
|
25
|
+
|
|
26
|
+
const slug = normalizeRepoInput(input);
|
|
27
|
+
if (!slug) {
|
|
28
|
+
console.log(' Could not parse repo. Use format: owner/name or full GitHub URL');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate with gh CLI if available
|
|
33
|
+
const info = await validateRepo(slug);
|
|
34
|
+
if (info) {
|
|
35
|
+
const langs = info.languages?.length > 0 ? info.languages.join(', ') : 'unknown';
|
|
36
|
+
console.log(` ✓ ${info.name || slug} (${langs})`);
|
|
37
|
+
repoSlugs.push({ slug, info });
|
|
38
|
+
} else {
|
|
39
|
+
console.log(` ⚠ Could not validate (gh CLI may not be installed). Added anyway.`);
|
|
40
|
+
repoSlugs.push({ slug, info: null });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (repoSlugs.length === 0) {
|
|
45
|
+
console.log(' No repositories linked.\n');
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ask how to access repos
|
|
50
|
+
const mode = await askList(rl, 'How should fleet access these repos?', [
|
|
51
|
+
{ label: 'Local path', description: 'Point to existing local checkouts', value: 'local' },
|
|
52
|
+
{ label: 'Clone', description: 'Fleet clones into .fleet/repos/ (shallow)', value: 'clone' },
|
|
53
|
+
{ label: 'URL only', description: 'Just store the slug, agents use gh CLI on demand', value: 'url' },
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const repos = [];
|
|
57
|
+
for (const { slug, info } of repoSlugs) {
|
|
58
|
+
const repo = { slug, mode };
|
|
59
|
+
|
|
60
|
+
if (info) {
|
|
61
|
+
repo.languages = info.languages || [];
|
|
62
|
+
repo.frameworks = info.frameworks || [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (mode === 'local') {
|
|
66
|
+
const repoName = slug.split('/').pop();
|
|
67
|
+
const defaultPath = path.join(path.dirname(projectRoot), repoName);
|
|
68
|
+
const localPath = await ask(rl, ` Local path for ${slug}`, defaultPath);
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(localPath)) {
|
|
71
|
+
repo.path = localPath;
|
|
72
|
+
// Scan for frameworks
|
|
73
|
+
try {
|
|
74
|
+
const scan = scanCodebase(localPath);
|
|
75
|
+
repo.languages = scan.languages || [];
|
|
76
|
+
repo.frameworks = scan.frameworks || [];
|
|
77
|
+
const fwStr = repo.frameworks.length > 0 ? `, ${repo.frameworks.join(', ')}` : '';
|
|
78
|
+
console.log(` ✓ Scanned: ${repo.languages.join(', ')}${fwStr}`);
|
|
79
|
+
} catch { /* scan failed, continue */ }
|
|
80
|
+
} else {
|
|
81
|
+
console.log(` ⚠ Path not found, storing slug only`);
|
|
82
|
+
repo.mode = 'url';
|
|
83
|
+
}
|
|
84
|
+
} else if (mode === 'clone') {
|
|
85
|
+
const reposDir = path.join(projectRoot, '.fleet', 'repos');
|
|
86
|
+
const repoName = slug.split('/').pop();
|
|
87
|
+
const clonePath = path.join(reposDir, repoName);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(reposDir, { recursive: true });
|
|
91
|
+
if (!fs.existsSync(clonePath)) {
|
|
92
|
+
console.log(` Cloning ${slug}...`);
|
|
93
|
+
execSync(`git clone --depth=1 https://github.com/${slug}.git "${clonePath}"`, {
|
|
94
|
+
stdio: 'pipe',
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
repo.path = clonePath;
|
|
99
|
+
|
|
100
|
+
// Scan cloned repo
|
|
101
|
+
try {
|
|
102
|
+
const scan = scanCodebase(clonePath);
|
|
103
|
+
repo.languages = scan.languages || [];
|
|
104
|
+
repo.frameworks = scan.frameworks || [];
|
|
105
|
+
} catch { /* scan failed */ }
|
|
106
|
+
|
|
107
|
+
console.log(` ✓ Cloned to .fleet/repos/${repoName}`);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.log(` ⚠ Clone failed: ${err.message}. Storing slug only.`);
|
|
110
|
+
repo.mode = 'url';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// mode === 'url': nothing extra to do
|
|
114
|
+
|
|
115
|
+
repos.push(repo);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`\n Linked ${repos.length} repository(s).\n`);
|
|
119
|
+
return repos;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalize various repo input formats to owner/name.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeRepoInput(input) {
|
|
128
|
+
// Full URL: https://github.com/owner/name or https://github.com/owner/name.git
|
|
129
|
+
const urlMatch = input.match(/github\.com\/([^/]+\/[^/.\s]+)/);
|
|
130
|
+
if (urlMatch) return urlMatch[1];
|
|
131
|
+
|
|
132
|
+
// owner/name format
|
|
133
|
+
if (/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(input)) {
|
|
134
|
+
return input;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validate a repo exists using gh CLI.
|
|
142
|
+
* Returns repo info or null if gh is unavailable.
|
|
143
|
+
*/
|
|
144
|
+
async function validateRepo(slug) {
|
|
145
|
+
try {
|
|
146
|
+
const output = execSync(
|
|
147
|
+
`gh repo view ${slug} --json name,description,languages`,
|
|
148
|
+
{ stdio: 'pipe', timeout: 10000 }
|
|
149
|
+
).toString();
|
|
150
|
+
const data = JSON.parse(output);
|
|
151
|
+
return {
|
|
152
|
+
name: data.name,
|
|
153
|
+
description: data.description,
|
|
154
|
+
languages: data.languages?.map(l => l.node?.name || l.name || l) || [],
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/init.js
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
// init.js —
|
|
1
|
+
// init.js — Fleet setup wizard: team selection, repo onboarding, agent building,
|
|
2
|
+
// conductor soul, and Slack integration. Produces Clawcob-quality agents.
|
|
2
3
|
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import os from 'node:os';
|
|
6
|
-
import readline from 'node:readline';
|
|
7
7
|
import { scanCodebase, generateRoster } from './generate.js';
|
|
8
8
|
import { setupAgentWorkspaces } from './setup-agents.js';
|
|
9
|
+
import { createReadline, ask, askRequired, askList, isInteractive, printHeader, printStep } from './prompt-utils.js';
|
|
10
|
+
import { runAgentWizard } from './agent-wizard.js';
|
|
11
|
+
import { generateRichAgentClaudeMd, generateConductorClaudeMd } from './agent-templates.js';
|
|
12
|
+
import { runGitHubRepos } from './github-repos.js';
|
|
13
|
+
import { runSlackSetup } from './slack-setup.js';
|
|
14
|
+
|
|
15
|
+
const TEAM_OPTIONS = [
|
|
16
|
+
{ label: 'ML', description: 'Machine learning, ranking, model training', value: 'grantx-ml' },
|
|
17
|
+
{ label: 'Data', description: 'Data engineering, pipelines, infrastructure', value: 'grantx-data' },
|
|
18
|
+
{ label: 'Fullstack', description: 'Frontend, backend, API, UI', value: 'grantx-fullstack' },
|
|
19
|
+
{ label: 'Skunkworks', description: 'Research, experiments, bleeding edge', value: 'grantx-skunkworks' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const TEAM_TYPE_MAP = {
|
|
23
|
+
'grantx-ml': 'ml',
|
|
24
|
+
'grantx-data': 'data',
|
|
25
|
+
'grantx-fullstack': 'fullstack',
|
|
26
|
+
'grantx-skunkworks': 'skunkworks',
|
|
27
|
+
};
|
|
9
28
|
|
|
10
29
|
export default async function init(args) {
|
|
11
30
|
const projectRoot = process.cwd();
|
|
12
|
-
console.log('\
|
|
31
|
+
console.log('\n Fleet Setup');
|
|
13
32
|
console.log(` Project: ${projectRoot}\n`);
|
|
14
33
|
|
|
15
34
|
// Check if already initialized
|
|
@@ -20,176 +39,247 @@ export default async function init(args) {
|
|
|
20
39
|
console.log(' Re-running will update config without destroying sessions.\n');
|
|
21
40
|
}
|
|
22
41
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
42
|
+
// Non-interactive fallback: if all env vars set and no TTY
|
|
43
|
+
const envUrl = process.env.FLEET_SUPABASE_URL || '';
|
|
44
|
+
const envKey = process.env.FLEET_SUPABASE_KEY || '';
|
|
45
|
+
const envTeam = process.env.FLEET_TEAM_ID || '';
|
|
26
46
|
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
if (scan.frameworks.length > 0) {
|
|
31
|
-
console.log(` Frameworks: ${scan.frameworks.join(', ')}`);
|
|
32
|
-
}
|
|
33
|
-
if (scan.infra.length > 0) {
|
|
34
|
-
console.log(` Infrastructure: ${scan.infra.join(', ')}`);
|
|
35
|
-
}
|
|
36
|
-
if (scan.cloud.length > 0) {
|
|
37
|
-
console.log(` Cloud: ${scan.cloud.join(', ')}`);
|
|
47
|
+
if (!isInteractive() && envUrl && envKey && envTeam) {
|
|
48
|
+
return initNonInteractive(projectRoot, configPath, envUrl, envKey, envTeam);
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
.sort((a, b) => b[1] - a[1])
|
|
43
|
-
.slice(0, 5)
|
|
44
|
-
.map(([ext, count]) => `${ext}: ${count}`)
|
|
45
|
-
.join(', ');
|
|
46
|
-
console.log(` Files: ${totalFiles} (${topExts})`);
|
|
47
|
-
if (scan.testFiles > 0) {
|
|
48
|
-
console.log(` Test files: ${scan.testFiles}`);
|
|
49
|
-
}
|
|
50
|
-
console.log('');
|
|
51
|
+
// Interactive wizard
|
|
52
|
+
const rl = createReadline();
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
for (const agent of roster) {
|
|
56
|
-
const type = agent.isConductor ? '(conductor)' : '';
|
|
57
|
-
console.log(` ${agent.name.padEnd(12)} -- ${agent.role} ${type}`);
|
|
58
|
-
}
|
|
59
|
-
console.log('');
|
|
54
|
+
try {
|
|
55
|
+
// ── Phase 1: Team Selection ─────────────────────────────────
|
|
56
|
+
printHeader('Team Selection');
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!teamId) teamId = suggestedTeamId;
|
|
76
|
-
console.log(` Supabase URL: ${supabaseUrl} [from env]`);
|
|
77
|
-
console.log(` Supabase key: ****${supabaseKey.slice(-4)} [from env]`);
|
|
78
|
-
console.log(` Team ID: ${teamId}${process.env.FLEET_TEAM_ID ? ' [from env]' : ' [auto]'}`);
|
|
79
|
-
} else {
|
|
80
|
-
// Interactive prompts
|
|
81
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
-
const ask = (q, dflt) => new Promise(resolve => {
|
|
83
|
-
const prompt = dflt ? `${q} [${dflt}]: ` : `${q}: `;
|
|
84
|
-
rl.question(` ${prompt}`, answer => resolve(answer.trim() || dflt || ''));
|
|
85
|
-
});
|
|
58
|
+
let teamId;
|
|
59
|
+
if (envTeam && TEAM_TYPE_MAP[envTeam]) {
|
|
60
|
+
teamId = envTeam;
|
|
61
|
+
console.log(` Team: ${teamId} [from env]\n`);
|
|
62
|
+
} else {
|
|
63
|
+
teamId = await askList(rl, 'Select your team:', TEAM_OPTIONS);
|
|
64
|
+
}
|
|
65
|
+
const teamType = TEAM_TYPE_MAP[teamId];
|
|
66
|
+
|
|
67
|
+
// ── Phase 2: Supabase Credentials ───────────────────────────
|
|
68
|
+
printHeader('Supabase Credentials');
|
|
69
|
+
|
|
70
|
+
let supabaseUrl = envUrl;
|
|
71
|
+
let supabaseKey = envKey;
|
|
86
72
|
|
|
87
73
|
if (!supabaseUrl) {
|
|
88
|
-
supabaseUrl = await ask('Supabase URL', 'https://svfqcnfxcbwjwbugiiyv.supabase.co');
|
|
74
|
+
supabaseUrl = await ask(rl, 'Supabase URL', 'https://svfqcnfxcbwjwbugiiyv.supabase.co');
|
|
89
75
|
} else {
|
|
90
76
|
console.log(` Supabase URL: ${supabaseUrl} [from env]`);
|
|
91
77
|
}
|
|
92
78
|
|
|
93
79
|
if (!supabaseKey) {
|
|
94
|
-
supabaseKey = await
|
|
95
|
-
if (!supabaseKey) {
|
|
96
|
-
console.error('\n Error: Supabase key is required. Set FLEET_SUPABASE_KEY or provide it here.');
|
|
97
|
-
rl.close();
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
80
|
+
supabaseKey = await askRequired(rl, 'Supabase API key (sb_secret_... or eyJ...)');
|
|
100
81
|
} else {
|
|
101
82
|
console.log(` Supabase key: ****${supabaseKey.slice(-4)} [from env]`);
|
|
102
83
|
}
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
// ── Phase 3: GitHub Repositories ────────────────────────────
|
|
87
|
+
const repos = await runGitHubRepos(rl, projectRoot);
|
|
103
88
|
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
// ── Phase 4: Agent Builder ──────────────────────────────────
|
|
90
|
+
const { workers, conductorInput } = await runAgentWizard(rl, projectRoot, teamType, repos);
|
|
91
|
+
|
|
92
|
+
// Build conductor agent definition
|
|
93
|
+
const conductor = {
|
|
94
|
+
name: 'conductor',
|
|
95
|
+
role: 'Orchestration, planning, task decomposition, sprint management',
|
|
96
|
+
isConductor: true,
|
|
97
|
+
keywords: [],
|
|
98
|
+
filePatterns: [],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const allAgents = [conductor, ...workers];
|
|
102
|
+
|
|
103
|
+
// ── Phase 5: Slack Integration ──────────────────────────────
|
|
104
|
+
const slackConfig = await runSlackSetup(rl, teamId);
|
|
105
|
+
|
|
106
|
+
rl.close();
|
|
107
|
+
|
|
108
|
+
// ── Phase 6: Write Everything ───────────────────────────────
|
|
109
|
+
printHeader('Writing Configuration');
|
|
110
|
+
|
|
111
|
+
// Build fleet.config.json
|
|
112
|
+
const config = {
|
|
113
|
+
version: 2,
|
|
114
|
+
teamId,
|
|
115
|
+
teamType,
|
|
116
|
+
supabase: {
|
|
117
|
+
url: supabaseUrl,
|
|
118
|
+
key: supabaseKey,
|
|
119
|
+
},
|
|
120
|
+
repos: repos.length > 0 ? repos : undefined,
|
|
121
|
+
slack: slackConfig || undefined,
|
|
122
|
+
execution: {
|
|
123
|
+
mode: 'on-demand',
|
|
124
|
+
maxConcurrent: Math.min(workers.length + 1, 5),
|
|
125
|
+
timeouts: {
|
|
126
|
+
task: 1800000,
|
|
127
|
+
review: 300000,
|
|
128
|
+
watch: 180000,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
agents: allAgents,
|
|
132
|
+
daemon: {
|
|
133
|
+
loopIntervalMs: 5000,
|
|
134
|
+
cron: {
|
|
135
|
+
healthCheckMinutes: 10,
|
|
136
|
+
synthesisHours: 2,
|
|
137
|
+
credCheckMinutes: 30,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Write config
|
|
143
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
144
|
+
console.log(' ✓ fleet.config.json');
|
|
145
|
+
|
|
146
|
+
// Setup agent workspaces with rich CLAUDE.md files
|
|
147
|
+
setupAgentWorkspaces(projectRoot, allAgents);
|
|
148
|
+
|
|
149
|
+
// Write rich CLAUDE.md files (overwrite the basic ones from setup-agents)
|
|
150
|
+
for (const agent of workers) {
|
|
151
|
+
const claudeMdPath = path.join(projectRoot, '.fleet', 'agents', agent.name, 'CLAUDE.md');
|
|
152
|
+
const content = generateRichAgentClaudeMd(agent, config);
|
|
153
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
106
154
|
}
|
|
107
155
|
|
|
156
|
+
// Write conductor soul
|
|
157
|
+
const conductorClaudeMdPath = path.join(projectRoot, '.fleet', 'agents', 'conductor', 'CLAUDE.md');
|
|
158
|
+
const conductorContent = generateConductorClaudeMd(conductorInput, allAgents, config);
|
|
159
|
+
fs.writeFileSync(conductorClaudeMdPath, conductorContent);
|
|
160
|
+
|
|
161
|
+
const agentNames = allAgents.map(a => a.name).join(', ');
|
|
162
|
+
console.log(` ✓ .fleet/agents/ (${allAgents.length} agents: ${agentNames})`);
|
|
163
|
+
console.log(' ✓ .fleet/sessions.json');
|
|
164
|
+
|
|
165
|
+
// Count CLAUDE.md lines for display
|
|
166
|
+
const conductorLines = conductorContent.split('\n').length;
|
|
167
|
+
console.log(` ✓ Conductor soul (${conductorLines} lines)`);
|
|
168
|
+
|
|
169
|
+
// Write .mcp.json
|
|
170
|
+
writeMcpJson(projectRoot);
|
|
171
|
+
console.log(' ✓ .mcp.json (Claude Code MCP registration)');
|
|
172
|
+
|
|
173
|
+
// Write slash command
|
|
174
|
+
writeFleetCommand(projectRoot);
|
|
175
|
+
|
|
176
|
+
// Update .gitignore
|
|
177
|
+
updateGitignore(projectRoot);
|
|
178
|
+
|
|
179
|
+
// Summary
|
|
180
|
+
console.log('');
|
|
181
|
+
printStep(1, 6, 'Team Selection', teamId);
|
|
182
|
+
printStep(2, 6, 'Supabase Credentials', `****${supabaseKey.slice(-4)}`);
|
|
183
|
+
printStep(3, 6, 'GitHub Repositories', repos.length > 0 ? `${repos.length} linked` : 'none');
|
|
184
|
+
printStep(4, 6, 'Agent Builder', `${workers.length} workers built`);
|
|
185
|
+
printStep(5, 6, 'Conductor Soul', `${conductorLines} lines`);
|
|
186
|
+
printStep(6, 6, 'Slack Integration', slackConfig ? 'connected' : 'skipped');
|
|
187
|
+
|
|
188
|
+
console.log('\n Done. Run `fleet start` to begin, or open Claude Code and try /fleet status.\n');
|
|
189
|
+
|
|
190
|
+
} catch (err) {
|
|
108
191
|
rl.close();
|
|
192
|
+
throw err;
|
|
109
193
|
}
|
|
110
|
-
|
|
194
|
+
}
|
|
111
195
|
|
|
112
|
-
|
|
113
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Non-interactive fallback: uses env vars and auto-generates agents (v1 behavior).
|
|
198
|
+
*/
|
|
199
|
+
function initNonInteractive(projectRoot, configPath, supabaseUrl, supabaseKey, teamId) {
|
|
200
|
+
console.log(' Non-interactive mode (all env vars set).');
|
|
201
|
+
console.log(' ⚠ Interactive mode produces richer agent definitions.\n');
|
|
114
202
|
|
|
115
|
-
|
|
203
|
+
console.log(` Supabase URL: ${supabaseUrl} [from env]`);
|
|
204
|
+
console.log(` Supabase key: ****${supabaseKey.slice(-4)} [from env]`);
|
|
205
|
+
console.log(` Team ID: ${teamId} [from env]\n`);
|
|
206
|
+
|
|
207
|
+
// Scan and auto-generate
|
|
208
|
+
console.log(' Scanning codebase...');
|
|
209
|
+
const scan = scanCodebase(projectRoot);
|
|
210
|
+
const roster = generateRoster(scan);
|
|
211
|
+
|
|
212
|
+
console.log(` Generated ${roster.length} agents:`);
|
|
213
|
+
for (const agent of roster) {
|
|
214
|
+
const type = agent.isConductor ? '(conductor)' : '';
|
|
215
|
+
console.log(` ${agent.name.padEnd(12)} -- ${agent.role} ${type}`);
|
|
216
|
+
}
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
const teamType = TEAM_TYPE_MAP[teamId] || teamId;
|
|
116
220
|
const config = {
|
|
117
221
|
version: 1,
|
|
118
222
|
teamId,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
key: supabaseKey,
|
|
122
|
-
},
|
|
223
|
+
teamType,
|
|
224
|
+
supabase: { url: supabaseUrl, key: supabaseKey },
|
|
123
225
|
execution: {
|
|
124
226
|
mode: 'on-demand',
|
|
125
227
|
maxConcurrent: 3,
|
|
126
|
-
timeouts: {
|
|
127
|
-
task: 1800000,
|
|
128
|
-
review: 300000,
|
|
129
|
-
watch: 180000,
|
|
130
|
-
},
|
|
228
|
+
timeouts: { task: 1800000, review: 300000, watch: 180000 },
|
|
131
229
|
},
|
|
132
230
|
agents: roster,
|
|
133
231
|
daemon: {
|
|
134
232
|
loopIntervalMs: 5000,
|
|
135
|
-
cron: {
|
|
136
|
-
healthCheckMinutes: 10,
|
|
137
|
-
synthesisHours: 2,
|
|
138
|
-
},
|
|
233
|
+
cron: { healthCheckMinutes: 10, synthesisHours: 2, credCheckMinutes: 30 },
|
|
139
234
|
},
|
|
140
235
|
};
|
|
141
236
|
|
|
142
237
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
143
|
-
console.log('
|
|
238
|
+
console.log(' ✓ fleet.config.json');
|
|
144
239
|
|
|
145
|
-
// Setup agent workspaces
|
|
146
240
|
setupAgentWorkspaces(projectRoot, roster);
|
|
147
|
-
|
|
148
|
-
console.log(
|
|
149
|
-
|
|
241
|
+
console.log(` ✓ .fleet/agents/`);
|
|
242
|
+
console.log(' ✓ .fleet/sessions.json');
|
|
243
|
+
|
|
244
|
+
writeMcpJson(projectRoot);
|
|
245
|
+
console.log(' ✓ .mcp.json');
|
|
246
|
+
|
|
247
|
+
writeFleetCommand(projectRoot);
|
|
248
|
+
updateGitignore(projectRoot);
|
|
150
249
|
|
|
151
|
-
|
|
250
|
+
console.log('\n Done. Run `fleet start` to begin.\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Shared helpers ────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function writeMcpJson(projectRoot) {
|
|
152
256
|
const mcpJsonPath = path.join(projectRoot, '.mcp.json');
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
command: 'npx',
|
|
158
|
-
args: ['-y', '@grantx/fleet-mcp', '--config', './fleet.config.json'],
|
|
159
|
-
},
|
|
160
|
-
},
|
|
257
|
+
const mcpEntry = {
|
|
258
|
+
type: 'stdio',
|
|
259
|
+
command: 'npx',
|
|
260
|
+
args: ['-y', '@grantx/fleet-mcp', '--config', './fleet.config.json'],
|
|
161
261
|
};
|
|
162
262
|
|
|
163
|
-
// Merge with existing .mcp.json if present
|
|
164
263
|
if (fs.existsSync(mcpJsonPath)) {
|
|
165
264
|
try {
|
|
166
265
|
const existing = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
167
266
|
existing.mcpServers = existing.mcpServers || {};
|
|
168
|
-
existing.mcpServers.fleet =
|
|
267
|
+
existing.mcpServers.fleet = mcpEntry;
|
|
169
268
|
fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
269
|
+
return;
|
|
270
|
+
} catch { /* parse failed, overwrite */ }
|
|
175
271
|
}
|
|
176
|
-
|
|
272
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: { fleet: mcpEntry } }, null, 2));
|
|
273
|
+
}
|
|
177
274
|
|
|
178
|
-
|
|
275
|
+
function writeFleetCommand(projectRoot) {
|
|
179
276
|
const commandsDir = path.join(projectRoot, '.claude', 'commands');
|
|
180
277
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
181
278
|
const fleetCommandPath = path.join(commandsDir, 'fleet.md');
|
|
182
279
|
if (!fs.existsSync(fleetCommandPath)) {
|
|
183
280
|
fs.writeFileSync(fleetCommandPath, FLEET_COMMAND_TEMPLATE);
|
|
184
|
-
console.log('
|
|
281
|
+
console.log(' ✓ .claude/commands/fleet.md');
|
|
185
282
|
}
|
|
186
|
-
|
|
187
|
-
// Update .gitignore
|
|
188
|
-
updateGitignore(projectRoot);
|
|
189
|
-
|
|
190
|
-
console.log('');
|
|
191
|
-
console.log(' Done. Open Claude Code and try: /fleet status');
|
|
192
|
-
console.log('');
|
|
193
283
|
}
|
|
194
284
|
|
|
195
285
|
function updateGitignore(projectRoot) {
|
|
@@ -209,7 +299,7 @@ function updateGitignore(projectRoot) {
|
|
|
209
299
|
|
|
210
300
|
if (modified) {
|
|
211
301
|
fs.writeFileSync(gitignorePath, content.trimEnd() + '\n');
|
|
212
|
-
console.log('
|
|
302
|
+
console.log(' ✓ .gitignore (updated)');
|
|
213
303
|
}
|
|
214
304
|
}
|
|
215
305
|
|