@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
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) => {
|
|
@@ -80,9 +80,9 @@ export function fromDatabase(row) {
|
|
|
80
80
|
relatedArtifacts: JSON.parse(row.related_artifacts),
|
|
81
81
|
createdAt: new Date(row.created_at),
|
|
82
82
|
updatedAt: new Date(row.updated_at),
|
|
83
|
-
...(row.plan
|
|
84
|
-
...(row.agent_run_id
|
|
85
|
-
...(row.spec_path
|
|
83
|
+
...(row.plan != null && { plan: JSON.parse(row.plan) }),
|
|
84
|
+
...(row.agent_run_id != null && { agentRunId: row.agent_run_id }),
|
|
85
|
+
...(row.spec_path != null && { specPath: row.spec_path }),
|
|
86
86
|
// Assemble workflow flags from flat columns
|
|
87
87
|
push: row.push === 1,
|
|
88
88
|
openPr: row.open_pr === 1,
|
|
@@ -91,22 +91,22 @@ export function fromDatabase(row) {
|
|
|
91
91
|
allowPlan: row.allow_plan === 1,
|
|
92
92
|
allowMerge: row.allow_merge === 1,
|
|
93
93
|
},
|
|
94
|
-
...(row.worktree_path
|
|
94
|
+
...(row.worktree_path != null && { worktreePath: row.worktree_path }),
|
|
95
95
|
// Repository reference
|
|
96
|
-
...(row.repository_id
|
|
96
|
+
...(row.repository_id != null && { repositoryId: row.repository_id }),
|
|
97
97
|
// Assemble pr from flat columns (only when pr_url exists)
|
|
98
|
-
...(row.pr_url
|
|
98
|
+
...(row.pr_url != null && {
|
|
99
99
|
pr: {
|
|
100
100
|
url: row.pr_url,
|
|
101
101
|
number: row.pr_number,
|
|
102
102
|
status: row.pr_status,
|
|
103
|
-
...(row.commit_hash
|
|
104
|
-
...(row.ci_status
|
|
105
|
-
...(row.ci_fix_attempts
|
|
106
|
-
...(row.ci_fix_history
|
|
103
|
+
...(row.commit_hash != null && { commitHash: row.commit_hash }),
|
|
104
|
+
...(row.ci_status != null && { ciStatus: row.ci_status }),
|
|
105
|
+
...(row.ci_fix_attempts != null && { ciFixAttempts: row.ci_fix_attempts }),
|
|
106
|
+
...(row.ci_fix_history != null && { ciFixHistory: JSON.parse(row.ci_fix_history) }),
|
|
107
107
|
},
|
|
108
108
|
}),
|
|
109
109
|
// Feature dependency
|
|
110
|
-
...(row.parent_id
|
|
110
|
+
...(row.parent_id != null && { parentId: row.parent_id }),
|
|
111
111
|
};
|
|
112
112
|
}
|
|
@@ -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"}
|