@grantx/fleet-cli 0.1.4 → 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.
@@ -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 — First-use wizard: scan codebase, generate agents, create workspace.
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('\nFleet Setup');
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
- // 1. Scan codebase
24
- console.log(' Scanning codebase...');
25
- const scan = scanCodebase(projectRoot);
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 (scan.languages.length > 0) {
28
- console.log(` Languages: ${scan.languages.join(', ')}`);
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
- const totalFiles = Object.values(scan.fileStats).reduce((a, b) => a + b, 0);
41
- const topExts = Object.entries(scan.fileStats)
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
- // 2. Generate roster
53
- const roster = generateRoster(scan);
54
- console.log(` Recommended agents (${roster.length}):`);
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
- // 3. Get Supabase config
62
- let supabaseUrl = process.env.FLEET_SUPABASE_URL || '';
63
- let supabaseKey = process.env.FLEET_SUPABASE_KEY || '';
64
- let teamId = process.env.FLEET_TEAM_ID || '';
65
-
66
- // Auto-suggest team ID
67
- const username = os.userInfo().username || 'user';
68
- const dirname = path.basename(projectRoot);
69
- const suggestedTeamId = `${username}-${dirname}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
70
-
71
- // Non-interactive mode: all env vars set
72
- const nonInteractive = supabaseUrl && supabaseKey;
73
-
74
- if (nonInteractive) {
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 ask('Supabase API key (sb_secret_... or eyJ...)', '');
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
- if (!teamId) {
105
- teamId = await ask('Team ID', suggestedTeamId);
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
- console.log('');
194
+ }
111
195
 
112
- // 4. Create workspace
113
- console.log(' Creating workspace...');
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
- // Write fleet.config.json
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
- supabase: {
120
- url: supabaseUrl,
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(' fleet.config.json');
238
+ console.log('fleet.config.json');
144
239
 
145
- // Setup agent workspaces
146
240
  setupAgentWorkspaces(projectRoot, roster);
147
- const agentNames = roster.map(a => a.name).join(', ');
148
- console.log(` .fleet/agents/{${agentNames}}/`);
149
- console.log(' .fleet/sessions.json');
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
- // Write .mcp.json
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 mcpConfig = {
154
- mcpServers: {
155
- fleet: {
156
- type: 'stdio',
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 = mcpConfig.mcpServers.fleet;
267
+ existing.mcpServers.fleet = mcpEntry;
169
268
  fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2));
170
- } catch {
171
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
172
- }
173
- } else {
174
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
269
+ return;
270
+ } catch { /* parse failed, overwrite */ }
175
271
  }
176
- console.log(' .mcp.json (Claude Code MCP registration)');
272
+ fs.writeFileSync(mcpJsonPath, JSON.stringify({ mcpServers: { fleet: mcpEntry } }, null, 2));
273
+ }
177
274
 
178
- // Create .claude/commands/fleet.md slash command
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(' .claude/commands/fleet.md');
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(' .gitignore (updated)');
302
+ console.log('.gitignore (updated)');
213
303
  }
214
304
  }
215
305