@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.
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +5 -5
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.js +41 -2
- package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
- package/dist/cmd/build/vite/server-bundler.js +1 -1
- package/dist/cmd/build/vite/server-bundler.js.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-asset-server.js +53 -4
- package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +3 -0
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/dev/dev-lock.d.ts +62 -0
- package/dist/cmd/dev/dev-lock.d.ts.map +1 -0
- package/dist/cmd/dev/dev-lock.js +250 -0
- package/dist/cmd/dev/dev-lock.js.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +56 -11
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/dev/sync.js +6 -6
- package/dist/cmd/dev/sync.js.map +1 -1
- package/dist/cmd/project/download.d.ts +1 -0
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/download.js +7 -5
- package/dist/cmd/project/download.js.map +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +3 -1
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/package.json +4 -4
- package/src/cmd/build/entry-generator.ts +5 -6
- package/src/cmd/build/vite/agent-discovery.ts +45 -3
- package/src/cmd/build/vite/server-bundler.ts +1 -1
- package/src/cmd/build/vite/vite-asset-server.ts +54 -4
- package/src/cmd/build/vite/vite-builder.ts +4 -1
- package/src/cmd/dev/dev-lock.ts +332 -0
- package/src/cmd/dev/index.ts +62 -11
- package/src/cmd/dev/sync.ts +6 -6
- package/src/cmd/project/download.ts +8 -6
- 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
|
+
}
|
package/src/cmd/dev/index.ts
CHANGED
|
@@ -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)
|
|
85
|
+
if (!server) {
|
|
86
|
+
logger.debug('No Bun server to stop');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
85
89
|
|
|
86
90
|
try {
|
|
87
|
-
logger.debug('Stopping
|
|
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
|
|
95
|
+
logger.debug('Error stopping Bun server: %s', err);
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
// Wait for socket to close to avoid
|
|
94
|
-
|
|
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(
|
|
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
|
|
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
|
|
440
|
+
// Close Vite asset server with timeout to prevent hanging
|
|
423
441
|
if (viteServer) {
|
|
442
|
+
logger.debug('Closing Vite server...');
|
|
424
443
|
try {
|
|
425
|
-
|
|
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 {
|
package/src/cmd/dev/sync.ts
CHANGED
|
@@ -273,7 +273,7 @@ class DevmodeSyncService implements IDevmodeSyncService {
|
|
|
273
273
|
evalsToDelete: string[],
|
|
274
274
|
deploymentId: string
|
|
275
275
|
): Promise<void> {
|
|
276
|
-
this.logger.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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');
|