@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.
Files changed (91) 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/persistence/sqlite/mappers/feature.mapper.js +11 -11
  10. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts +1 -0
  11. package/dist/packages/core/src/infrastructure/services/browser-opener.service.d.ts.map +1 -1
  12. package/dist/packages/core/src/infrastructure/services/browser-opener.service.js +4 -0
  13. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts +16 -0
  14. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.d.ts.map +1 -0
  15. package/dist/packages/core/src/infrastructure/services/daemon/daemon-pid.service.js +72 -0
  16. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts +7 -0
  17. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.d.ts.map +1 -1
  18. package/dist/packages/core/src/infrastructure/services/filesystem/shep-directory.service.js +9 -0
  19. package/dist/src/presentation/cli/commands/_serve.command.d.ts +32 -0
  20. package/dist/src/presentation/cli/commands/_serve.command.d.ts.map +1 -0
  21. package/dist/src/presentation/cli/commands/_serve.command.js +91 -0
  22. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts +27 -0
  23. package/dist/src/presentation/cli/commands/daemon/start-daemon.d.ts.map +1 -0
  24. package/dist/src/presentation/cli/commands/daemon/start-daemon.js +135 -0
  25. package/dist/src/presentation/cli/commands/start.command.d.ts +15 -0
  26. package/dist/src/presentation/cli/commands/start.command.d.ts.map +1 -0
  27. package/dist/src/presentation/cli/commands/start.command.js +33 -0
  28. package/dist/src/presentation/cli/commands/status.command.d.ts +20 -0
  29. package/dist/src/presentation/cli/commands/status.command.d.ts.map +1 -0
  30. package/dist/src/presentation/cli/commands/status.command.js +119 -0
  31. package/dist/src/presentation/cli/commands/stop.command.d.ts +22 -0
  32. package/dist/src/presentation/cli/commands/stop.command.d.ts.map +1 -0
  33. package/dist/src/presentation/cli/commands/stop.command.js +79 -0
  34. package/dist/src/presentation/cli/index.d.ts +5 -2
  35. package/dist/src/presentation/cli/index.d.ts.map +1 -1
  36. package/dist/src/presentation/cli/index.js +23 -4
  37. package/dist/tsconfig.build.tsbuildinfo +1 -1
  38. package/package.json +1 -1
  39. package/web/.next/BUILD_ID +1 -1
  40. package/web/.next/build-manifest.json +2 -2
  41. package/web/.next/cache/.previewinfo +1 -1
  42. package/web/.next/cache/.rscinfo +1 -1
  43. package/web/.next/cache/.tsbuildinfo +1 -1
  44. package/web/.next/cache/config.json +3 -3
  45. package/web/.next/fallback-build-manifest.json +2 -2
  46. package/web/.next/prerender-manifest.json +3 -3
  47. package/web/.next/required-server-files.js +1 -1
  48. package/web/.next/required-server-files.json +1 -1
  49. package/web/.next/server/app/_global-error.html +2 -2
  50. package/web/.next/server/app/_global-error.rsc +1 -1
  51. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  52. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  53. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/web/.next/server/app/page/server-reference-manifest.json +14 -14
  57. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  58. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  59. package/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js.map +1 -1
  60. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  61. package/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js.map +1 -1
  62. package/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
  63. package/web/.next/server/pages/500.html +2 -2
  64. package/web/.next/server/server-reference-manifest.js +1 -1
  65. package/web/.next/server/server-reference-manifest.json +15 -15
  66. package/web/.next/standalone/src/presentation/web/.next/BUILD_ID +1 -1
  67. package/web/.next/standalone/src/presentation/web/.next/build-manifest.json +2 -2
  68. package/web/.next/standalone/src/presentation/web/.next/prerender-manifest.json +3 -3
  69. package/web/.next/standalone/src/presentation/web/.next/required-server-files.json +1 -1
  70. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.html +2 -2
  71. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.rsc +1 -1
  72. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  73. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  74. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  75. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  76. package/web/.next/standalone/src/presentation/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  77. package/web/.next/standalone/src/presentation/web/.next/server/app/page/server-reference-manifest.json +14 -14
  78. package/web/.next/standalone/src/presentation/web/.next/server/app/page_client-reference-manifest.js +1 -1
  79. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__03a0dba7._.js +2 -2
  80. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__2d1a8e37._.js +2 -2
  81. package/web/.next/standalone/src/presentation/web/.next/server/chunks/ssr/[root-of-the-server]__6e8b5181._.js +1 -1
  82. package/web/.next/standalone/src/presentation/web/.next/server/pages/500.html +2 -2
  83. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.js +1 -1
  84. package/web/.next/standalone/src/presentation/web/.next/server/server-reference-manifest.json +15 -15
  85. package/web/.next/standalone/src/presentation/web/server.js +1 -1
  86. package/web/.next/static/chunks/{702384d364482e4b.js → 7d66f03293ffca30.js} +2 -2
  87. package/web/.next/trace +1 -1
  88. package/web/.next/trace-build +1 -1
  89. /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → nEAJoHmU4elsBOt8hrgha}/_buildManifest.js +0 -0
  90. /package/web/.next/static/{RwzxYU9D2fPc_4j5RYu0v → nEAJoHmU4elsBOt8hrgha}/_clientMiddlewareManifest.json +0 -0
  91. /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 Show help
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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,kBAAkB,CAAC"}
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 Show help
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
- .action(() => {
92
- program.outputHelp();
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
  }