@agentuity/cli 0.0.23 → 0.0.25

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cmd/dev/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,OAAO,mCAgKlB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cmd/dev/index.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,OAAO,mCAuVlB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/download.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,eAAe;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,UAAU,YAAY;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAsBD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAyH9E;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCvE"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/download.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,eAAe;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,UAAU,YAAY;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAsBD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAyH9E;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DvE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -41,13 +41,42 @@ export const command = createCommand({
41
41
  const agentuityDir = resolve(rootDir, '.agentuity');
42
42
  const appPath = resolve(agentuityDir, 'app.js');
43
43
 
44
- const watches = [appTs, srcDir];
44
+ // Watch directories instead of files to survive atomic replacements (sed -i, cp)
45
+ const watches = [rootDir];
45
46
  const watchers: FSWatcher[] = [];
46
47
  let failures = 0;
47
48
  let running = false;
48
49
  let pid = 0;
49
50
  let failed = false;
50
51
  let devServer: Bun.Subprocess | undefined;
52
+ let exitPromise: Promise<number> | undefined;
53
+ let restarting = false;
54
+ let shuttingDownForRestart = false;
55
+ let pendingRestart = false;
56
+
57
+ // Track restart timestamps to detect restart loops
58
+ const restartTimestamps: number[] = [];
59
+ const MAX_RESTARTS = 10;
60
+ const TIME_WINDOW_MS = 10000; // 10 seconds
61
+
62
+ function checkRestartThrottle() {
63
+ const now = Date.now();
64
+ restartTimestamps.push(now);
65
+
66
+ // Remove timestamps older than the time window
67
+ while (restartTimestamps.length > 0 && now - restartTimestamps[0]! > TIME_WINDOW_MS) {
68
+ restartTimestamps.shift();
69
+ }
70
+
71
+ // Check if we've exceeded the threshold
72
+ if (restartTimestamps.length >= MAX_RESTARTS) {
73
+ tui.error(`Detected ${MAX_RESTARTS} restarts in ${TIME_WINDOW_MS / 1000} seconds`);
74
+ tui.error(
75
+ 'This usually indicates a file watcher loop (e.g., log files in the project root)'
76
+ );
77
+ tui.fatal('Too many rapid restarts, exiting to prevent infinite loop');
78
+ }
79
+ }
51
80
 
52
81
  function failure(msg: string) {
53
82
  failed = true;
@@ -60,23 +89,42 @@ export const command = createCommand({
60
89
  }
61
90
  }
62
91
 
63
- const kill = () => {
92
+ const kill = async () => {
93
+ if (!running || !devServer) {
94
+ logger.trace('kill() called but server not running');
95
+ return;
96
+ }
97
+
98
+ logger.trace('Killing dev server (pid: %d)', pid);
99
+ shuttingDownForRestart = true;
64
100
  running = false;
65
101
  try {
66
102
  // Kill the process group (negative PID kills entire group)
67
103
  process.kill(-pid, 'SIGTERM');
104
+ logger.trace('Sent SIGTERM to process group');
68
105
  } catch {
69
106
  // Fallback: kill the direct process
70
107
  try {
71
108
  if (devServer) {
72
109
  devServer.kill();
110
+ logger.trace('Killed dev server process directly');
73
111
  }
74
112
  } catch {
75
113
  // Ignore if already dead
114
+ logger.trace('Process already dead');
76
115
  }
77
- } finally {
78
- devServer = undefined;
79
116
  }
117
+
118
+ // Wait for the server to actually exit
119
+ if (exitPromise) {
120
+ logger.trace('Waiting for dev server to exit...');
121
+ await exitPromise;
122
+ logger.trace('Dev server exited');
123
+ }
124
+
125
+ devServer = undefined;
126
+ exitPromise = undefined;
127
+ shuttingDownForRestart = false;
80
128
  };
81
129
 
82
130
  // Handle signals to ensure entire process tree is killed
@@ -95,11 +143,27 @@ export const command = createCommand({
95
143
  process.on('SIGTERM', cleanup);
96
144
 
97
145
  async function restart() {
146
+ // Queue restart if already restarting
147
+ if (restarting) {
148
+ logger.trace('Restart already in progress, queuing another restart');
149
+ pendingRestart = true;
150
+ return;
151
+ }
152
+
153
+ logger.trace('restart() called, restarting=%s, running=%s', restarting, running);
154
+ restarting = true;
155
+ pendingRestart = false;
156
+ failed = false;
98
157
  try {
99
158
  if (running) {
159
+ logger.trace('Server is running, killing before restart');
160
+ checkRestartThrottle();
100
161
  tui.info('Restarting on file change');
101
- kill();
102
- return;
162
+ await kill();
163
+ logger.trace('Server killed, continuing with restart');
164
+ // Continue with restart after kill completes
165
+ } else {
166
+ logger.trace('Initial server start');
103
167
  }
104
168
  await Promise.all([
105
169
  tui.runCommand({
@@ -132,9 +196,10 @@ export const command = createCommand({
132
196
  return;
133
197
  }
134
198
 
199
+ logger.trace('Starting dev server: %s', appPath);
135
200
  // Use shell to run in a process group for proper cleanup
136
201
  // The 'exec' ensures the shell is replaced by the actual process
137
- const devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
202
+ devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
138
203
  cwd: rootDir,
139
204
  stdout: 'inherit',
140
205
  stderr: 'inherit',
@@ -144,25 +209,143 @@ export const command = createCommand({
144
209
  running = true;
145
210
  failed = false;
146
211
  pid = devServer.pid;
212
+ exitPromise = devServer.exited;
213
+ logger.trace('Dev server started (pid: %d)', pid);
147
214
 
148
- const exitCode = await devServer.exited;
149
- if (exitCode === 0) {
150
- process.exit(exitCode);
151
- }
215
+ // Attach non-blocking exit handler
216
+ exitPromise
217
+ .then((exitCode) => {
218
+ logger.trace(
219
+ 'Dev server exited with code %d (shuttingDownForRestart=%s)',
220
+ exitCode,
221
+ shuttingDownForRestart
222
+ );
223
+ running = false;
224
+ devServer = undefined;
225
+ exitPromise = undefined;
226
+ // Only exit the CLI if this is a clean exit AND not a restart
227
+ if (exitCode === 0 && !shuttingDownForRestart) {
228
+ logger.trace('Clean exit, stopping CLI');
229
+ process.exit(exitCode);
230
+ }
231
+ // Non-zero exit codes are treated as restartable failures
232
+ })
233
+ .catch((error) => {
234
+ logger.trace(
235
+ 'Dev server exit error (shuttingDownForRestart=%s): %s',
236
+ shuttingDownForRestart,
237
+ error
238
+ );
239
+ running = false;
240
+ devServer = undefined;
241
+ exitPromise = undefined;
242
+ if (!shuttingDownForRestart) {
243
+ if (error instanceof Error) {
244
+ failure(`Dev server failed: ${error.message}`);
245
+ } else {
246
+ failure('Dev server failed');
247
+ }
248
+ }
249
+ });
152
250
  } catch (error) {
153
251
  if (error instanceof Error) {
154
252
  failure(`Dev server failed: ${error.message}`);
155
253
  } else {
156
254
  failure('Dev server failed');
157
255
  }
158
- } finally {
159
256
  running = false;
257
+ devServer = undefined;
258
+ } finally {
259
+ const hadPendingRestart = pendingRestart;
260
+ restarting = false;
261
+ pendingRestart = false;
262
+ logger.trace(
263
+ 'restart() completed, restarting=%s, hadPendingRestart=%s',
264
+ restarting,
265
+ hadPendingRestart
266
+ );
267
+
268
+ // If another restart was queued while we were restarting, trigger it now
269
+ if (hadPendingRestart) {
270
+ logger.trace('Triggering queued restart');
271
+ setImmediate(restart);
272
+ }
160
273
  }
161
274
  }
275
+
276
+ logger.trace('Starting initial build and server');
162
277
  await restart();
163
- for (const filename of watches) {
164
- logger.trace('watching %s', filename);
165
- watchers.push(watch(filename, { recursive: true }, restart));
278
+ logger.trace('Initial restart completed, setting up watchers');
279
+
280
+ // Patterns to ignore (generated files that change during build)
281
+ const ignorePatterns = [
282
+ /\.generated\.(js|ts|d\.ts)$/,
283
+ /registry\.generated\.ts$/,
284
+ /types\.generated\.d\.ts$/,
285
+ /client\.generated\.js$/,
286
+ ];
287
+
288
+ logger.trace('Setting up file watchers for: %s', watches.join(', '));
289
+ for (const watchDir of watches) {
290
+ try {
291
+ logger.trace('Setting up watcher for %s', watchDir);
292
+ const watcher = watch(watchDir, { recursive: true }, (eventType, changedFile) => {
293
+ const absPath = changedFile ? join(watchDir, changedFile) : watchDir;
294
+
295
+ // Ignore node_modules folder
296
+ if (absPath.includes('node_modules')) {
297
+ logger.trace(
298
+ 'File change ignored (node_modules): %s (event: %s, file: %s)',
299
+ watchDir,
300
+ eventType,
301
+ changedFile
302
+ );
303
+ return;
304
+ }
305
+
306
+ // Ignore changes in .agentuity directory (build output)
307
+ if (absPath.startsWith(agentuityDir)) {
308
+ logger.trace(
309
+ 'File change ignored (.agentuity dir): %s (event: %s, file: %s)',
310
+ watchDir,
311
+ eventType,
312
+ changedFile
313
+ );
314
+ return;
315
+ }
316
+
317
+ // Ignore generated files to prevent restart loops
318
+ if (changedFile) {
319
+ for (const pattern of ignorePatterns) {
320
+ if (pattern.test(changedFile)) {
321
+ logger.trace(
322
+ 'File change ignored (generated file): %s (event: %s, file: %s)',
323
+ watchDir,
324
+ eventType,
325
+ changedFile
326
+ );
327
+ return;
328
+ }
329
+ }
330
+ }
331
+
332
+ logger.trace(
333
+ 'File change detected: %s (event: %s, file: %s)',
334
+ absPath,
335
+ eventType,
336
+ changedFile
337
+ );
338
+ restart();
339
+ });
340
+ watchers.push(watcher);
341
+ logger.trace('✓ Watcher added for %s', watchDir);
342
+ } catch (error) {
343
+ logger.error('Failed to setup watcher for %s: %s', watchDir, error);
344
+ }
166
345
  }
346
+ logger.debug('Dev server watching for changes');
347
+
348
+ // Keep the handler alive indefinitely
349
+ await new Promise(() => {});
167
350
  },
168
351
  });
@@ -213,6 +213,34 @@ export async function setupProject(options: SetupOptions): Promise<void> {
213
213
  logger.error('Failed to build project');
214
214
  }
215
215
  }
216
+
217
+ // Initialize git repository if git is available
218
+ const gitPath = Bun.which('git');
219
+ if (gitPath) {
220
+ // Git is available, initialize repository
221
+ await tui.runCommand({
222
+ command: 'git init',
223
+ cwd: dest,
224
+ cmd: ['git', 'init'],
225
+ clearOnSuccess: true,
226
+ });
227
+
228
+ // Add all files
229
+ await tui.runCommand({
230
+ command: 'git add .',
231
+ cwd: dest,
232
+ cmd: ['git', 'add', '.'],
233
+ clearOnSuccess: true,
234
+ });
235
+
236
+ // Create initial commit
237
+ await tui.runCommand({
238
+ command: 'git commit -m "Initial Setup"',
239
+ cwd: dest,
240
+ cmd: ['git', 'commit', '-m', 'Initial Setup'],
241
+ clearOnSuccess: true,
242
+ });
243
+ }
216
244
  }
217
245
 
218
246
  async function replaceInFiles(dir: string, projectName: string, dirName: string): Promise<void> {
package/src/tui.ts CHANGED
@@ -890,7 +890,7 @@ export async function runCommand(options: CommandRunnerOptions): Promise<number>
890
890
  for (const line of lines) {
891
891
  if (line.trim()) {
892
892
  allOutputLines.push(line);
893
- renderOutput(3); // Show last 3 lines while streaming
893
+ renderOutput(maxLinesOutput); // Show last N lines while streaming
894
894
  }
895
895
  }
896
896
  }