@shepai/cli 1.54.0 → 1.55.0

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 (90) hide show
  1. package/README.md +22 -8
  2. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts +49 -0
  3. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts.map +1 -0
  4. package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.js +7 -0
  5. package/dist/packages/core/src/application/ports/output/services/index.d.ts +1 -0
  6. package/dist/packages/core/src/application/ports/output/services/index.d.ts.map +1 -1
  7. package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
  8. package/dist/packages/core/src/infrastructure/di/container.js +2 -0
  9. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts +1 -0
  10. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts.map +1 -1
  11. package/dist/packages/core/src/infrastructure/services/browser-opener.service.js +4 -0
  12. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts +16 -0
  13. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts.map +1 -0
  14. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.js +72 -0
  15. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts +7 -0
  16. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map +1 -1
  17. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.js +9 -0
  18. package/dist/src/presentation/cli/commands/_serve.command.d.ts +32 -0
  19. package/dist/src/presentation/cli/commands/_serve.command.d.ts.map +1 -0
  20. package/dist/src/presentation/cli/commands/_serve.command.js +91 -0
  21. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts +27 -0
  22. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts.map +1 -0
  23. package/dist/src/presentation/cli/commands/daemon/start-daemon.js +135 -0
  24. package/dist/src/presentation/cli/commands/start.command.d.ts +15 -0
  25. package/dist/src/presentation/cli/commands/start.command.d.ts.map +1 -0
  26. package/dist/src/presentation/cli/commands/start.command.js +33 -0
  27. package/dist/src/presentation/cli/commands/status.command.d.ts +20 -0
  28. package/dist/src/presentation/cli/commands/status.command.d.ts.map +1 -0
  29. package/dist/src/presentation/cli/commands/status.command.js +119 -0
  30. package/dist/src/presentation/cli/commands/stop.command.d.ts +22 -0
  31. package/dist/src/presentation/cli/commands/stop.command.d.ts.map +1 -0
  32. package/dist/src/presentation/cli/commands/stop.command.js +79 -0
  33. package/dist/src/presentation/cli/index.d.ts +5 -2
  34. package/dist/src/presentation/cli/index.d.ts.map +1 -1
  35. package/dist/src/presentation/cli/index.js +23 -4
  36. package/dist/tsconfig.build.tsbuildinfo +1 -1
  37. package/package.json +1 -1
  38. package/web/.next/BUILD_ID +1 -1
  39. package/web/.next/build-manifest.json +2 -2
  40. package/web/.next/cache/.previewinfo +1 -1
  41. package/web/.next/cache/.rscinfo +1 -1
  42. package/web/.next/cache/.tsbuildinfo +1 -1
  43. package/web/.next/cache/config.json +3 -3
  44. package/web/.next/fallback-build-manifest.json +2 -2
  45. package/web/.next/prerender-manifest.json +3 -3
  46. package/web/.next/required-server-files.js +1 -1
  47. package/web/.next/required-server-files.json +1 -1
  48. package/web/.next/server/app/_global-error.html +2 -2
  49. package/web/.next/server/app/_global-error.rsc +1 -1
  50. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  51. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  56. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  58. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  59. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  60. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
  61. package/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
  62. package/web/.next/server/pages/500.html +2 -2
  63. package/web/.next/server/server-reference-manifest.js +1 -1
  64. package/web/.next/server/server-reference-manifest.json +15 -15
  65. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  66. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  67. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  68. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  69. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  70. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  71. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  73. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  77. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  79. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  80. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
  81. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  82. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  83. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  84. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  85. package/web/.next/static/chunks/{702384d364482e4b.js → 3eaa6f2966e27b8f.js} +2 -2
  86. package/web/.next/trace +1 -1
  87. package/web/.next/trace-build +1 -1
  88. /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → JmDtIt3S6EfLuRKwlzJrW}/_buildManifest.js +0 -0
  89. /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → JmDtIt3S6EfLuRKwlzJrW}/_clientMiddlewareManifest.json +0 -0
  90. /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → JmDtIt3S6EfLuRKwlzJrW}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -198,7 +198,7 @@ Running AI agents is easy. **Managing many of them through a full SDLC is the ha
198
198
 
199
199
  ## Features
200
200
 
201
- > 🚧 **Under active development** — Web UI screenshots are from the design spec. Core CLI (`feat new`, `feat ls`, `feat show`, `settings`, `ui`) works today. Some commands (`mem`, `start`) are still WIP.
201
+ > 🚧 **Under active development** — Web UI screenshots are from the design spec. Core CLI (`feat new`, `feat ls`, `feat show`, `settings`, `ui`, `start`, `stop`, `status`) works today. The `mem` command is still WIP.
202
202
 
203
203
  <table>
204
204
  <tr>
@@ -338,25 +338,39 @@ Running AI agents is easy. **Managing many of them through a full SDLC is the ha
338
338
  ## Quick Start
339
339
 
340
340
  ```bash
341
- # Navigate to any repo and create a feature
342
- cd ~/projects/your-app
343
- shep feat new "Add user authentication with OAuth 2.0"
341
+ # 1. Install
342
+ npm i -g @shepai/cli
343
+
344
+ # 2. Run Shep — completes setup on first run, then opens the web UI
345
+ shep
346
+
347
+ # 3. Browser opens at http://localhost:4050 — you're in
348
+ ```
344
349
 
345
- # Open the control center to watch progress
346
- shep ui
350
+ ## Daemon Management
351
+
352
+ `shep` runs the web UI as a background daemon so your terminal stays free. Use these commands to manage it:
353
+
354
+ ```bash
355
+ shep start # Start (or re-open) the web UI daemon
356
+ shep stop # Gracefully stop the daemon
357
+ shep status # Show live PID, port, uptime, CPU%, and memory
347
358
  ```
348
359
 
349
360
  ## CLI
350
361
 
351
362
  ```
352
- shep start Start background service
363
+ shep Start daemon (onboarding on first run)
364
+ shep start [--port <number>] Start the web UI as a background daemon
365
+ shep stop Stop the running web UI daemon
366
+ shep status Show daemon status and live metrics
353
367
  shep feat new <description> Create a new feature
354
368
  shep feat new --interactive Interactive mode with human checkpoints
355
369
  shep feat new --allow-all Full autonomous mode
356
370
  shep feat ls List features across repos
357
371
  shep feat show <id> Show feature details
358
372
  shep feat preview <id> Preview feature dev environment
359
- shep ui Open the web control center
373
+ shep ui Start the web UI in the foreground
360
374
  shep settings show Display current configuration
361
375
  shep settings agent Configure AI coding agent
362
376
  shep mem add <text> --global|--repo Add agent memory
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Daemon Service Interface
3
+ *
4
+ * Output port for managing the daemon state file (~/.shep/daemon.json).
5
+ * Infrastructure layer provides the concrete DaemonPidService implementation.
6
+ */
7
+ /**
8
+ * Shape of the daemon state persisted to ~/.shep/daemon.json.
9
+ */
10
+ export interface DaemonState {
11
+ /** PID of the running daemon process */
12
+ pid: number;
13
+ /** Port the daemon web server is listening on */
14
+ port: number;
15
+ /** ISO 8601 timestamp of when the daemon was started */
16
+ startedAt: string;
17
+ }
18
+ /**
19
+ * Port interface for reading and writing daemon lifecycle state.
20
+ *
21
+ * Implementations must:
22
+ * - Write atomically (write-to-temp + rename) to prevent corruption
23
+ * - Return null from read() when no daemon.json exists (ENOENT)
24
+ * - Swallow ENOENT in delete() — safe to call when file is absent
25
+ * - Validate pid before any process.kill() call
26
+ */
27
+ export interface IDaemonService {
28
+ /**
29
+ * Read the daemon state from daemon.json.
30
+ * Returns null if the file does not exist.
31
+ */
32
+ read(): Promise<DaemonState | null>;
33
+ /**
34
+ * Atomically write daemon state to daemon.json.
35
+ * Uses write-to-temp + fs.rename to prevent partial writes.
36
+ */
37
+ write(data: DaemonState): Promise<void>;
38
+ /**
39
+ * Delete daemon.json. Safe to call when file is absent (ENOENT swallowed).
40
+ */
41
+ delete(): Promise<void>;
42
+ /**
43
+ * Check whether a process with the given PID is alive.
44
+ * Uses process.kill(pid, 0) — returns true if alive, false on ESRCH.
45
+ * Validates that pid is a positive finite integer before checking.
46
+ */
47
+ isAlive(pid: number): boolean;
48
+ }
49
+ //# sourceMappingURL=daemon-service.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-service.interface.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/application/ports/output/services/daemon-service.interface.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEpC;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/B"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Daemon Service Interface
3
+ *
4
+ * Output port for managing the daemon state file (~/.shep/daemon.json).
5
+ * Infrastructure layer provides the concrete DaemonPidService implementation.
6
+ */
7
+ export {};
@@ -15,4 +15,5 @@ export type { INotificationService } from './notification-service.interface.js';
15
15
  export type { IGitPrService, CiStatus, CiStatusResult, DiffSummary, MergeStrategy, PrCreateResult, } from './git-pr-service.interface.js';
16
16
  export { GitPrError, GitPrErrorCode } from './git-pr-service.interface.js';
17
17
  export type { IIdeLauncherService, LaunchIdeInput, LaunchIdeResult, LaunchIdeSuccess, LaunchIdeFailed, } from './ide-launcher-service.interface.js';
18
+ export type { IDaemonService, DaemonState } from './daemon-service.interface.js';
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/application/ports/output/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAClG,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACnF,YAAY,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACzE,YAAY,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAChF,YAAY,EACV,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,aAAa,EACb,cAAc,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3E,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,qCAAqC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/application/ports/output/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAClG,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,uCAAuC,CAAC;AAC/C,YAAY,EACV,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACnF,YAAY,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACzE,YAAY,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAChF,YAAY,EACV,aAAa,EACb,QAAQ,EACR,cAAc,EACd,WAAW,EACX,aAAa,EACb,cAAc,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3E,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,qCAAqC,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../../../../../packages/core/src/infrastructure/di/container.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AA2GrC;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,SAAS,CAAC,CAsPrE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../../../../../packages/core/src/infrastructure/di/container.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AA6GrC;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,SAAS,CAAC,CAuPrE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,CAAC"}
@@ -24,6 +24,7 @@ import { WorktreeService } from '../services/git/worktree.service.js';
24
24
  import { ToolInstallerServiceImpl } from '../services/tool-installer/tool-installer.service.js';
25
25
  import { GitPrService } from '../services/git/git-pr.service.js';
26
26
  import { JsonDrivenIdeLauncherService } from '../services/ide-launchers/json-driven-ide-launcher.service.js';
27
+ import { DaemonPidService } from '../services/daemon/daemon-pid.service.js';
27
28
  import { AgentExecutorFactory } from '../services/agents/common/agent-executor-factory.service.js';
28
29
  import { AgentExecutorProvider } from '../services/agents/common/agent-executor-provider.service.js';
29
30
  import { StructuredAgentCallerService } from '../services/agents/common/structured-agent-caller.service.js';
@@ -132,6 +133,7 @@ export async function initializeContainer() {
132
133
  container.registerSingleton('IToolInstallerService', ToolInstallerServiceImpl);
133
134
  container.registerSingleton('IGitPrService', GitPrService);
134
135
  container.registerSingleton('IIdeLauncherService', JsonDrivenIdeLauncherService);
136
+ container.registerSingleton('IDaemonService', DaemonPidService);
135
137
  // Register agent infrastructure
136
138
  container.register('IAgentRunRepository', {
137
139
  useFactory: (c) => {
@@ -3,6 +3,7 @@ export interface BrowserOpenerDeps {
3
3
  platform: NodeJS.Platform;
4
4
  execFile: (cmd: string, args: readonly string[], callback: (error: Error | null) => void) => ChildProcess;
5
5
  warn: (msg: string) => void;
6
+ isTTY: boolean;
6
7
  }
7
8
  export declare class BrowserOpenerService {
8
9
  private deps;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-opener.service.d.ts","sourceRoot":"","sources":["../../../../../../packages/core/src/infrastructure/services/browser-opener.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,KACpC,YAAY,CAAC;IAClB,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAeD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,IAAI,CAAoB;gBAEpB,IAAI,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAIjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAWxB"}
1
+ {"version":3,"file":"browser-opener.service.d.ts","sourceRoot":"","sources":["../../../../../../packages/core/src/infrastructure/services/browser-opener.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,KACpC,YAAY,CAAC;IAClB,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,OAAO,CAAC;CAChB;AAgBD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,IAAI,CAAoB;gBAEpB,IAAI,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAIjD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAcxB"}
@@ -4,6 +4,7 @@ const defaultDeps = {
4
4
  execFile: cpExecFile,
5
5
  // eslint-disable-next-line no-console
6
6
  warn: (msg) => console.warn(msg),
7
+ isTTY: !!process.stdout.isTTY,
7
8
  };
8
9
  const PLATFORM_COMMANDS = {
9
10
  darwin: { cmd: 'open', args: (url) => [url] },
@@ -16,6 +17,9 @@ export class BrowserOpenerService {
16
17
  this.deps = { ...defaultDeps, ...deps };
17
18
  }
18
19
  open(url) {
20
+ // Skip browser opening in non-interactive environments (tests, CI, piped output).
21
+ if (!this.deps.isTTY)
22
+ return;
19
23
  const entry = PLATFORM_COMMANDS[this.deps.platform];
20
24
  if (!entry)
21
25
  return;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DaemonPidService
3
+ *
4
+ * Infrastructure service for managing the daemon state file (~/.shep/daemon.json).
5
+ * Implements IDaemonService output port.
6
+ *
7
+ * Uses atomic write (write-to-temp + rename) to prevent partial-write corruption.
8
+ */
9
+ import type { IDaemonService, DaemonState } from '../../../application/ports/output/services/daemon-service.interface.js';
10
+ export declare class DaemonPidService implements IDaemonService {
11
+ read(): Promise<DaemonState | null>;
12
+ write(data: DaemonState): Promise<void>;
13
+ delete(): Promise<void>;
14
+ isAlive(pid: number): boolean;
15
+ }
16
+ //# sourceMappingURL=daemon-pid.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-pid.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/daemon/daemon-pid.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EACZ,MAAM,wEAAwE,CAAC;AAGhF,qBACa,gBAAiB,YAAW,cAAc;IAC/C,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAWnC,KAAK,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAU7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAS9B"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * DaemonPidService
3
+ *
4
+ * Infrastructure service for managing the daemon state file (~/.shep/daemon.json).
5
+ * Implements IDaemonService output port.
6
+ *
7
+ * Uses atomic write (write-to-temp + rename) to prevent partial-write corruption.
8
+ */
9
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
10
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
11
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
12
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
13
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
14
+ };
15
+ import { readFile, writeFile, unlink, rename } from 'node:fs/promises';
16
+ import { injectable } from 'tsyringe';
17
+ import { getDaemonStatePath } from '../filesystem/shep-directory.service.js';
18
+ let DaemonPidService = class DaemonPidService {
19
+ async read() {
20
+ const filePath = getDaemonStatePath();
21
+ try {
22
+ const raw = await readFile(filePath, 'utf-8');
23
+ return JSON.parse(raw);
24
+ }
25
+ catch (err) {
26
+ if (isEnoent(err))
27
+ return null;
28
+ throw err;
29
+ }
30
+ }
31
+ async write(data) {
32
+ const finalPath = getDaemonStatePath();
33
+ const tmpPath = `${finalPath}.tmp`;
34
+ await writeFile(tmpPath, JSON.stringify(data), 'utf-8');
35
+ await rename(tmpPath, finalPath);
36
+ }
37
+ async delete() {
38
+ const filePath = getDaemonStatePath();
39
+ try {
40
+ await unlink(filePath);
41
+ }
42
+ catch (err) {
43
+ if (isEnoent(err))
44
+ return;
45
+ throw err;
46
+ }
47
+ }
48
+ isAlive(pid) {
49
+ if (!isValidPid(pid))
50
+ return false;
51
+ try {
52
+ process.kill(pid, 0);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ };
60
+ DaemonPidService = __decorate([
61
+ injectable()
62
+ ], DaemonPidService);
63
+ export { DaemonPidService };
64
+ function isEnoent(err) {
65
+ return (typeof err === 'object' &&
66
+ err !== null &&
67
+ 'code' in err &&
68
+ err.code === 'ENOENT');
69
+ }
70
+ function isValidPid(pid) {
71
+ return Number.isFinite(pid) && Number.isInteger(pid) && pid > 0;
72
+ }
@@ -19,6 +19,13 @@ export declare function getShepHomeDir(): string;
19
19
  * @returns Path to the database file
20
20
  */
21
21
  export declare function getShepDbPath(): string;
22
+ /**
23
+ * Gets the path to the daemon state file.
24
+ * Uses SHEP_HOME env var if set (for test isolation), otherwise ~/.shep/daemon.json
25
+ *
26
+ * @returns Path to daemon.json
27
+ */
28
+ export declare function getDaemonStatePath(): string;
22
29
  /**
23
30
  * Ensures the shep home directory exists with correct permissions.
24
31
  * Creates the directory if it doesn't exist.
@@ -1 +1 @@
1
- {"version":3,"file":"shep-directory.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/filesystem/shep-directory.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBzD"}
1
+ {"version":3,"file":"shep-directory.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/filesystem/shep-directory.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBzD"}
@@ -34,6 +34,15 @@ export function getShepHomeDir() {
34
34
  export function getShepDbPath() {
35
35
  return join(resolveShepHomeDir(), 'data');
36
36
  }
37
+ /**
38
+ * Gets the path to the daemon state file.
39
+ * Uses SHEP_HOME env var if set (for test isolation), otherwise ~/.shep/daemon.json
40
+ *
41
+ * @returns Path to daemon.json
42
+ */
43
+ export function getDaemonStatePath() {
44
+ return join(resolveShepHomeDir(), 'daemon.json');
45
+ }
37
46
  /**
38
47
  * Ensures the shep home directory exists with correct permissions.
39
48
  * Creates the directory if it doesn't exist.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * _serve Command (hidden daemon entry point)
3
+ *
4
+ * Runs the web server in-process as a detached daemon child.
5
+ * This command is hidden from --help output and is invoked internally by
6
+ * the startDaemon() helper via child_process.spawn({detached: true}).
7
+ *
8
+ * NOTE: The DI container and settings are already initialized by index.ts
9
+ * bootstrap() before Commander dispatches to this action. This command
10
+ * only needs to start the web server and notification watcher.
11
+ *
12
+ * Lifecycle:
13
+ * 1. Start WebServerService on the provided --port
14
+ * 2. Initialize notification watcher
15
+ * 3. Block until SIGTERM or SIGINT triggers graceful shutdown
16
+ *
17
+ * The shutdown sequence mirrors ui.command.ts:
18
+ * - Set isShuttingDown flag (idempotent — prevents double-shutdown)
19
+ * - Start 5s forceExit timer (unref'd so it doesn't block)
20
+ * - Stop notification watcher
21
+ * - Stop WebServerService
22
+ * - process.exit(0)
23
+ *
24
+ * Usage (internal only):
25
+ * process.execPath _serve --port <N>
26
+ */
27
+ import { Command } from 'commander';
28
+ /**
29
+ * Create the hidden _serve command (daemon child entry point).
30
+ */
31
+ export declare function createServeCommand(): Command;
32
+ //# sourceMappingURL=_serve.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_serve.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/_serve.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAuB1D;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAwD5C"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * _serve Command (hidden daemon entry point)
3
+ *
4
+ * Runs the web server in-process as a detached daemon child.
5
+ * This command is hidden from --help output and is invoked internally by
6
+ * the startDaemon() helper via child_process.spawn({detached: true}).
7
+ *
8
+ * NOTE: The DI container and settings are already initialized by index.ts
9
+ * bootstrap() before Commander dispatches to this action. This command
10
+ * only needs to start the web server and notification watcher.
11
+ *
12
+ * Lifecycle:
13
+ * 1. Start WebServerService on the provided --port
14
+ * 2. Initialize notification watcher
15
+ * 3. Block until SIGTERM or SIGINT triggers graceful shutdown
16
+ *
17
+ * The shutdown sequence mirrors ui.command.ts:
18
+ * - Set isShuttingDown flag (idempotent — prevents double-shutdown)
19
+ * - Start 5s forceExit timer (unref'd so it doesn't block)
20
+ * - Stop notification watcher
21
+ * - Stop WebServerService
22
+ * - process.exit(0)
23
+ *
24
+ * Usage (internal only):
25
+ * process.execPath _serve --port <N>
26
+ */
27
+ import { Command, InvalidArgumentError } from 'commander';
28
+ import { container } from '../../../../packages/core/src/infrastructure/di/container.js';
29
+ import { setVersionEnvVars } from '../../../../packages/core/src/infrastructure/services/version.service.js';
30
+ import { resolveWebDir } from '../../../../packages/core/src/infrastructure/services/web-server.service.js';
31
+ import { initializeNotificationWatcher, getNotificationWatcher, } from '../../../../packages/core/src/infrastructure/services/notifications/notification-watcher.service.js';
32
+ function parsePort(value) {
33
+ const port = parseInt(value, 10);
34
+ if (isNaN(port) || port < 1024 || port > 65535) {
35
+ throw new InvalidArgumentError('Port must be an integer between 1024 and 65535');
36
+ }
37
+ return port;
38
+ }
39
+ /**
40
+ * Create the hidden _serve command (daemon child entry point).
41
+ */
42
+ export function createServeCommand() {
43
+ const cmd = new Command('_serve')
44
+ .description('Start the web server daemon (internal use only)')
45
+ .helpOption(false)
46
+ .addHelpCommand(false)
47
+ .option('-p, --port <number>', 'Port to listen on', parsePort)
48
+ .action(async (options) => {
49
+ try {
50
+ const port = options.port ?? 4050;
51
+ const { dir, dev } = resolveWebDir();
52
+ // Set version env vars for the web UI
53
+ const versionService = container.resolve('IVersionService');
54
+ setVersionEnvVars(versionService.getVersion());
55
+ // Start the web server
56
+ const service = container.resolve('IWebServerService');
57
+ await service.start(port, dir, dev);
58
+ // Start notification watcher
59
+ const runRepo = container.resolve('IAgentRunRepository');
60
+ const phaseTimingRepo = container.resolve('IPhaseTimingRepository');
61
+ const featureRepo = container.resolve('IFeatureRepository');
62
+ const notificationService = container.resolve('INotificationService');
63
+ initializeNotificationWatcher(runRepo, phaseTimingRepo, featureRepo, notificationService);
64
+ getNotificationWatcher().start();
65
+ // Graceful shutdown handler — identical pattern to ui.command.ts
66
+ let isShuttingDown = false;
67
+ const shutdown = async () => {
68
+ if (isShuttingDown)
69
+ return;
70
+ isShuttingDown = true;
71
+ // Force exit after 5s if graceful shutdown stalls
72
+ const forceExit = setTimeout(() => process.exit(0), 5000);
73
+ forceExit.unref();
74
+ getNotificationWatcher().stop();
75
+ await service.stop();
76
+ process.exit(0);
77
+ };
78
+ process.on('SIGTERM', shutdown);
79
+ process.on('SIGINT', shutdown);
80
+ }
81
+ catch (error) {
82
+ const err = error instanceof Error ? error : new Error(String(error));
83
+ // Write to stderr so it appears in daemon logs if redirected
84
+ process.stderr.write(`[_serve] Fatal error: ${err.message}\n`);
85
+ process.exit(1);
86
+ }
87
+ });
88
+ // Mark hidden so Commander omits it from --help output
89
+ cmd._hidden = true;
90
+ return cmd;
91
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * startDaemon() — Shared daemon-spawn helper
3
+ *
4
+ * Contains the parent-side logic for starting the Shep web UI as a
5
+ * detached background daemon. Used by both:
6
+ * - The default `shep` action (index.ts)
7
+ * - The `shep start` command (start.command.ts)
8
+ *
9
+ * Flow:
10
+ * 1. Resolve available port (respects --port override)
11
+ * 2. Check if daemon is already running (idempotent — print URL and return)
12
+ * 3. Spawn the daemon with execArgv propagated (supports tsx in dev mode)
13
+ * {detached: true, stdio: ['ignore','ignore','pipe']} + child.unref()
14
+ * 4. Wait briefly to confirm the child is alive; surface stderr on crash
15
+ * 5. Write daemon.json atomically via IDaemonService
16
+ * 6. Print formatted URL to stdout
17
+ * 7. Open browser via BrowserOpenerService
18
+ */
19
+ export interface StartDaemonOptions {
20
+ port?: number;
21
+ }
22
+ /**
23
+ * Start the Shep web UI as a detached background daemon.
24
+ * Idempotent: if a daemon is already running, prints the existing URL and returns.
25
+ */
26
+ export declare function startDaemon(opts?: StartDaemonOptions): Promise<void>;
27
+ //# sourceMappingURL=start-daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start-daemon.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/daemon/start-daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAkBH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F9E"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * startDaemon() — Shared daemon-spawn helper
3
+ *
4
+ * Contains the parent-side logic for starting the Shep web UI as a
5
+ * detached background daemon. Used by both:
6
+ * - The default `shep` action (index.ts)
7
+ * - The `shep start` command (start.command.ts)
8
+ *
9
+ * Flow:
10
+ * 1. Resolve available port (respects --port override)
11
+ * 2. Check if daemon is already running (idempotent — print URL and return)
12
+ * 3. Spawn the daemon with execArgv propagated (supports tsx in dev mode)
13
+ * {detached: true, stdio: ['ignore','ignore','pipe']} + child.unref()
14
+ * 4. Wait briefly to confirm the child is alive; surface stderr on crash
15
+ * 5. Write daemon.json atomically via IDaemonService
16
+ * 6. Print formatted URL to stdout
17
+ * 7. Open browser via BrowserOpenerService
18
+ */
19
+ import { spawn } from 'node:child_process';
20
+ import http from 'node:http';
21
+ import { container } from '../../../../../packages/core/src/infrastructure/di/container.js';
22
+ import { findAvailablePort, DEFAULT_PORT } from '../../../../../packages/core/src/infrastructure/services/port.service.js';
23
+ import { BrowserOpenerService } from '../../../../../packages/core/src/infrastructure/services/browser-opener.service.js';
24
+ import { fmt, messages, spinner } from '../../ui/index.js';
25
+ /** How long to wait (ms) after spawn to verify the child is still alive. */
26
+ const SPAWN_SETTLE_MS = 500;
27
+ /** Max time (ms) to wait for the server to become reachable before opening the browser. */
28
+ const READY_TIMEOUT_MS = 30_000;
29
+ /** Interval (ms) between readiness probes. */
30
+ const READY_POLL_MS = 300;
31
+ /**
32
+ * Start the Shep web UI as a detached background daemon.
33
+ * Idempotent: if a daemon is already running, prints the existing URL and returns.
34
+ */
35
+ export async function startDaemon(opts = {}) {
36
+ const daemonService = container.resolve('IDaemonService');
37
+ // Check for an already-running daemon
38
+ const existing = await daemonService.read();
39
+ if (existing && daemonService.isAlive(existing.pid)) {
40
+ const url = `http://localhost:${existing.port}`;
41
+ messages.newline();
42
+ messages.info(`Shep is already running at ${fmt.code(url)}`);
43
+ messages.newline();
44
+ return;
45
+ }
46
+ // Resolve the port
47
+ const startPort = opts.port ?? DEFAULT_PORT;
48
+ const port = await findAvailablePort(startPort);
49
+ // Spawn the daemon as a detached child process.
50
+ // Propagate process.execArgv so tsx loader hooks (--require / --import) are
51
+ // available in dev mode. In production (compiled JS), execArgv is empty.
52
+ const child = spawn(process.execPath, [...process.execArgv, process.argv[1], '_serve', '--port', String(port)], {
53
+ detached: true,
54
+ // Keep stderr piped so we can surface startup errors; stdin/stdout detached.
55
+ stdio: ['ignore', 'ignore', 'pipe'],
56
+ });
57
+ // Collect stderr in case the child crashes during startup.
58
+ let stderrChunks = [];
59
+ child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
60
+ // Wait briefly for the child to either settle or crash.
61
+ const exitCode = await Promise.race([
62
+ new Promise((resolve) => child.on('exit', (code) => resolve(code))),
63
+ new Promise((resolve) => setTimeout(() => resolve(undefined), SPAWN_SETTLE_MS)),
64
+ ]);
65
+ if (exitCode !== undefined) {
66
+ // Child exited during the settle window — startup failed.
67
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
68
+ messages.newline();
69
+ messages.error(`Daemon failed to start (exit code ${exitCode ?? 'unknown'}).`);
70
+ if (stderr) {
71
+ console.error(stderr);
72
+ }
73
+ messages.newline();
74
+ // Clean up stale daemon.json if it exists
75
+ await daemonService.delete();
76
+ return;
77
+ }
78
+ // Child is alive — detach fully.
79
+ // Unref stderr so the parent event loop is not held open, then unref the child.
80
+ child.stderr.destroy();
81
+ stderrChunks = [];
82
+ child.unref();
83
+ // Write daemon.json atomically
84
+ await daemonService.write({
85
+ pid: child.pid,
86
+ port,
87
+ startedAt: new Date().toISOString(),
88
+ });
89
+ const url = `http://localhost:${port}`;
90
+ messages.newline();
91
+ console.log(fmt.heading('Shep Web UI'));
92
+ messages.newline();
93
+ // Poll until the server responds, with a spinner on stderr.
94
+ // Skip readiness check in E2E / CI environments where the daemon child
95
+ // cannot actually start a Next.js server within the test's time window.
96
+ if (process.env.SHEP_SKIP_READINESS_CHECK) {
97
+ messages.success(`Daemon spawned at ${fmt.code(url)}`);
98
+ }
99
+ else {
100
+ const ready = await spinner('Starting server', () => waitForServer(url, READY_TIMEOUT_MS));
101
+ if (ready) {
102
+ messages.success(`Server ready at ${fmt.code(url)}`);
103
+ }
104
+ else {
105
+ messages.warning(`Server may still be starting at ${fmt.code(url)}`);
106
+ }
107
+ }
108
+ messages.newline();
109
+ const opener = new BrowserOpenerService({ warn: messages.warning });
110
+ opener.open(url);
111
+ }
112
+ /**
113
+ * Poll a URL until it returns any HTTP response (even 500).
114
+ * Resolves true when reachable, false on timeout.
115
+ */
116
+ function waitForServer(url, timeoutMs) {
117
+ const deadline = Date.now() + timeoutMs;
118
+ return new Promise((resolve) => {
119
+ const probe = () => {
120
+ if (Date.now() > deadline)
121
+ return resolve(false);
122
+ const req = http.get(url, () => {
123
+ resolve(true);
124
+ });
125
+ req.on('error', () => {
126
+ setTimeout(probe, READY_POLL_MS);
127
+ });
128
+ req.setTimeout(2000, () => {
129
+ req.destroy();
130
+ setTimeout(probe, READY_POLL_MS);
131
+ });
132
+ };
133
+ probe();
134
+ });
135
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * start Command
3
+ *
4
+ * Starts the Shep web UI as a detached background daemon.
5
+ * All spawn logic lives in the shared startDaemon() helper to avoid
6
+ * duplication between this command and the default `shep` action.
7
+ *
8
+ * Usage: shep start [--port <number>]
9
+ */
10
+ import { Command } from 'commander';
11
+ /**
12
+ * Create the start command.
13
+ */
14
+ export declare function createStartCommand(): Command;
15
+ //# sourceMappingURL=start.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/start.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAwB,MAAM,WAAW,CAAC;AAW1D;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAc5C"}