@shepai/cli 1.54.0 → 1.55.1
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/persistence/sqlite/mappers/feature.mapper.js +11 -11
- 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 → 7d66f03293ffca30.js} +2 -2
- package/web/.next/trace +1 -1
- package/web/.next/trace-build +1 -1
- /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → nEAJoHmU4elsBOt8hrgha}/_buildManifest.js +0 -0
- /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → nEAJoHmU4elsBOt8hrgha}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → nEAJoHmU4elsBOt8hrgha}/_ssgManifest.js +0 -0
|
@@ -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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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, InvalidArgumentError } from 'commander';
|
|
11
|
+
import { startDaemon } from './daemon/start-daemon.js';
|
|
12
|
+
function parsePort(value) {
|
|
13
|
+
const port = parseInt(value, 10);
|
|
14
|
+
if (isNaN(port) || port < 1024 || port > 65535) {
|
|
15
|
+
throw new InvalidArgumentError('Port must be an integer between 1024 and 65535');
|
|
16
|
+
}
|
|
17
|
+
return port;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create the start command.
|
|
21
|
+
*/
|
|
22
|
+
export function createStartCommand() {
|
|
23
|
+
return new Command('start')
|
|
24
|
+
.description('Start the Shep web UI as a background daemon')
|
|
25
|
+
.option('-p, --port <number>', 'Port number (1024-65535)', parsePort)
|
|
26
|
+
.addHelpText('after', `
|
|
27
|
+
Examples:
|
|
28
|
+
$ shep start Start on default port (4050)
|
|
29
|
+
$ shep start --port 8080 Start on custom port`)
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
await startDaemon({ port: options.port });
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status Command
|
|
3
|
+
*
|
|
4
|
+
* Displays live status and metrics for the running Shep web UI daemon.
|
|
5
|
+
*
|
|
6
|
+
* Metrics collected:
|
|
7
|
+
* - PID, port, URL (from daemon.json)
|
|
8
|
+
* - Uptime (computed from startedAt)
|
|
9
|
+
* - CPU%, RSS memory (from ps shell-out via execFile — injection-safe)
|
|
10
|
+
*
|
|
11
|
+
* Gracefully degrades if ps is unavailable (timeout or error).
|
|
12
|
+
*
|
|
13
|
+
* Usage: shep status
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
/**
|
|
17
|
+
* Create the status command.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createStatusCommand(): Command;
|
|
20
|
+
//# sourceMappingURL=status.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/status.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+EpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CA2C7C"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status Command
|
|
3
|
+
*
|
|
4
|
+
* Displays live status and metrics for the running Shep web UI daemon.
|
|
5
|
+
*
|
|
6
|
+
* Metrics collected:
|
|
7
|
+
* - PID, port, URL (from daemon.json)
|
|
8
|
+
* - Uptime (computed from startedAt)
|
|
9
|
+
* - CPU%, RSS memory (from ps shell-out via execFile — injection-safe)
|
|
10
|
+
*
|
|
11
|
+
* Gracefully degrades if ps is unavailable (timeout or error).
|
|
12
|
+
*
|
|
13
|
+
* Usage: shep status
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
import { execFile } from 'node:child_process';
|
|
17
|
+
import { container } from '../../../../packages/core/src/infrastructure/di/container.js';
|
|
18
|
+
import { renderDetailView, messages } from '../ui/index.js';
|
|
19
|
+
const PS_TIMEOUT_MS = 2000;
|
|
20
|
+
/**
|
|
21
|
+
* Parse ps output: "pid cpu% rss_kb\n"
|
|
22
|
+
* Returns formatted strings for display.
|
|
23
|
+
*/
|
|
24
|
+
function parsePs(output) {
|
|
25
|
+
const parts = output.trim().split(/\s+/);
|
|
26
|
+
const cpu = parts[1] ?? '?';
|
|
27
|
+
const rssKb = parseFloat(parts[2] ?? '0');
|
|
28
|
+
const rssMb = (rssKb / 1024).toFixed(1);
|
|
29
|
+
return { cpu, rssMb };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Human-readable uptime string, e.g. "2h 14m 3s".
|
|
33
|
+
*/
|
|
34
|
+
function humanizeUptime(ms) {
|
|
35
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
36
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
37
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
38
|
+
const seconds = totalSeconds % 60;
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (hours > 0)
|
|
41
|
+
parts.push(`${hours}h`);
|
|
42
|
+
if (minutes > 0)
|
|
43
|
+
parts.push(`${minutes}m`);
|
|
44
|
+
parts.push(`${seconds}s`);
|
|
45
|
+
return parts.join(' ');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Shell out to ps for CPU% and RSS, with a timeout guard.
|
|
49
|
+
* Uses execFile (not exec) — the PID is a separate array argument,
|
|
50
|
+
* preventing any possibility of shell injection.
|
|
51
|
+
*/
|
|
52
|
+
function fetchPsMetrics(pid) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const pidStr = String(pid);
|
|
55
|
+
let settled = false;
|
|
56
|
+
const fallback = () => ({ cpu: 'unavailable', rssMb: 'unavailable' });
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
if (!settled) {
|
|
59
|
+
settled = true;
|
|
60
|
+
child.kill();
|
|
61
|
+
resolve(fallback());
|
|
62
|
+
}
|
|
63
|
+
}, PS_TIMEOUT_MS);
|
|
64
|
+
const child = execFile('ps', ['-o', 'pid=,pcpu=,rss=', '-p', pidStr], { timeout: PS_TIMEOUT_MS }, (err, stdout) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
if (err || !stdout.trim()) {
|
|
70
|
+
resolve(fallback());
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
resolve(parsePs(stdout));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create the status command.
|
|
80
|
+
*/
|
|
81
|
+
export function createStatusCommand() {
|
|
82
|
+
return new Command('status')
|
|
83
|
+
.description('Show the status of the Shep web UI daemon')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
const daemonService = container.resolve('IDaemonService');
|
|
86
|
+
const state = await daemonService.read();
|
|
87
|
+
if (!state || !daemonService.isAlive(state.pid)) {
|
|
88
|
+
messages.newline();
|
|
89
|
+
messages.info('Shep daemon is not running.');
|
|
90
|
+
messages.info('Run `shep start` to launch it.');
|
|
91
|
+
messages.newline();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const { pid, port, startedAt } = state;
|
|
95
|
+
const url = `http://localhost:${port}`;
|
|
96
|
+
const uptimeMs = Date.now() - Date.parse(startedAt);
|
|
97
|
+
const uptime = humanizeUptime(uptimeMs);
|
|
98
|
+
const { cpu, rssMb } = await fetchPsMetrics(pid);
|
|
99
|
+
renderDetailView({
|
|
100
|
+
title: 'Shep Daemon Status',
|
|
101
|
+
sections: [
|
|
102
|
+
{
|
|
103
|
+
fields: [
|
|
104
|
+
{ label: 'PID', value: String(pid) },
|
|
105
|
+
{ label: 'Port', value: String(port) },
|
|
106
|
+
{ label: 'URL', value: url },
|
|
107
|
+
{ label: 'Started', value: new Date(startedAt).toLocaleString() },
|
|
108
|
+
{ label: 'Uptime', value: uptime },
|
|
109
|
+
{ label: 'CPU %', value: cpu === 'unavailable' ? 'unavailable' : `${cpu}%` },
|
|
110
|
+
{
|
|
111
|
+
label: 'Memory (RSS)',
|
|
112
|
+
value: rssMb === 'unavailable' ? 'unavailable' : `${rssMb} MB`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stop Command
|
|
3
|
+
*
|
|
4
|
+
* Stops the running Shep web UI daemon.
|
|
5
|
+
*
|
|
6
|
+
* Stop sequence:
|
|
7
|
+
* 1. Read daemon.json via IDaemonService
|
|
8
|
+
* 2. If no daemon / PID not alive: print clear message and exit 0
|
|
9
|
+
* 3. Validate PID is a positive finite integer
|
|
10
|
+
* 4. Send SIGTERM
|
|
11
|
+
* 5. Poll process liveness every 200ms for up to 5000ms
|
|
12
|
+
* 6. If still alive after 5s, send SIGKILL
|
|
13
|
+
* 7. Always delete daemon.json (in finally block)
|
|
14
|
+
*
|
|
15
|
+
* Usage: shep stop
|
|
16
|
+
*/
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
/**
|
|
19
|
+
* Create the stop command.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createStopCommand(): Command;
|
|
22
|
+
//# sourceMappingURL=stop.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stop.command.d.ts","sourceRoot":"","sources":["../../../../../src/presentation/cli/commands/stop.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CA+C3C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stop Command
|
|
3
|
+
*
|
|
4
|
+
* Stops the running Shep web UI daemon.
|
|
5
|
+
*
|
|
6
|
+
* Stop sequence:
|
|
7
|
+
* 1. Read daemon.json via IDaemonService
|
|
8
|
+
* 2. If no daemon / PID not alive: print clear message and exit 0
|
|
9
|
+
* 3. Validate PID is a positive finite integer
|
|
10
|
+
* 4. Send SIGTERM
|
|
11
|
+
* 5. Poll process liveness every 200ms for up to 5000ms
|
|
12
|
+
* 6. If still alive after 5s, send SIGKILL
|
|
13
|
+
* 7. Always delete daemon.json (in finally block)
|
|
14
|
+
*
|
|
15
|
+
* Usage: shep stop
|
|
16
|
+
*/
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import { container } from '../../../../packages/core/src/infrastructure/di/container.js';
|
|
19
|
+
import { messages } from '../ui/index.js';
|
|
20
|
+
const POLL_INTERVAL_MS = 200;
|
|
21
|
+
const MAX_WAIT_MS = 5000;
|
|
22
|
+
/**
|
|
23
|
+
* Poll until the given PID is dead or the timeout expires.
|
|
24
|
+
* Returns true if the process is dead, false if it survived the timeout.
|
|
25
|
+
*/
|
|
26
|
+
async function pollUntilDead(daemonService, pid, maxMs, intervalMs) {
|
|
27
|
+
const deadline = Date.now() + maxMs;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
30
|
+
if (!daemonService.isAlive(pid)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create the stop command.
|
|
38
|
+
*/
|
|
39
|
+
export function createStopCommand() {
|
|
40
|
+
return new Command('stop').description('Stop the running Shep web UI daemon').action(async () => {
|
|
41
|
+
const daemonService = container.resolve('IDaemonService');
|
|
42
|
+
const state = await daemonService.read();
|
|
43
|
+
// No daemon running or PID not alive
|
|
44
|
+
if (!state || !daemonService.isAlive(state.pid)) {
|
|
45
|
+
messages.info('No Shep daemon is running.');
|
|
46
|
+
await daemonService.delete();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { pid } = state;
|
|
50
|
+
// Validate PID is a positive finite integer before kill
|
|
51
|
+
if (!Number.isFinite(pid) || !Number.isInteger(pid) || pid <= 0) {
|
|
52
|
+
messages.info('No Shep daemon is running (invalid PID in state file).');
|
|
53
|
+
await daemonService.delete();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
messages.info(`Stopping Shep daemon (PID ${pid})...`);
|
|
58
|
+
// Send SIGTERM — request graceful shutdown
|
|
59
|
+
process.kill(pid, 'SIGTERM');
|
|
60
|
+
// Poll for up to 5s waiting for the process to exit
|
|
61
|
+
const died = await pollUntilDead(daemonService, pid, MAX_WAIT_MS, POLL_INTERVAL_MS);
|
|
62
|
+
if (!died) {
|
|
63
|
+
// Graceful shutdown timed out — force kill
|
|
64
|
+
messages.info('Daemon did not stop gracefully, sending SIGKILL...');
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 'SIGKILL');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Process may have exited between the check and the kill — ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
messages.success('Shep daemon stopped.');
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
// Always clean up daemon.json regardless of termination path
|
|
76
|
+
await daemonService.delete();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* shep [command] [options]
|
|
9
9
|
*
|
|
10
10
|
* Commands:
|
|
11
|
-
* shep
|
|
11
|
+
* shep Start the web UI daemon (or run onboarding on first run)
|
|
12
|
+
* shep start Start the web UI as a background daemon
|
|
13
|
+
* shep stop Stop the running web UI daemon
|
|
14
|
+
* shep status Show status and metrics of the running daemon
|
|
12
15
|
* shep version Display version information
|
|
13
|
-
* shep ui Start the web UI
|
|
16
|
+
* shep ui Start the web UI (foreground, interactive)
|
|
14
17
|
* shep run Run an AI agent workflow
|
|
15
18
|
* shep agent Manage and view agent runs
|
|
16
19
|
* shep feat Manage features through the SDLC lifecycle
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/presentation/cli/index.ts"],"names":[],"mappings":";AAEA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/presentation/cli/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,kBAAkB,CAAC"}
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* shep [command] [options]
|
|
9
9
|
*
|
|
10
10
|
* Commands:
|
|
11
|
-
* shep
|
|
11
|
+
* shep Start the web UI daemon (or run onboarding on first run)
|
|
12
|
+
* shep start Start the web UI as a background daemon
|
|
13
|
+
* shep stop Stop the running web UI daemon
|
|
14
|
+
* shep status Show status and metrics of the running daemon
|
|
12
15
|
* shep version Display version information
|
|
13
|
-
* shep ui Start the web UI
|
|
16
|
+
* shep ui Start the web UI (foreground, interactive)
|
|
14
17
|
* shep run Run an AI agent workflow
|
|
15
18
|
* shep agent Manage and view agent runs
|
|
16
19
|
* shep feat Manage features through the SDLC lifecycle
|
|
@@ -38,6 +41,12 @@ import { createIdeOpenCommand } from './commands/ide-open.command.js';
|
|
|
38
41
|
import { createInstallCommand } from './commands/install.command.js';
|
|
39
42
|
import { createUpgradeCommand } from './commands/upgrade.command.js';
|
|
40
43
|
import { messages } from './ui/index.js';
|
|
44
|
+
// Daemon lifecycle commands
|
|
45
|
+
import { createStartCommand } from './commands/start.command.js';
|
|
46
|
+
import { createStopCommand } from './commands/stop.command.js';
|
|
47
|
+
import { createStatusCommand } from './commands/status.command.js';
|
|
48
|
+
import { createServeCommand } from './commands/_serve.command.js';
|
|
49
|
+
import { startDaemon } from './commands/daemon/start-daemon.js';
|
|
41
50
|
// DI container and settings
|
|
42
51
|
import { initializeContainer, container } from '../../../packages/core/src/infrastructure/di/container.js';
|
|
43
52
|
import { InitializeSettingsUseCase } from '../../../packages/core/src/application/use-cases/settings/initialize-settings.use-case.js';
|
|
@@ -88,8 +97,13 @@ async function bootstrap() {
|
|
|
88
97
|
.name('shep')
|
|
89
98
|
.description(description)
|
|
90
99
|
.version(version, '-v, --version', 'Display version number')
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
// task-10: Default action starts the daemon (or shows already-running URL).
|
|
101
|
+
// The onboarding gate above (lines 82-89) ensures the wizard runs on first launch;
|
|
102
|
+
// after the gate, startDaemon() is the correct next step in both cases.
|
|
103
|
+
// Commander only fires this action when no subcommand matches, so `shep start` etc.
|
|
104
|
+
// are unaffected.
|
|
105
|
+
.action(async () => {
|
|
106
|
+
await startDaemon();
|
|
93
107
|
});
|
|
94
108
|
// Register commands
|
|
95
109
|
program.addCommand(createVersionCommand());
|
|
@@ -103,6 +117,11 @@ async function bootstrap() {
|
|
|
103
117
|
program.addCommand(createIdeOpenCommand());
|
|
104
118
|
program.addCommand(createInstallCommand());
|
|
105
119
|
program.addCommand(createUpgradeCommand());
|
|
120
|
+
// Daemon lifecycle commands (task-9)
|
|
121
|
+
program.addCommand(createStartCommand());
|
|
122
|
+
program.addCommand(createStopCommand());
|
|
123
|
+
program.addCommand(createStatusCommand());
|
|
124
|
+
program.addCommand(createServeCommand()); // hidden from --help
|
|
106
125
|
// Parse arguments (parseAsync needed for async command actions like init)
|
|
107
126
|
await program.parseAsync();
|
|
108
127
|
}
|