@agentuity/cli 0.0.102 → 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/bin/cli.ts +12 -12
- 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/bun-dev-server.d.ts.map +1 -1
- package/dist/cmd/build/vite/bun-dev-server.js +5 -27
- package/dist/cmd/build/vite/bun-dev-server.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/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +3 -1
- package/dist/cmd/cloud/deploy.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 +91 -22
- 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/ai/prompt/web.md +47 -47
- package/src/cmd/build/entry-generator.ts +5 -6
- package/src/cmd/build/vite/agent-discovery.ts +45 -3
- package/src/cmd/build/vite/bun-dev-server.ts +5 -32
- 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/cloud/deploy.ts +6 -2
- package/src/cmd/dev/dev-lock.ts +332 -0
- package/src/cmd/dev/index.ts +116 -30
- 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;
|
|
@@ -42,9 +43,9 @@ interface BunServer {
|
|
|
42
43
|
* Kill any lingering gravity processes from previous dev sessions.
|
|
43
44
|
* This is a defensive measure to clean up orphaned processes.
|
|
44
45
|
*/
|
|
45
|
-
async function killLingeringGravityProcesses(
|
|
46
|
-
|
|
47
|
-
): Promise<void> {
|
|
46
|
+
async function killLingeringGravityProcesses(logger: {
|
|
47
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
48
|
+
}): Promise<void> {
|
|
48
49
|
// Only attempt on Unix-like systems (macOS, Linux)
|
|
49
50
|
if (process.platform === 'win32') {
|
|
50
51
|
return;
|
|
@@ -72,7 +73,7 @@ async function killLingeringGravityProcesses(
|
|
|
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
|
}
|
|
@@ -371,6 +387,8 @@ export const command = createCommand({
|
|
|
371
387
|
|
|
372
388
|
// Track if cleanup is in progress to avoid duplicate cleanup
|
|
373
389
|
let cleaningUp = false;
|
|
390
|
+
// Track if shutdown was requested (SIGINT/SIGTERM) to break the main loop
|
|
391
|
+
let shutdownRequested = false;
|
|
374
392
|
|
|
375
393
|
/**
|
|
376
394
|
* Centralized cleanup function for all resources.
|
|
@@ -403,6 +421,7 @@ export const command = createCommand({
|
|
|
403
421
|
|
|
404
422
|
// Kill gravity client with SIGTERM first, then SIGKILL as fallback
|
|
405
423
|
if (gravityProcess) {
|
|
424
|
+
logger.debug('Killing gravity process...');
|
|
406
425
|
try {
|
|
407
426
|
gravityProcess.kill('SIGTERM');
|
|
408
427
|
// Give it a moment to gracefully shutdown
|
|
@@ -410,6 +429,7 @@ export const command = createCommand({
|
|
|
410
429
|
if (gravityProcess.exitCode === null) {
|
|
411
430
|
gravityProcess.kill('SIGKILL');
|
|
412
431
|
}
|
|
432
|
+
logger.debug('Gravity process killed');
|
|
413
433
|
} catch (err) {
|
|
414
434
|
logger.debug('Error killing gravity process: %s', err);
|
|
415
435
|
} finally {
|
|
@@ -417,10 +437,20 @@ export const command = createCommand({
|
|
|
417
437
|
}
|
|
418
438
|
}
|
|
419
439
|
|
|
420
|
-
// Close Vite asset server
|
|
440
|
+
// Close Vite asset server with timeout to prevent hanging
|
|
421
441
|
if (viteServer) {
|
|
442
|
+
logger.debug('Closing Vite server...');
|
|
422
443
|
try {
|
|
423
|
-
|
|
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');
|
|
424
454
|
} catch (err) {
|
|
425
455
|
logger.debug('Error closing Vite server: %s', err);
|
|
426
456
|
} finally {
|
|
@@ -428,10 +458,20 @@ export const command = createCommand({
|
|
|
428
458
|
}
|
|
429
459
|
}
|
|
430
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
|
+
|
|
431
470
|
// Reset cleanup flag if not exiting (allows restart)
|
|
432
471
|
if (!exitAfter) {
|
|
433
472
|
cleaningUp = false;
|
|
434
473
|
} else {
|
|
474
|
+
logger.debug('Exiting with code %d', exitCode);
|
|
435
475
|
originalExit(exitCode);
|
|
436
476
|
}
|
|
437
477
|
};
|
|
@@ -474,6 +514,7 @@ export const command = createCommand({
|
|
|
474
514
|
if (reason) {
|
|
475
515
|
logger.debug('DevMode terminating (%d) due to: %s', code, reason);
|
|
476
516
|
}
|
|
517
|
+
shutdownRequested = true;
|
|
477
518
|
await cleanup(true, code);
|
|
478
519
|
};
|
|
479
520
|
|
|
@@ -488,7 +529,7 @@ export const command = createCommand({
|
|
|
488
529
|
// Handle uncaught exceptions - clean up and exit rather than limping on
|
|
489
530
|
process.on('uncaughtException', (err) => {
|
|
490
531
|
tui.error(
|
|
491
|
-
`Uncaught exception: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
|
|
532
|
+
`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`
|
|
492
533
|
);
|
|
493
534
|
void safeExit(1, 'uncaughtException');
|
|
494
535
|
});
|
|
@@ -497,7 +538,7 @@ export const command = createCommand({
|
|
|
497
538
|
process.on('unhandledRejection', (reason) => {
|
|
498
539
|
logger.warn(
|
|
499
540
|
'Unhandled promise rejection: %s',
|
|
500
|
-
reason instanceof Error ? reason.stack ?? reason.message : String(reason)
|
|
541
|
+
reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)
|
|
501
542
|
);
|
|
502
543
|
});
|
|
503
544
|
}
|
|
@@ -532,9 +573,12 @@ export const command = createCommand({
|
|
|
532
573
|
// Ignore errors during exit cleanup
|
|
533
574
|
}
|
|
534
575
|
}
|
|
576
|
+
|
|
577
|
+
// Release the dev lockfile synchronously
|
|
578
|
+
releaseLockSync(rootDir);
|
|
535
579
|
});
|
|
536
580
|
|
|
537
|
-
while (
|
|
581
|
+
while (!shutdownRequested) {
|
|
538
582
|
shouldRestart = false;
|
|
539
583
|
|
|
540
584
|
// Pause file watcher during build to avoid loops
|
|
@@ -589,10 +633,16 @@ export const command = createCommand({
|
|
|
589
633
|
|
|
590
634
|
const srcDir = join(rootDir, 'src');
|
|
591
635
|
|
|
636
|
+
const promises: Promise<void>[] = [];
|
|
637
|
+
|
|
592
638
|
// Generate/update prompt files (non-blocking)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
639
|
+
promises.push(
|
|
640
|
+
import('../build/vite/prompt-generator')
|
|
641
|
+
.then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
|
|
642
|
+
.catch((err) =>
|
|
643
|
+
logger.warn('Failed to generate prompt files: %s', err.message)
|
|
644
|
+
)
|
|
645
|
+
);
|
|
596
646
|
const agents = await discoverAgents(
|
|
597
647
|
srcDir,
|
|
598
648
|
project?.projectId ?? '',
|
|
@@ -621,14 +671,17 @@ export const command = createCommand({
|
|
|
621
671
|
|
|
622
672
|
// Sync metadata with backend (creates agents and evals in the database)
|
|
623
673
|
if (syncService && project?.projectId) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
674
|
+
promises.push(
|
|
675
|
+
syncService.sync(
|
|
676
|
+
metadata,
|
|
677
|
+
previousMetadata,
|
|
678
|
+
project.projectId,
|
|
679
|
+
deploymentId
|
|
680
|
+
)
|
|
629
681
|
);
|
|
630
682
|
previousMetadata = metadata;
|
|
631
683
|
}
|
|
684
|
+
await Promise.all(promises);
|
|
632
685
|
},
|
|
633
686
|
clearOnSuccess: true,
|
|
634
687
|
});
|
|
@@ -694,22 +747,35 @@ export const command = createCommand({
|
|
|
694
747
|
// Wait for app.ts to finish loading (Vite is ready but app may still be initializing)
|
|
695
748
|
// Give it 2 seconds to ensure app initialization completes
|
|
696
749
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
750
|
+
|
|
751
|
+
// Check if shutdown was requested during startup
|
|
752
|
+
if (shutdownRequested) {
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
697
755
|
} catch (error) {
|
|
698
756
|
tui.error(`Failed to start dev server: ${error}`);
|
|
699
757
|
tui.warn('Waiting for file changes to retry...');
|
|
700
758
|
|
|
701
|
-
// Wait for next restart trigger
|
|
759
|
+
// Wait for next restart trigger or shutdown
|
|
702
760
|
await new Promise<void>((resolve) => {
|
|
703
761
|
const checkRestart = setInterval(() => {
|
|
704
|
-
if (shouldRestart) {
|
|
762
|
+
if (shouldRestart || shutdownRequested) {
|
|
705
763
|
clearInterval(checkRestart);
|
|
706
764
|
resolve();
|
|
707
765
|
}
|
|
708
766
|
}, 100);
|
|
709
767
|
});
|
|
768
|
+
if (shutdownRequested) {
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
710
771
|
continue;
|
|
711
772
|
}
|
|
712
773
|
|
|
774
|
+
// Exit early if shutdown was requested
|
|
775
|
+
if (shutdownRequested) {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
|
|
713
779
|
try {
|
|
714
780
|
// Start gravity client if we have devmode
|
|
715
781
|
if (gravityBin && gravityURL && devmode) {
|
|
@@ -734,6 +800,16 @@ export const command = createCommand({
|
|
|
734
800
|
}
|
|
735
801
|
);
|
|
736
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
|
+
|
|
737
813
|
// Log gravity output
|
|
738
814
|
(async () => {
|
|
739
815
|
try {
|
|
@@ -824,16 +900,21 @@ export const command = createCommand({
|
|
|
824
900
|
// Start/resume file watcher now that server is ready
|
|
825
901
|
fileWatcher.resume();
|
|
826
902
|
|
|
827
|
-
// Wait for restart signal
|
|
903
|
+
// Wait for restart signal or shutdown
|
|
828
904
|
await new Promise<void>((resolve) => {
|
|
829
905
|
const checkRestart = setInterval(() => {
|
|
830
|
-
if (shouldRestart) {
|
|
906
|
+
if (shouldRestart || shutdownRequested) {
|
|
831
907
|
clearInterval(checkRestart);
|
|
832
908
|
resolve();
|
|
833
909
|
}
|
|
834
910
|
}, 100);
|
|
835
911
|
});
|
|
836
912
|
|
|
913
|
+
// Exit loop if shutdown was requested
|
|
914
|
+
if (shutdownRequested) {
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
|
|
837
918
|
// Restart triggered - cleanup and loop (Vite stays running)
|
|
838
919
|
logger.debug('Restarting backend server...');
|
|
839
920
|
|
|
@@ -849,13 +930,18 @@ export const command = createCommand({
|
|
|
849
930
|
// Cleanup on error (Vite stays running)
|
|
850
931
|
await cleanupForRestart();
|
|
851
932
|
|
|
933
|
+
// Exit if shutdown was requested during error handling
|
|
934
|
+
if (shutdownRequested) {
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
|
|
852
938
|
// Resume file watcher to detect changes for retry
|
|
853
939
|
fileWatcher.resume();
|
|
854
940
|
|
|
855
|
-
// Wait for next restart trigger
|
|
941
|
+
// Wait for next restart trigger or shutdown
|
|
856
942
|
await new Promise<void>((resolve) => {
|
|
857
943
|
const checkRestart = setInterval(() => {
|
|
858
|
-
if (shouldRestart) {
|
|
944
|
+
if (shouldRestart || shutdownRequested) {
|
|
859
945
|
clearInterval(checkRestart);
|
|
860
946
|
resolve();
|
|
861
947
|
}
|