@agentuity/cli 0.0.103 → 0.0.104

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.
Files changed (41) hide show
  1. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  2. package/dist/cmd/build/entry-generator.js +5 -5
  3. package/dist/cmd/build/entry-generator.js.map +1 -1
  4. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  5. package/dist/cmd/build/vite/agent-discovery.js +41 -2
  6. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  7. package/dist/cmd/build/vite/server-bundler.js +1 -1
  8. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  9. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  10. package/dist/cmd/build/vite/vite-asset-server.js +53 -4
  11. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  12. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  13. package/dist/cmd/build/vite/vite-builder.js +3 -0
  14. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  15. package/dist/cmd/dev/dev-lock.d.ts +62 -0
  16. package/dist/cmd/dev/dev-lock.d.ts.map +1 -0
  17. package/dist/cmd/dev/dev-lock.js +250 -0
  18. package/dist/cmd/dev/dev-lock.js.map +1 -0
  19. package/dist/cmd/dev/index.d.ts.map +1 -1
  20. package/dist/cmd/dev/index.js +56 -11
  21. package/dist/cmd/dev/index.js.map +1 -1
  22. package/dist/cmd/dev/sync.js +6 -6
  23. package/dist/cmd/dev/sync.js.map +1 -1
  24. package/dist/cmd/project/download.d.ts +1 -0
  25. package/dist/cmd/project/download.d.ts.map +1 -1
  26. package/dist/cmd/project/download.js +7 -5
  27. package/dist/cmd/project/download.js.map +1 -1
  28. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  29. package/dist/cmd/project/template-flow.js +3 -1
  30. package/dist/cmd/project/template-flow.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/cmd/build/entry-generator.ts +5 -6
  33. package/src/cmd/build/vite/agent-discovery.ts +45 -3
  34. package/src/cmd/build/vite/server-bundler.ts +1 -1
  35. package/src/cmd/build/vite/vite-asset-server.ts +54 -4
  36. package/src/cmd/build/vite/vite-builder.ts +4 -1
  37. package/src/cmd/dev/dev-lock.ts +332 -0
  38. package/src/cmd/dev/index.ts +62 -11
  39. package/src/cmd/dev/sync.ts +6 -6
  40. package/src/cmd/project/download.ts +8 -6
  41. package/src/cmd/project/template-flow.ts +4 -1
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Dev Lock Manager
3
+ *
4
+ * Manages a lockfile to track the dev server process and its children.
5
+ * On startup, detects and cleans up stale processes from previous sessions.
6
+ * Ensures proper cleanup on all exit paths.
7
+ */
8
+
9
+ import { join, dirname } from 'node:path';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { existsSync, unlinkSync } from 'node:fs';
12
+ import { promises as fs } from 'node:fs';
13
+
14
+ interface LoggerLike {
15
+ debug: (msg: string, ...args: unknown[]) => void;
16
+ warn: (msg: string, ...args: unknown[]) => void;
17
+ error: (msg: string, ...args: unknown[]) => void;
18
+ }
19
+
20
+ /**
21
+ * Lockfile format for tracking dev server processes
22
+ */
23
+ export interface DevLockFileV1 {
24
+ version: 1;
25
+ projectRoot: string;
26
+ mainPid: number;
27
+ instanceId: string;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ ports: {
31
+ bun?: number;
32
+ vite?: number;
33
+ gravity?: number;
34
+ };
35
+ children: Array<{
36
+ pid: number;
37
+ type: 'gravity' | 'vite' | 'other';
38
+ description?: string;
39
+ }>;
40
+ }
41
+
42
+ export interface DevLockManager {
43
+ state: DevLockFileV1;
44
+ registerChild: (info: {
45
+ pid: number;
46
+ type: 'gravity' | 'vite' | 'other';
47
+ description?: string;
48
+ }) => Promise<void>;
49
+ updatePorts: (ports: Partial<DevLockFileV1['ports']>) => Promise<void>;
50
+ release: () => Promise<void>;
51
+ }
52
+
53
+ function getLockPath(rootDir: string): string {
54
+ return join(rootDir, '.agentuity', 'devserver.lock');
55
+ }
56
+
57
+ /**
58
+ * Check if a process with the given PID exists
59
+ */
60
+ function pidExists(pid: number): boolean {
61
+ if (!Number.isInteger(pid) || pid <= 0) return false;
62
+ try {
63
+ process.kill(pid, 0);
64
+ return true;
65
+ } catch (err: unknown) {
66
+ const error = err as NodeJS.ErrnoException;
67
+ if (error.code === 'ESRCH' || error.code === 'EINVAL') return false;
68
+ // EPERM means it exists but we can't signal it
69
+ return error.code === 'EPERM';
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Kill a process by PID with SIGTERM, then SIGKILL if still alive
75
+ */
76
+ async function killPid(pid: number, logger: LoggerLike): Promise<void> {
77
+ if (!pidExists(pid)) return;
78
+
79
+ try {
80
+ process.kill(pid, 'SIGTERM');
81
+ logger.debug('Sent SIGTERM to pid %d', pid);
82
+ } catch (err: unknown) {
83
+ const error = err as NodeJS.ErrnoException;
84
+ if (error.code === 'ESRCH') return;
85
+ logger.debug('Error sending SIGTERM to pid %d: %s', pid, error.message);
86
+ }
87
+
88
+ // Give it a moment to exit gracefully
89
+ await new Promise((r) => setTimeout(r, 500));
90
+
91
+ if (!pidExists(pid)) return;
92
+
93
+ // Force kill
94
+ try {
95
+ process.kill(pid, 'SIGKILL');
96
+ logger.debug('Sent SIGKILL to pid %d', pid);
97
+ } catch (err: unknown) {
98
+ const error = err as NodeJS.ErrnoException;
99
+ if (error.code !== 'ESRCH') {
100
+ logger.debug('Error sending SIGKILL to pid %d: %s', pid, error.message);
101
+ }
102
+ }
103
+
104
+ // Wait for process to fully terminate
105
+ await new Promise((r) => setTimeout(r, 100));
106
+ }
107
+
108
+ /**
109
+ * Read an existing lockfile (if any)
110
+ */
111
+ async function readLock(lockPath: string, logger: LoggerLike): Promise<DevLockFileV1 | null> {
112
+ if (!existsSync(lockPath)) return null;
113
+ try {
114
+ const raw = await fs.readFile(lockPath, 'utf8');
115
+ const parsed = JSON.parse(raw);
116
+ if (parsed && parsed.version === 1) return parsed as DevLockFileV1;
117
+ } catch (err) {
118
+ logger.warn('Failed to read/parse devserver.lock: %s', err);
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Remove lockfile if it exists
125
+ */
126
+ async function removeLock(lockPath: string, logger: LoggerLike): Promise<void> {
127
+ try {
128
+ await fs.unlink(lockPath);
129
+ logger.debug('Removed devserver.lock');
130
+ } catch (err: unknown) {
131
+ const error = err as NodeJS.ErrnoException;
132
+ if (error.code !== 'ENOENT') {
133
+ logger.debug('Failed to remove devserver.lock: %s', error.message);
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if a port is in use by attempting to connect to it.
140
+ * Uses GET instead of HEAD since some servers return 405 for HEAD requests.
141
+ * Any response (including errors like 404, 500) means the port is in use.
142
+ */
143
+ async function isPortResponding(port: number): Promise<boolean> {
144
+ try {
145
+ const response = await fetch(`http://127.0.0.1:${port}/`, {
146
+ method: 'GET',
147
+ signal: AbortSignal.timeout(500),
148
+ });
149
+ // Consume body to avoid memory leaks
150
+ await response.text().catch(() => {});
151
+ return true;
152
+ } catch (err: unknown) {
153
+ // Connection refused (ECONNREFUSED) means nothing is listening
154
+ // Other errors (timeout, reset) might indicate a busy port
155
+ const error = err as Error & { cause?: { code?: string } };
156
+ const code = error.cause?.code;
157
+ if (code === 'ECONNREFUSED' || code === 'ECONNRESET') {
158
+ return false;
159
+ }
160
+ // For other errors (like timeout), assume port might be in use but unresponsive
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Kill processes referenced by a stale lock, then remove the lock
167
+ */
168
+ async function cleanupStaleLock(
169
+ rootDir: string,
170
+ lock: DevLockFileV1,
171
+ logger: LoggerLike
172
+ ): Promise<void> {
173
+ const lockPath = getLockPath(rootDir);
174
+ logger.debug(
175
+ 'Cleaning up stale devserver.lock (pid=%d, instance=%s)',
176
+ lock.mainPid,
177
+ lock.instanceId
178
+ );
179
+
180
+ // Collect all PIDs to kill (children first, then main)
181
+ const childPids: number[] = [];
182
+ for (const child of lock.children ?? []) {
183
+ if (child.pid && child.pid !== lock.mainPid && child.pid !== process.pid) {
184
+ childPids.push(child.pid);
185
+ }
186
+ }
187
+
188
+ // Kill children first
189
+ for (const pid of childPids) {
190
+ await killPid(pid, logger);
191
+ }
192
+
193
+ // Kill main process if it's not us
194
+ if (lock.mainPid !== process.pid) {
195
+ await killPid(lock.mainPid, logger);
196
+ }
197
+
198
+ // Remove the stale lockfile
199
+ await removeLock(lockPath, logger);
200
+ }
201
+
202
+ /**
203
+ * Ensure there is no conflicting dev server for this project
204
+ * Always cleans up any existing lock and kills associated processes
205
+ */
206
+ async function ensureNoActiveDevForProject(
207
+ rootDir: string,
208
+ port: number,
209
+ logger: LoggerLike
210
+ ): Promise<void> {
211
+ const lockPath = getLockPath(rootDir);
212
+ const existing = await readLock(lockPath, logger);
213
+ if (!existing) return;
214
+
215
+ const now = Date.now();
216
+ const createdAt = Date.parse(existing.createdAt || '');
217
+ const ageMs = isFinite(createdAt) ? now - createdAt : Infinity;
218
+
219
+ const mainAlive = pidExists(existing.mainPid);
220
+
221
+ // Check if the recorded Bun port is still responding
222
+ let bunPortInUse = false;
223
+ if (existing.ports?.bun) {
224
+ bunPortInUse = await isPortResponding(existing.ports.bun);
225
+ }
226
+
227
+ logger.debug(
228
+ 'Found existing lock (pid=%d, mainAlive=%s, bunPortInUse=%s, age=%dms) - cleaning up',
229
+ existing.mainPid,
230
+ mainAlive,
231
+ bunPortInUse,
232
+ ageMs
233
+ );
234
+
235
+ await cleanupStaleLock(rootDir, existing, logger);
236
+ }
237
+
238
+ /**
239
+ * Initialize a new lock for the current dev run
240
+ * This should be called after ensureNoActiveDevForProject has possibly cleaned stale state
241
+ */
242
+ async function initDevLock(
243
+ rootDir: string,
244
+ port: number,
245
+ logger: LoggerLike
246
+ ): Promise<DevLockManager> {
247
+ const lockPath = getLockPath(rootDir);
248
+ await fs.mkdir(dirname(lockPath), { recursive: true });
249
+
250
+ const state: DevLockFileV1 = {
251
+ version: 1,
252
+ projectRoot: rootDir,
253
+ mainPid: process.pid,
254
+ instanceId: randomUUID(),
255
+ createdAt: new Date().toISOString(),
256
+ updatedAt: new Date().toISOString(),
257
+ ports: { bun: port },
258
+ children: [],
259
+ };
260
+
261
+ const writeLock = async () => {
262
+ state.updatedAt = new Date().toISOString();
263
+ await fs.writeFile(lockPath, JSON.stringify(state, null, 2), { encoding: 'utf8' });
264
+ };
265
+
266
+ await writeLock();
267
+ logger.debug('Created devserver.lock (pid=%d, instance=%s)', state.mainPid, state.instanceId);
268
+
269
+ const manager: DevLockManager = {
270
+ state,
271
+
272
+ async registerChild(child) {
273
+ if (!child.pid) return;
274
+ // Avoid duplicates
275
+ if (state.children.some((c) => c.pid === child.pid)) return;
276
+ state.children.push(child);
277
+ await writeLock();
278
+ logger.debug('Registered child process (pid=%d, type=%s)', child.pid, child.type);
279
+ },
280
+
281
+ async updatePorts(ports) {
282
+ state.ports = { ...state.ports, ...ports };
283
+ await writeLock();
284
+ },
285
+
286
+ async release() {
287
+ await removeLock(lockPath, logger);
288
+ },
289
+ };
290
+
291
+ return manager;
292
+ }
293
+
294
+ /**
295
+ * Main entry point for dev lock management
296
+ * Call this early in the dev command to:
297
+ * 1. Clean up any stale processes from previous sessions
298
+ * 2. Create a new lockfile for this session
299
+ */
300
+ export async function prepareDevLock(
301
+ rootDir: string,
302
+ port: number,
303
+ logger: LoggerLike
304
+ ): Promise<DevLockManager> {
305
+ await ensureNoActiveDevForProject(rootDir, port, logger);
306
+ return initDevLock(rootDir, port, logger);
307
+ }
308
+
309
+ /**
310
+ * Utility to kill all processes in a lockfile by path
311
+ * Useful for emergency cleanup without creating a new lock
312
+ */
313
+ export async function cleanupLockfile(rootDir: string, logger: LoggerLike): Promise<void> {
314
+ const lockPath = getLockPath(rootDir);
315
+ const existing = await readLock(lockPath, logger);
316
+ if (existing) {
317
+ await cleanupStaleLock(rootDir, existing, logger);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Synchronous lockfile removal for use in process.on('exit') handlers
323
+ * Does not kill processes - just removes the file
324
+ */
325
+ export function releaseLockSync(rootDir: string): void {
326
+ const lockPath = getLockPath(rootDir);
327
+ try {
328
+ unlinkSync(lockPath);
329
+ } catch {
330
+ // Ignore errors - file may already be gone
331
+ }
332
+ }
@@ -16,6 +16,7 @@ import { getDefaultConfigDir, saveConfig } from '../../config';
16
16
  import type { Config } from '../../types';
17
17
  import { createFileWatcher } from './file-watcher';
18
18
  import { regenerateSkillsAsync } from './skills';
19
+ import { prepareDevLock, releaseLockSync } from './dev-lock';
19
20
 
20
21
  const DEFAULT_PORT = 3500;
21
22
  const MIN_PORT = 1024;
@@ -72,7 +73,7 @@ async function killLingeringGravityProcesses(logger: {
72
73
 
73
74
  /**
74
75
  * Stop the existing Bun server if one is running.
75
- * Waits for the port to become available before returning.
76
+ * Waits for the port to become available before returning (with timeout).
76
77
  */
77
78
  async function stopBunServer(
78
79
  port: number,
@@ -81,26 +82,32 @@ async function stopBunServer(
81
82
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
83
  const globalAny = globalThis as any;
83
84
  const server = globalAny.__AGENTUITY_SERVER__ as BunServer | undefined;
84
- if (!server) return;
85
+ if (!server) {
86
+ logger.debug('No Bun server to stop');
87
+ return;
88
+ }
85
89
 
86
90
  try {
87
- logger.debug('Stopping previous Bun server...');
91
+ logger.debug('Stopping Bun server...');
88
92
  server.stop(true); // Close active connections immediately
93
+ logger.debug('Bun server stop() called');
89
94
  } catch (err) {
90
- logger.debug('Error stopping previous Bun server: %s', err);
95
+ logger.debug('Error stopping Bun server: %s', err);
91
96
  }
92
97
 
93
- // Wait for socket to close to avoid EADDRINUSE races
94
- for (let i = 0; i < 20; i++) {
98
+ // Wait for socket to close (max 2 seconds to avoid hanging on shutdown)
99
+ const MAX_WAIT_ITERATIONS = 10;
100
+ for (let i = 0; i < MAX_WAIT_ITERATIONS; i++) {
95
101
  try {
96
102
  await fetch(`http://127.0.0.1:${port}/`, {
97
103
  method: 'HEAD',
98
- signal: AbortSignal.timeout(200),
104
+ signal: AbortSignal.timeout(150),
99
105
  });
100
106
  // Still responding, wait a bit more
101
107
  await new Promise((r) => setTimeout(r, 50));
102
108
  } catch {
103
109
  // Connection refused or timeout => server is down
110
+ logger.debug('Bun server stopped');
104
111
  break;
105
112
  }
106
113
  }
@@ -196,8 +203,12 @@ export const command = createCommand({
196
203
  originalExit(1);
197
204
  }
198
205
 
206
+ // Prepare dev lock: cleans up stale processes from previous sessions
207
+ // and creates a new lockfile for this session
208
+ const devLock = await prepareDevLock(rootDir, opts.port, logger);
209
+
199
210
  // Kill any lingering gravity processes from previous dev sessions
200
- // This prevents "zombie" gravity clients from blocking the public URL
211
+ // This is a fallback for cases where the lockfile was corrupted
201
212
  await killLingeringGravityProcesses(logger);
202
213
 
203
214
  // Setup devmode and gravity (if using public URL)
@@ -336,11 +347,16 @@ export const command = createCommand({
336
347
  });
337
348
  viteServer = viteResult.server;
338
349
  vitePort = viteResult.port;
350
+
351
+ // Update dev lock with actual Vite port
352
+ await devLock.updatePorts({ vite: vitePort });
353
+
339
354
  logger.debug(
340
355
  `Vite asset server running on port ${vitePort} (stays running across backend restarts)`
341
356
  );
342
357
  } catch (error) {
343
358
  tui.error(`Failed to start Vite asset server: ${error}`);
359
+ await devLock.release();
344
360
  originalExit(1);
345
361
  return;
346
362
  }
@@ -405,6 +421,7 @@ export const command = createCommand({
405
421
 
406
422
  // Kill gravity client with SIGTERM first, then SIGKILL as fallback
407
423
  if (gravityProcess) {
424
+ logger.debug('Killing gravity process...');
408
425
  try {
409
426
  gravityProcess.kill('SIGTERM');
410
427
  // Give it a moment to gracefully shutdown
@@ -412,6 +429,7 @@ export const command = createCommand({
412
429
  if (gravityProcess.exitCode === null) {
413
430
  gravityProcess.kill('SIGKILL');
414
431
  }
432
+ logger.debug('Gravity process killed');
415
433
  } catch (err) {
416
434
  logger.debug('Error killing gravity process: %s', err);
417
435
  } finally {
@@ -419,10 +437,20 @@ export const command = createCommand({
419
437
  }
420
438
  }
421
439
 
422
- // Close Vite asset server last (it handles frontend, should stay up longest)
440
+ // Close Vite asset server with timeout to prevent hanging
423
441
  if (viteServer) {
442
+ logger.debug('Closing Vite server...');
424
443
  try {
425
- await viteServer.close();
444
+ // Use Promise.race with timeout to prevent hanging
445
+ const closePromise = viteServer.close();
446
+ const timeoutPromise = new Promise<void>((resolve) => {
447
+ setTimeout(() => {
448
+ logger.debug('Vite server close timed out, continuing...');
449
+ resolve();
450
+ }, 2000);
451
+ });
452
+ await Promise.race([closePromise, timeoutPromise]);
453
+ logger.debug('Vite server closed');
426
454
  } catch (err) {
427
455
  logger.debug('Error closing Vite server: %s', err);
428
456
  } finally {
@@ -430,10 +458,20 @@ export const command = createCommand({
430
458
  }
431
459
  }
432
460
 
461
+ // Release the dev lockfile
462
+ logger.debug('Releasing dev lock...');
463
+ try {
464
+ await devLock.release();
465
+ logger.debug('Dev lock released');
466
+ } catch (err) {
467
+ logger.debug('Error releasing dev lock: %s', err);
468
+ }
469
+
433
470
  // Reset cleanup flag if not exiting (allows restart)
434
471
  if (!exitAfter) {
435
472
  cleaningUp = false;
436
473
  } else {
474
+ logger.debug('Exiting with code %d', exitCode);
437
475
  originalExit(exitCode);
438
476
  }
439
477
  };
@@ -535,6 +573,9 @@ export const command = createCommand({
535
573
  // Ignore errors during exit cleanup
536
574
  }
537
575
  }
576
+
577
+ // Release the dev lockfile synchronously
578
+ releaseLockSync(rootDir);
538
579
  });
539
580
 
540
581
  while (!shutdownRequested) {
@@ -640,7 +681,7 @@ export const command = createCommand({
640
681
  );
641
682
  previousMetadata = metadata;
642
683
  }
643
- await promises;
684
+ await Promise.all(promises);
644
685
  },
645
686
  clearOnSuccess: true,
646
687
  });
@@ -759,6 +800,16 @@ export const command = createCommand({
759
800
  }
760
801
  );
761
802
 
803
+ // Register gravity process in dev lock for cleanup tracking
804
+ const gravityPid = (gravityProcess as { pid?: number }).pid;
805
+ if (gravityPid) {
806
+ await devLock.registerChild({
807
+ pid: gravityPid,
808
+ type: 'gravity',
809
+ description: 'Gravity public URL tunnel',
810
+ });
811
+ }
812
+
762
813
  // Log gravity output
763
814
  (async () => {
764
815
  try {
@@ -273,7 +273,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
273
273
  evalsToDelete: string[],
274
274
  deploymentId: string
275
275
  ): Promise<void> {
276
- this.logger.info(
276
+ this.logger.debug(
277
277
  '[CLI EVAL SYNC] syncEvals called: %d to create, %d to delete',
278
278
  evals.length,
279
279
  evalsToDelete.length
@@ -360,24 +360,24 @@ class MockDevmodeSyncService implements IDevmodeSyncService {
360
360
 
361
361
  // Log the requests that would be made
362
362
  if (agentsToCreate.length > 0 || agentsToDelete.length > 0) {
363
- this.logger.info(
363
+ this.logger.debug(
364
364
  '[MOCK] Would make request: POST /cli/devmode/agent with %d agent(s) to create, %d agent(s) to delete',
365
365
  agentsToCreate.length,
366
366
  agentsToDelete.length
367
367
  );
368
- this.logger.info(
368
+ this.logger.debug(
369
369
  '[MOCK] Request payload: %s',
370
370
  JSON.stringify({ create: agentsToCreate, delete: agentsToDelete }, null, 2)
371
371
  );
372
372
  }
373
373
 
374
374
  if (evalsToCreate.length > 0 || evalsToDelete.length > 0) {
375
- this.logger.info(
375
+ this.logger.debug(
376
376
  '[MOCK] Would make request: POST /cli/devmode/eval with %d eval(s) to create, %d eval(s) to delete',
377
377
  evalsToCreate.length,
378
378
  evalsToDelete.length
379
379
  );
380
- this.logger.info(
380
+ this.logger.debug(
381
381
  '[MOCK] Request payload: %s',
382
382
  JSON.stringify({ create: evalsToCreate, delete: evalsToDelete }, null, 2)
383
383
  );
@@ -389,7 +389,7 @@ class MockDevmodeSyncService implements IDevmodeSyncService {
389
389
  evalsToCreate.length === 0 &&
390
390
  evalsToDelete.length === 0
391
391
  ) {
392
- this.logger.info('[MOCK] No requests would be made (no changes detected)');
392
+ this.logger.debug('[MOCK] No requests would be made (no changes detected)');
393
393
  }
394
394
  }
395
395
  }
@@ -375,9 +375,9 @@ export async function setupProject(options: SetupOptions): Promise<void> {
375
375
  // Build project
376
376
  if (!noBuild) {
377
377
  const exitCode = await tui.runCommand({
378
- command: 'bun run build',
378
+ command: 'bun run build --dev',
379
379
  cwd: dest,
380
- cmd: ['bun', 'run', 'build'],
380
+ cmd: ['bun', 'run', 'build', '--dev'],
381
381
  clearOnSuccess: true,
382
382
  });
383
383
  if (exitCode !== 0) {
@@ -385,6 +385,12 @@ export async function setupProject(options: SetupOptions): Promise<void> {
385
385
  }
386
386
  }
387
387
 
388
+ // Generate and write AGENTS.md files for the CLI and source folders
389
+ // Always overwrite during project setup to ensure fresh content
390
+ await writeAgentsDocs(dest);
391
+ }
392
+
393
+ export async function initGitRepo(dest: string): Promise<void> {
388
394
  // Initialize git repository if git is available
389
395
  // Check for real git (not macOS stub that triggers Xcode CLT popup)
390
396
  const { isGitAvailable, getDefaultBranch } = await import('../../git-helper');
@@ -435,10 +441,6 @@ export async function setupProject(options: SetupOptions): Promise<void> {
435
441
  clearOnSuccess: true,
436
442
  });
437
443
  }
438
-
439
- // Generate and write AGENTS.md files for the CLI and source folders
440
- // Always overwrite during project setup to ensure fresh content
441
- await writeAgentsDocs(dest);
442
444
  }
443
445
 
444
446
  async function replaceInFiles(dir: string, projectName: string, dirName: string): Promise<void> {
@@ -18,7 +18,7 @@ import * as tui from '../../tui';
18
18
  import { createPrompt, note } from '../../tui';
19
19
  import { playSound } from '../../sound';
20
20
  import { fetchTemplates, type TemplateInfo } from './templates';
21
- import { downloadTemplate, setupProject } from './download';
21
+ import { downloadTemplate, setupProject, initGitRepo } from './download';
22
22
  import { type AuthData, type Config } from '../../types';
23
23
  import { ErrorCode } from '../../errors';
24
24
  import type { APIClient } from '../../api';
@@ -423,6 +423,9 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
423
423
  }
424
424
  }
425
425
 
426
+ // Initialize git repository after all files are generated
427
+ await initGitRepo(dest);
428
+
426
429
  // Show completion message
427
430
  if (!skipPrompts) {
428
431
  tui.success('✨ Project created successfully!\n');