@castlekit/castle 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@castlekit/castle",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The multi-agent workspace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -332,6 +332,9 @@ export async function runOnboarding(): Promise<void> {
332
332
  }
333
333
 
334
334
  // Step 5: Create Castle config
335
+ const serverSpinner = p.spinner();
336
+ serverSpinner.start("Saving configuration...");
337
+
335
338
  ensureCastleDir();
336
339
 
337
340
  const config: CastleConfig = {
@@ -346,12 +349,9 @@ export async function runOnboarding(): Promise<void> {
346
349
  };
347
350
 
348
351
  writeConfig(config);
352
+ serverSpinner.message("Building Castle...");
349
353
 
350
- // Step 6: Build and start server as a persistent service
351
- const serverSpinner = p.spinner();
352
- serverSpinner.start("Building Castle...");
353
-
354
- const { spawn, execSync: execSyncChild } = await import("child_process");
354
+ const { spawn, exec, execSync: execSyncChild } = await import("child_process");
355
355
  const { join } = await import("path");
356
356
  const { writeFileSync: writeFile, mkdirSync: mkDir, readFileSync: readF } = await import("fs");
357
357
  const { homedir: home } = await import("os");
@@ -360,14 +360,17 @@ export async function runOnboarding(): Promise<void> {
360
360
  const logsDir = join(castleDir, "logs");
361
361
  mkDir(logsDir, { recursive: true });
362
362
 
363
- // Build for production
364
- try {
365
- execSyncChild("npm run build", {
363
+ // Build for production (async so the spinner can animate)
364
+ const buildOk = await new Promise<boolean>((resolve) => {
365
+ const child = exec("npm run build", {
366
366
  cwd: PROJECT_ROOT,
367
- stdio: "ignore",
368
367
  timeout: 120000,
369
368
  });
370
- } catch {
369
+ child.on("close", (code) => resolve(code === 0));
370
+ child.on("error", () => resolve(false));
371
+ });
372
+
373
+ if (!buildOk) {
371
374
  serverSpinner.stop(pc.red("Build failed"));
372
375
  p.outro(pc.dim(`Try running ${BLUE("npm run build")} manually in the castle directory.`));
373
376
  return;
@@ -377,17 +380,7 @@ export async function runOnboarding(): Promise<void> {
377
380
 
378
381
  // Find node and next paths for the service
379
382
  const nodePath = process.execPath;
380
-
381
- // Locate next binary reliably (works for both local and global installs)
382
- let nextBin: string;
383
- try {
384
- // npm bin gives the local node_modules/.bin directory
385
- const binDir = execSyncChild("npm bin", { cwd: PROJECT_ROOT, encoding: "utf-8" }).trim();
386
- nextBin = join(binDir, "next");
387
- } catch {
388
- // Fallback: try local node_modules directly
389
- nextBin = join(PROJECT_ROOT, "node_modules", ".bin", "next");
390
- }
383
+ const nextBin = join(PROJECT_ROOT, "node_modules", ".bin", "next");
391
384
 
392
385
  // Castle port from config or default
393
386
  const castlePort = String(config.server?.port || 3333);
@@ -395,18 +388,17 @@ export async function runOnboarding(): Promise<void> {
395
388
  // Write PID file helper
396
389
  const pidFile = join(castleDir, "server.pid");
397
390
 
398
- // Kill any existing Castle server and wait for it to release the port
391
+ // Kill any existing Castle server (by PID file)
399
392
  try {
400
393
  const existingPid = parseInt(readF(pidFile, "utf-8").trim(), 10);
401
394
  if (Number.isInteger(existingPid) && existingPid > 0) {
402
395
  process.kill(existingPid);
403
- // Wait up to 3s for old process to die
404
396
  for (let i = 0; i < 30; i++) {
405
397
  try {
406
- process.kill(existingPid, 0); // Test if alive
398
+ process.kill(existingPid, 0);
407
399
  await new Promise((r) => setTimeout(r, 100));
408
400
  } catch {
409
- break; // Process is gone
401
+ break;
410
402
  }
411
403
  }
412
404
  }
@@ -414,25 +406,19 @@ export async function runOnboarding(): Promise<void> {
414
406
  // No existing server or already dead
415
407
  }
416
408
 
417
- // Start production server
418
- const server = spawn(nodePath, [nextBin, "start", "-p", castlePort], {
419
- cwd: PROJECT_ROOT,
420
- stdio: ["ignore", "ignore", "ignore"],
421
- detached: true,
422
- });
423
-
424
- // Write PID file so we can manage the server later
425
- if (server.pid != null) {
426
- writeFile(pidFile, String(server.pid));
409
+ // Kill anything else on the target port
410
+ try {
411
+ execSyncChild(`lsof -ti:${castlePort} | xargs kill -9 2>/dev/null`, {
412
+ stdio: "ignore",
413
+ timeout: 5000,
414
+ });
415
+ await new Promise((r) => setTimeout(r, 500));
416
+ } catch {
417
+ // Nothing on port or lsof not available
427
418
  }
428
- server.unref();
429
419
 
430
420
  // Install as a persistent service (auto-start on login)
431
421
  if (process.platform === "darwin") {
432
- // macOS: LaunchAgent
433
- // We unload first to avoid conflicts, then load. KeepAlive is set to
434
- // SuccessfulExit=false so launchd only restarts on crashes, not when
435
- // we intentionally stop it.
436
422
  const plistDir = join(home(), "Library", "LaunchAgents");
437
423
  mkDir(plistDir, { recursive: true });
438
424
  const plistPath = join(plistDir, "com.castlekit.castle.plist");
@@ -472,23 +458,16 @@ export async function runOnboarding(): Promise<void> {
472
458
  </dict>
473
459
  </dict>
474
460
  </plist>`;
475
- // Stop any running instance first, then kill our manually spawned one
476
- // so launchd takes over as the sole process manager
477
461
  try {
478
- execSyncChild(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
462
+ execSyncChild(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore", timeout: 10000 });
479
463
  } catch { /* ignore */ }
480
464
  writeFile(plistPath, plist);
481
465
  try {
482
- // Kill the spawn()ed server launchd will now be the owner
483
- if (server.pid) {
484
- try { process.kill(server.pid); } catch { /* already dead */ }
485
- }
486
- execSyncChild(`launchctl load "${plistPath}"`, { stdio: "ignore" });
466
+ execSyncChild(`launchctl load "${plistPath}"`, { stdio: "ignore", timeout: 10000 });
487
467
  } catch {
488
- // Non-fatal: server is already running via spawn
468
+ // Non-fatal fall back to spawning directly
489
469
  }
490
470
  } else if (process.platform === "linux") {
491
- // Linux: systemd user service
492
471
  const systemdDir = join(home(), ".config", "systemd", "user");
493
472
  mkDir(systemdDir, { recursive: true });
494
473
  const servicePath = join(systemdDir, "castle.service");
@@ -509,14 +488,30 @@ WantedBy=default.target
509
488
  `;
510
489
  writeFile(servicePath, service);
511
490
  try {
512
- execSyncChild("systemctl --user daemon-reload && systemctl --user enable --now castle.service", { stdio: "ignore" });
491
+ execSyncChild("systemctl --user daemon-reload && systemctl --user enable --now castle.service", { stdio: "ignore", timeout: 15000 });
513
492
  } catch {
514
- // Non-fatal: server is already running via spawn
493
+ // Non-fatal
494
+ }
495
+ }
496
+
497
+ // If no service manager started it, spawn directly
498
+ try {
499
+ await fetch(`http://localhost:${castlePort}`);
500
+ } catch {
501
+ // Server not up yet — spawn it directly as fallback
502
+ const server = spawn(nodePath, [nextBin, "start", "-p", castlePort], {
503
+ cwd: PROJECT_ROOT,
504
+ stdio: ["ignore", "ignore", "ignore"],
505
+ detached: true,
506
+ });
507
+ if (server.pid != null) {
508
+ writeFile(pidFile, String(server.pid));
515
509
  }
510
+ server.unref();
516
511
  }
517
512
 
518
513
  // Wait for server to be ready
519
- const maxWait = 30000;
514
+ const maxWait = 45000;
520
515
  const startTime = Date.now();
521
516
  let serverReady = false;
522
517