@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.
- package/README.md +22 -8
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts +49 -0
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts.map +1 -0
- package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.js +7 -0
- package/dist/packages/core/src/application/ports/output/services/index.d.ts +1 -0
- package/dist/packages/core/src/application/ports/output/services/index.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/di/container.js +2 -0
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts +1 -0
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/browser-opener.service.js +4 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts +16 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts.map +1 -0
- package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.js +72 -0
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts +7 -0
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map +1 -1
- package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.js +9 -0
- package/dist/src/presentation/cli/commands/_serve.command.d.ts +32 -0
- package/dist/src/presentation/cli/commands/_serve.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/_serve.command.js +91 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts +27 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/daemon/start-daemon.js +135 -0
- package/dist/src/presentation/cli/commands/start.command.d.ts +15 -0
- package/dist/src/presentation/cli/commands/start.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/start.command.js +33 -0
- package/dist/src/presentation/cli/commands/status.command.d.ts +20 -0
- package/dist/src/presentation/cli/commands/status.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/status.command.js +119 -0
- package/dist/src/presentation/cli/commands/stop.command.d.ts +22 -0
- package/dist/src/presentation/cli/commands/stop.command.d.ts.map +1 -0
- package/dist/src/presentation/cli/commands/stop.command.js +79 -0
- package/dist/src/presentation/cli/index.d.ts +5 -2
- package/dist/src/presentation/cli/index.d.ts.map +1 -1
- package/dist/src/presentation/cli/index.js +23 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/cache/.previewinfo +1 -1
- package/web/.next/cache/.rscinfo +1 -1
- package/web/.next/cache/.tsbuildinfo +1 -1
- package/web/.next/cache/config.json +3 -3
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +1 -1
- package/web/.next/required-server-files.json +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
- package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
- package/web/.next/standalone/src/presentation/web/server.js +1 -1
- package/web/.next/static/chunks/{702384d364482e4b.js → 3eaa6f2966e27b8f.js} +2 -2
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → JmDtIt3S6EfLuRKwlzJrW}/_buildManifest.js +0 -0
- /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → JmDtIt3S6EfLuRKwlzJrW}/_clientMiddlewareManifest.json +0 -0
- /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.
|
|
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
|
-
#
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
|
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
|
|
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
|
package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts
ADDED
|
@@ -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
|
package/dist/packages/core/src/application/ports/output/services/daemon-service.interface.d.ts.map
ADDED
|
@@ -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"}
|
|
@@ -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;
|
|
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;
|
|
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
|
+
}
|
package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts
CHANGED
|
@@ -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.
|
package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map
CHANGED
|
@@ -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"}
|