feat: add stack restart orchestration
This commit is contained in:
13
CLAUDE.md
13
CLAUDE.md
@@ -45,19 +45,21 @@ npm run dev # Dev mode with hot reload
|
|||||||
|
|
||||||
Service management (Linux):
|
Service management (Linux):
|
||||||
```bash
|
```bash
|
||||||
systemctl --user restart ejclaw ejclaw-codex # Restart both
|
npm run restart:stack # Restart the configured stack and verify it
|
||||||
|
systemctl --user restart ejclaw ejclaw-codex ejclaw-review
|
||||||
systemctl --user status ejclaw # Check status
|
systemctl --user status ejclaw # Check status
|
||||||
journalctl --user -u ejclaw -f # Follow logs
|
journalctl --user -u ejclaw -f # Follow logs
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy to server: `scp dist/*.js clone-ej@100.64.185.108:~/EJClaw/dist/`
|
Deploy to server: `scp dist/*.js clone-ej@100.64.185.108:~/EJClaw/dist/`
|
||||||
|
|
||||||
## Dual-Service Architecture
|
## Service Stack Architecture
|
||||||
|
|
||||||
- `ejclaw.service` — Claude Code bot (`@claude`), `SERVICE_ID=claude`, `SERVICE_AGENT_TYPE=claude-code`
|
- `ejclaw.service` — Claude Code bot (`@claude`), `SERVICE_ID=claude`, `SERVICE_AGENT_TYPE=claude-code`
|
||||||
- `ejclaw-codex.service` — Codex bot (`@codex`), `SERVICE_ID=codex`, `SERVICE_AGENT_TYPE=codex`
|
- `ejclaw-codex.service` — Codex bot (`@codex`), `SERVICE_ID=codex-main`, `SERVICE_AGENT_TYPE=codex`
|
||||||
- Both share the same codebase (`dist/index.js`), differentiated by env vars
|
- `ejclaw-review.service` — Codex reviewer bot (`@codex-review`), `SERVICE_ID=codex-review`, `SERVICE_AGENT_TYPE=codex`
|
||||||
- Unified dirs (`store/`, `groups/`, `data/` shared by both services):
|
- All services share the same codebase (`dist/index.js`), differentiated by env vars
|
||||||
|
- Unified dirs (`store/`, `groups/`, `data/` shared by all services):
|
||||||
- `router_state`: keys prefixed with `{SERVICE_ID}:` (e.g., `claude:last_timestamp`)
|
- `router_state`: keys prefixed with `{SERVICE_ID}:` (e.g., `claude:last_timestamp`)
|
||||||
- `sessions`: composite PK `(group_folder, agent_type)`
|
- `sessions`: composite PK `(group_folder, agent_type)`
|
||||||
- `registered_groups`: filtered by `agent_type` on load
|
- `registered_groups`: filtered by `agent_type` on load
|
||||||
@@ -72,6 +74,7 @@ Unified DB + directories (both services share `store/`, `groups/`, `data/`):
|
|||||||
| **DB** | `store/messages.db` (공유, WAL 모드) |
|
| **DB** | `store/messages.db` (공유, WAL 모드) |
|
||||||
| 서비스 로그 (Claude) | `journalctl --user -u ejclaw -f` 또는 `logs/ejclaw.log` |
|
| 서비스 로그 (Claude) | `journalctl --user -u ejclaw -f` 또는 `logs/ejclaw.log` |
|
||||||
| 서비스 로그 (Codex) | `journalctl --user -u ejclaw-codex -f` 또는 `logs/ejclaw-codex.log` |
|
| 서비스 로그 (Codex) | `journalctl --user -u ejclaw-codex -f` 또는 `logs/ejclaw-codex.log` |
|
||||||
|
| 서비스 로그 (Review) | `journalctl --user -u ejclaw-review -f` 또는 `logs/ejclaw-review.log` |
|
||||||
| 그룹별 로그 | `groups/{name}/logs/` (공유 채널은 양쪽 봇 로그가 같은 폴더) |
|
| 그룹별 로그 | `groups/{name}/logs/` (공유 채널은 양쪽 봇 로그가 같은 폴더) |
|
||||||
| Claude 세션 | `data/sessions/{name}/.claude/` |
|
| Claude 세션 | `data/sessions/{name}/.claude/` |
|
||||||
| Codex 세션 | `data/sessions/{name}/.codex/` |
|
| Codex 세션 | `data/sessions/{name}/.codex/` |
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -91,9 +91,12 @@ To run the Codex agent alongside Claude, create `.env.codex`:
|
|||||||
```bash
|
```bash
|
||||||
# .env.codex
|
# .env.codex
|
||||||
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex
|
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex
|
||||||
|
|
||||||
|
# .env.codex-review
|
||||||
|
DISCORD_BOT_TOKEN= # Separate Discord bot token for Codex Review
|
||||||
```
|
```
|
||||||
|
|
||||||
The setup step (`npm run setup -- --step service`) auto-detects `.env.codex` and installs `ejclaw-codex` alongside `ejclaw`. Additional Codex settings (`CODEX_MODEL`, `CODEX_EFFORT`, etc.) can be added to `.env.codex` or as `Environment=` lines in the systemd unit.
|
The setup step (`npm run setup -- --step service`) auto-detects `.env.codex` and `.env.codex-review`, then installs `ejclaw-codex` and `ejclaw-review` alongside `ejclaw`. Additional Codex settings (`CODEX_MODEL`, `CODEX_EFFORT`, etc.) can be added to `.env.codex`, `.env.codex-review`, or as `Environment=` lines in the systemd units.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
@@ -104,12 +107,14 @@ Token auto-refresh runs on the Claude service only, refreshing access tokens 30
|
|||||||
### Systemd Services (Linux)
|
### Systemd Services (Linux)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user enable ejclaw ejclaw-codex
|
npm run restart:stack
|
||||||
systemctl --user start ejclaw ejclaw-codex
|
systemctl --user enable ejclaw ejclaw-codex ejclaw-review
|
||||||
|
systemctl --user start ejclaw ejclaw-codex ejclaw-review
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
journalctl --user -u ejclaw -f
|
journalctl --user -u ejclaw -f
|
||||||
journalctl --user -u ejclaw-codex -f
|
journalctl --user -u ejclaw-codex -f
|
||||||
|
journalctl --user -u ejclaw-review -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"build:runners": "npm --prefix runners/agent-runner install && npm --prefix runners/agent-runner run build && npm --prefix runners/codex-runner install && npm --prefix runners/codex-runner run build",
|
"build:runners": "npm --prefix runners/agent-runner install && npm --prefix runners/agent-runner run build && npm --prefix runners/codex-runner install && npm --prefix runners/codex-runner run build",
|
||||||
|
"restart:stack": "bash scripts/restart-ejclaw-stack.sh",
|
||||||
"restart:hint": "tsx src/restart-context-cli.ts write",
|
"restart:hint": "tsx src/restart-context-cli.ts write",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
|
|||||||
8
scripts/restart-ejclaw-stack.sh
Executable file
8
scripts/restart-ejclaw-stack.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
cd "${PROJECT_ROOT}"
|
||||||
|
npx tsx setup/restart-stack.ts "$@"
|
||||||
69
setup/restart-stack.test.ts
Normal file
69
setup/restart-stack.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { restartStackServices } from './restart-stack.js';
|
||||||
|
|
||||||
|
describe('restartStackServices', () => {
|
||||||
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of tempRoots.splice(0)) {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restarts and verifies the configured three-service stack', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
||||||
|
tempRoots.push(tempRoot);
|
||||||
|
fs.writeFileSync(path.join(tempRoot, '.env.codex'), 'A=1\n');
|
||||||
|
fs.writeFileSync(path.join(tempRoot, '.env.codex-review'), 'B=1\n');
|
||||||
|
|
||||||
|
const execFileSyncImpl = vi.fn();
|
||||||
|
|
||||||
|
const services = restartStackServices(tempRoot, {
|
||||||
|
execFileSyncImpl,
|
||||||
|
runningAsRoot: false,
|
||||||
|
serviceManager: 'systemd',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(services).toEqual(['ejclaw', 'ejclaw-codex', 'ejclaw-review']);
|
||||||
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'systemctl',
|
||||||
|
['--user', 'restart', 'ejclaw', 'ejclaw-codex', 'ejclaw-review'],
|
||||||
|
{ stdio: 'ignore' },
|
||||||
|
);
|
||||||
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'systemctl',
|
||||||
|
['--user', 'is-active', '--quiet', 'ejclaw'],
|
||||||
|
{ stdio: 'ignore' },
|
||||||
|
);
|
||||||
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
'systemctl',
|
||||||
|
['--user', 'is-active', '--quiet', 'ejclaw-codex'],
|
||||||
|
{ stdio: 'ignore' },
|
||||||
|
);
|
||||||
|
expect(execFileSyncImpl).toHaveBeenNthCalledWith(
|
||||||
|
4,
|
||||||
|
'systemctl',
|
||||||
|
['--user', 'is-active', '--quiet', 'ejclaw-review'],
|
||||||
|
{ stdio: 'ignore' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-systemd environments', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
|
||||||
|
tempRoots.push(tempRoot);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
restartStackServices(tempRoot, {
|
||||||
|
serviceManager: 'nohup',
|
||||||
|
}),
|
||||||
|
).toThrow('restart:stack only supports Linux systemd services in this repo');
|
||||||
|
});
|
||||||
|
});
|
||||||
63
setup/restart-stack.ts
Normal file
63
setup/restart-stack.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { execFileSync } from 'child_process';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
|
import { getServiceManager, isRoot } from './platform.js';
|
||||||
|
import { getConfiguredServiceNames } from './service-defs.js';
|
||||||
|
|
||||||
|
type ServiceManager = ReturnType<typeof getServiceManager>;
|
||||||
|
|
||||||
|
interface RestartStackDeps {
|
||||||
|
execFileSyncImpl?: typeof execFileSync;
|
||||||
|
runningAsRoot?: boolean;
|
||||||
|
serviceManager?: ServiceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restartStackServices(
|
||||||
|
projectRoot: string,
|
||||||
|
deps: RestartStackDeps = {},
|
||||||
|
): string[] {
|
||||||
|
const serviceManager = deps.serviceManager ?? getServiceManager();
|
||||||
|
if (serviceManager !== 'systemd') {
|
||||||
|
throw new Error(
|
||||||
|
'restart:stack only supports Linux systemd services in this repo',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = getConfiguredServiceNames(projectRoot);
|
||||||
|
if (services.length === 0) {
|
||||||
|
throw new Error('No EJClaw services are configured in this project');
|
||||||
|
}
|
||||||
|
|
||||||
|
const execImpl = deps.execFileSyncImpl ?? execFileSync;
|
||||||
|
const systemctlArgs = deps.runningAsRoot ?? isRoot() ? [] : ['--user'];
|
||||||
|
|
||||||
|
execImpl('systemctl', [...systemctlArgs, 'restart', ...services], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
execImpl('systemctl', [...systemctlArgs, 'is-active', '--quiet', service], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function run(_args: string[]): Promise<void> {
|
||||||
|
const services = restartStackServices(process.cwd());
|
||||||
|
console.log(`Restarted and verified: ${services.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1]) {
|
||||||
|
const isMain =
|
||||||
|
import.meta.url === pathToFileURL(process.argv[1]).href ||
|
||||||
|
import.meta.url === pathToFileURL(process.argv[1]).toString();
|
||||||
|
if (isMain) {
|
||||||
|
run([]).catch((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
62
setup/service-defs.ts
Normal file
62
setup/service-defs.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export interface ServiceDef {
|
||||||
|
/** systemd unit name / nohup script name */
|
||||||
|
name: string;
|
||||||
|
/** launchd label */
|
||||||
|
launchdLabel: string;
|
||||||
|
/** Human-readable description for systemd/launchd */
|
||||||
|
description: string;
|
||||||
|
/** Log file prefix (e.g. "ejclaw" → logs/ejclaw.log) */
|
||||||
|
logName: string;
|
||||||
|
/** Absolute path to EnvironmentFile (systemd) — loaded before Environment= */
|
||||||
|
environmentFile?: string;
|
||||||
|
/** Extra Environment= lines for systemd / env dict entries for launchd */
|
||||||
|
extraEnv?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServiceDefs(projectRoot: string): ServiceDef[] {
|
||||||
|
const defs: ServiceDef[] = [
|
||||||
|
{
|
||||||
|
name: 'ejclaw',
|
||||||
|
launchdLabel: 'com.ejclaw',
|
||||||
|
description: 'EJClaw Personal Assistant (Claude Code)',
|
||||||
|
logName: 'ejclaw',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const codexEnvPath = path.join(projectRoot, '.env.codex');
|
||||||
|
if (fs.existsSync(codexEnvPath)) {
|
||||||
|
defs.push({
|
||||||
|
name: 'ejclaw-codex',
|
||||||
|
launchdLabel: 'com.ejclaw-codex',
|
||||||
|
description: 'EJClaw Codex Assistant',
|
||||||
|
logName: 'ejclaw-codex',
|
||||||
|
environmentFile: codexEnvPath,
|
||||||
|
extraEnv: {
|
||||||
|
ASSISTANT_NAME: 'codex',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewEnvPath = path.join(projectRoot, '.env.codex-review');
|
||||||
|
if (fs.existsSync(reviewEnvPath)) {
|
||||||
|
defs.push({
|
||||||
|
name: 'ejclaw-review',
|
||||||
|
launchdLabel: 'com.ejclaw-review',
|
||||||
|
description: 'EJClaw Codex Review Assistant',
|
||||||
|
logName: 'ejclaw-review',
|
||||||
|
environmentFile: reviewEnvPath,
|
||||||
|
extraEnv: {
|
||||||
|
ASSISTANT_NAME: 'codex',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return defs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfiguredServiceNames(projectRoot: string): string[] {
|
||||||
|
return getServiceDefs(projectRoot).map((def) => def.name);
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { getServiceDefs } from './service-defs.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for service configuration generation.
|
* Tests for service configuration generation.
|
||||||
@@ -174,3 +178,28 @@ echo $! > ${JSON.stringify(pidFile)}`;
|
|||||||
expect(wrapper).toContain('ejclaw.pid');
|
expect(wrapper).toContain('ejclaw.pid');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('service definitions', () => {
|
||||||
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of tempRoots.splice(0)) {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the review service when .env.codex-review exists', () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-stack-'));
|
||||||
|
tempRoots.push(tempRoot);
|
||||||
|
fs.writeFileSync(path.join(tempRoot, '.env.codex'), 'A=1\n');
|
||||||
|
fs.writeFileSync(path.join(tempRoot, '.env.codex-review'), 'B=1\n');
|
||||||
|
|
||||||
|
const defs = getServiceDefs(tempRoot);
|
||||||
|
|
||||||
|
expect(defs.map((def) => def.name)).toEqual([
|
||||||
|
'ejclaw',
|
||||||
|
'ejclaw-codex',
|
||||||
|
'ejclaw-review',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* Step: service — Generate and load service manager config.
|
* Step: service — Generate and load service manager config.
|
||||||
* Replaces 08-setup-service.sh
|
* Replaces 08-setup-service.sh
|
||||||
*
|
*
|
||||||
* Supports dual-service architecture:
|
* Supports the EJClaw service stack:
|
||||||
* - ejclaw (Claude Code) — always installed
|
* - ejclaw (Claude Code) — always installed
|
||||||
* - ejclaw-codex (Codex) — installed when .env.codex exists
|
* - ejclaw-codex (Codex) — installed when .env.codex exists
|
||||||
|
* - ejclaw-review (Codex Review) — installed when .env.codex-review exists
|
||||||
*/
|
*/
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -16,61 +17,11 @@ import {
|
|||||||
getPlatform,
|
getPlatform,
|
||||||
getNodePath,
|
getNodePath,
|
||||||
getServiceManager,
|
getServiceManager,
|
||||||
hasSystemd,
|
|
||||||
isRoot,
|
isRoot,
|
||||||
isWSL,
|
|
||||||
} from './platform.js';
|
} from './platform.js';
|
||||||
|
import { getServiceDefs, type ServiceDef } from './service-defs.js';
|
||||||
import { emitStatus } from './status.js';
|
import { emitStatus } from './status.js';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Service definition */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
interface ServiceDef {
|
|
||||||
/** systemd unit name / nohup script name */
|
|
||||||
name: string;
|
|
||||||
/** launchd label */
|
|
||||||
launchdLabel: string;
|
|
||||||
/** Human-readable description for systemd/launchd */
|
|
||||||
description: string;
|
|
||||||
/** Log file prefix (e.g. "ejclaw" → logs/ejclaw.log) */
|
|
||||||
logName: string;
|
|
||||||
/** Absolute path to EnvironmentFile (systemd) — loaded before Environment= */
|
|
||||||
environmentFile?: string;
|
|
||||||
/** Extra Environment= lines for systemd / env dict entries for launchd */
|
|
||||||
extraEnv?: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServiceDefs(projectRoot: string): ServiceDef[] {
|
|
||||||
const defs: ServiceDef[] = [
|
|
||||||
{
|
|
||||||
name: 'ejclaw',
|
|
||||||
launchdLabel: 'com.ejclaw',
|
|
||||||
description: 'EJClaw Personal Assistant (Claude Code)',
|
|
||||||
logName: 'ejclaw',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const codexEnvPath = path.join(projectRoot, '.env.codex');
|
|
||||||
if (fs.existsSync(codexEnvPath)) {
|
|
||||||
defs.push({
|
|
||||||
name: 'ejclaw-codex',
|
|
||||||
launchdLabel: 'com.ejclaw-codex',
|
|
||||||
description: 'EJClaw Codex Assistant',
|
|
||||||
logName: 'ejclaw-codex',
|
|
||||||
environmentFile: codexEnvPath,
|
|
||||||
extraEnv: {
|
|
||||||
ASSISTANT_NAME: 'codex',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
logger.info(
|
|
||||||
'Detected .env.codex — will also install ejclaw-codex service',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return defs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Entry point */
|
/* Entry point */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -107,6 +58,16 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
||||||
|
|
||||||
const serviceDefs = getServiceDefs(projectRoot);
|
const serviceDefs = getServiceDefs(projectRoot);
|
||||||
|
if (serviceDefs.some((def) => def.name === 'ejclaw-codex')) {
|
||||||
|
logger.info(
|
||||||
|
'Detected .env.codex — will also install ejclaw-codex service',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serviceDefs.some((def) => def.name === 'ejclaw-review')) {
|
||||||
|
logger.info(
|
||||||
|
'Detected .env.codex-review — will also install ejclaw-review service',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (platform === 'macos') {
|
if (platform === 'macos') {
|
||||||
for (const def of serviceDefs) {
|
for (const def of serviceDefs) {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* Step: verify — End-to-end health check of the full installation.
|
* Step: verify — End-to-end health check of the full installation.
|
||||||
* Replaces 09-verify.sh
|
* Replaces 09-verify.sh
|
||||||
*
|
*
|
||||||
* Supports dual-service architecture:
|
* Supports the EJClaw service stack:
|
||||||
* - ejclaw (Claude Code) — always checked
|
* - ejclaw (Claude Code) — always checked
|
||||||
* - ejclaw-codex (Codex) — checked when .env.codex exists
|
* - ejclaw-codex (Codex) — checked when .env.codex exists
|
||||||
|
* - ejclaw-review (Codex Review) — checked when .env.codex-review exists
|
||||||
*
|
*
|
||||||
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
|
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +19,7 @@ import { STORE_DIR } from '../src/config.js';
|
|||||||
import { readEnvFile } from '../src/env.js';
|
import { readEnvFile } from '../src/env.js';
|
||||||
import { logger } from '../src/logger.js';
|
import { logger } from '../src/logger.js';
|
||||||
import { getPlatform, getServiceManager, isRoot } from './platform.js';
|
import { getPlatform, getServiceManager, isRoot } from './platform.js';
|
||||||
|
import { getServiceDefs } from './service-defs.js';
|
||||||
import { emitStatus } from './status.js';
|
import { emitStatus } from './status.js';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -113,28 +115,11 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
logger.info('Starting verification');
|
logger.info('Starting verification');
|
||||||
|
|
||||||
// 1. Check service statuses
|
// 1. Check service statuses
|
||||||
const services: ServiceCheck[] = [];
|
const serviceDefs = getServiceDefs(projectRoot);
|
||||||
|
const services: ServiceCheck[] = serviceDefs.map((def) => ({
|
||||||
// Primary service (always checked)
|
name: def.name,
|
||||||
services.push({
|
status: checkService(projectRoot, mgr, def.name, def.launchdLabel),
|
||||||
name: 'ejclaw',
|
}));
|
||||||
status: checkService(projectRoot, mgr, 'ejclaw', 'com.ejclaw'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Codex service (checked when .env.codex exists)
|
|
||||||
const codexEnvPath = path.join(projectRoot, '.env.codex');
|
|
||||||
const codexConfigured = fs.existsSync(codexEnvPath);
|
|
||||||
if (codexConfigured) {
|
|
||||||
services.push({
|
|
||||||
name: 'ejclaw-codex',
|
|
||||||
status: checkService(
|
|
||||||
projectRoot,
|
|
||||||
mgr,
|
|
||||||
'ejclaw-codex',
|
|
||||||
'com.ejclaw-codex',
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const svc of services) {
|
for (const svc of services) {
|
||||||
logger.info({ service: svc.name, status: svc.status }, 'Service status');
|
logger.info({ service: svc.name, status: svc.status }, 'Service status');
|
||||||
@@ -195,12 +180,18 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine overall status
|
// Determine overall status
|
||||||
const primaryRunning = services[0].status === 'running';
|
const allConfiguredServicesRunning = services.every(
|
||||||
const codexOk = !codexConfigured || services[1]?.status === 'running';
|
(service) => service.status === 'running',
|
||||||
|
);
|
||||||
|
const codexConfigured = serviceDefs.some(
|
||||||
|
(service) => service.name === 'ejclaw-codex',
|
||||||
|
);
|
||||||
|
const reviewConfigured = serviceDefs.some(
|
||||||
|
(service) => service.name === 'ejclaw-review',
|
||||||
|
);
|
||||||
|
|
||||||
const status =
|
const status =
|
||||||
primaryRunning &&
|
allConfiguredServicesRunning &&
|
||||||
codexOk &&
|
|
||||||
credentials !== 'missing' &&
|
credentials !== 'missing' &&
|
||||||
anyChannelConfigured &&
|
anyChannelConfigured &&
|
||||||
registeredGroups > 0
|
registeredGroups > 0
|
||||||
@@ -225,6 +216,7 @@ export async function run(_args: string[]): Promise<void> {
|
|||||||
REGISTERED_GROUPS: registeredGroups,
|
REGISTERED_GROUPS: registeredGroups,
|
||||||
GROUPS_BY_AGENT: JSON.stringify(groupsByAgent),
|
GROUPS_BY_AGENT: JSON.stringify(groupsByAgent),
|
||||||
CODEX_CONFIGURED: codexConfigured,
|
CODEX_CONFIGURED: codexConfigured,
|
||||||
|
REVIEW_CONFIGURED: reviewConfigured,
|
||||||
STATUS: status,
|
STATUS: status,
|
||||||
LOG: 'logs/setup.log',
|
LOG: 'logs/setup.log',
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user