instar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/.claude/skills/setup-wizard/skill.md +343 -0
- package/.github/workflows/ci.yml +78 -0
- package/CLAUDE.md +82 -0
- package/README.md +194 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +141 -0
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +568 -0
- package/dist/commands/job.d.ts +20 -0
- package/dist/commands/job.js +84 -0
- package/dist/commands/server.d.ts +19 -0
- package/dist/commands/server.js +273 -0
- package/dist/commands/setup.d.ts +24 -0
- package/dist/commands/setup.js +865 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.js +114 -0
- package/dist/commands/user.d.ts +17 -0
- package/dist/commands/user.js +53 -0
- package/dist/core/Config.d.ts +16 -0
- package/dist/core/Config.js +144 -0
- package/dist/core/Prerequisites.d.ts +28 -0
- package/dist/core/Prerequisites.js +159 -0
- package/dist/core/RelationshipManager.d.ts +73 -0
- package/dist/core/RelationshipManager.js +318 -0
- package/dist/core/SessionManager.d.ts +89 -0
- package/dist/core/SessionManager.js +326 -0
- package/dist/core/StateManager.d.ts +28 -0
- package/dist/core/StateManager.js +96 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/types.js +8 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -0
- package/dist/messaging/TelegramAdapter.d.ts +73 -0
- package/dist/messaging/TelegramAdapter.js +288 -0
- package/dist/monitoring/HealthChecker.d.ts +38 -0
- package/dist/monitoring/HealthChecker.js +148 -0
- package/dist/scaffold/bootstrap.d.ts +21 -0
- package/dist/scaffold/bootstrap.js +110 -0
- package/dist/scaffold/templates.d.ts +34 -0
- package/dist/scaffold/templates.js +187 -0
- package/dist/scheduler/JobLoader.d.ts +18 -0
- package/dist/scheduler/JobLoader.js +70 -0
- package/dist/scheduler/JobScheduler.d.ts +111 -0
- package/dist/scheduler/JobScheduler.js +402 -0
- package/dist/server/AgentServer.d.ts +40 -0
- package/dist/server/AgentServer.js +73 -0
- package/dist/server/middleware.d.ts +12 -0
- package/dist/server/middleware.js +50 -0
- package/dist/server/routes.d.ts +25 -0
- package/dist/server/routes.js +224 -0
- package/dist/users/UserManager.d.ts +45 -0
- package/dist/users/UserManager.js +113 -0
- package/docs/dawn-audit-report.md +412 -0
- package/docs/positioning-vs-openclaw.md +246 -0
- package/package.json +52 -0
- package/src/cli.ts +169 -0
- package/src/commands/init.ts +654 -0
- package/src/commands/job.ts +110 -0
- package/src/commands/server.ts +325 -0
- package/src/commands/setup.ts +958 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/user.ts +71 -0
- package/src/core/Config.ts +161 -0
- package/src/core/Prerequisites.ts +187 -0
- package/src/core/RelationshipManager.ts +366 -0
- package/src/core/SessionManager.ts +385 -0
- package/src/core/StateManager.ts +121 -0
- package/src/core/types.ts +320 -0
- package/src/index.ts +58 -0
- package/src/messaging/TelegramAdapter.ts +365 -0
- package/src/monitoring/HealthChecker.ts +172 -0
- package/src/scaffold/bootstrap.ts +122 -0
- package/src/scaffold/templates.ts +204 -0
- package/src/scheduler/JobLoader.ts +85 -0
- package/src/scheduler/JobScheduler.ts +476 -0
- package/src/server/AgentServer.ts +93 -0
- package/src/server/middleware.ts +58 -0
- package/src/server/routes.ts +278 -0
- package/src/templates/default-jobs.json +47 -0
- package/src/templates/hooks/compaction-recovery.sh +23 -0
- package/src/templates/hooks/dangerous-command-guard.sh +35 -0
- package/src/templates/hooks/grounding-before-messaging.sh +22 -0
- package/src/templates/hooks/session-start.sh +37 -0
- package/src/templates/hooks/settings-template.json +45 -0
- package/src/templates/scripts/health-watchdog.sh +63 -0
- package/src/templates/scripts/telegram-reply.sh +54 -0
- package/src/users/UserManager.ts +129 -0
- package/tests/e2e/lifecycle.test.ts +376 -0
- package/tests/fixtures/test-repo/CLAUDE.md +3 -0
- package/tests/fixtures/test-repo/README.md +1 -0
- package/tests/helpers/setup.ts +209 -0
- package/tests/integration/fresh-install.test.ts +218 -0
- package/tests/integration/scheduler-basic.test.ts +109 -0
- package/tests/integration/server-full.test.ts +284 -0
- package/tests/integration/session-lifecycle.test.ts +181 -0
- package/tests/unit/Config.test.ts +22 -0
- package/tests/unit/HealthChecker.test.ts +168 -0
- package/tests/unit/JobLoader.test.ts +151 -0
- package/tests/unit/JobScheduler.test.ts +267 -0
- package/tests/unit/Prerequisites.test.ts +59 -0
- package/tests/unit/RelationshipManager.test.ts +345 -0
- package/tests/unit/StateManager.test.ts +143 -0
- package/tests/unit/TelegramAdapter.test.ts +165 -0
- package/tests/unit/UserManager.test.ts +131 -0
- package/tests/unit/bootstrap.test.ts +28 -0
- package/tests/unit/commands.test.ts +138 -0
- package/tests/unit/middleware.test.ts +92 -0
- package/tests/unit/relationship-routes.test.ts +131 -0
- package/tests/unit/scaffold-templates.test.ts +132 -0
- package/tests/unit/server.test.ts +163 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.e2e.config.ts +9 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `instar status` — Show agent infrastructure status.
|
|
3
|
+
*
|
|
4
|
+
* Checks for: config, tmux, server, sessions, scheduler.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { loadConfig, detectTmuxPath } from '../core/Config.js';
|
|
10
|
+
import { StateManager } from '../core/StateManager.js';
|
|
11
|
+
|
|
12
|
+
interface StatusOptions {
|
|
13
|
+
dir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function showStatus(options: StatusOptions): Promise<void> {
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = loadConfig(options.dir);
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
console.log(pc.red(`Not initialized: ${err.message}`));
|
|
22
|
+
console.log(`Run ${pc.cyan('instar init')} first.`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(pc.bold(`\nInstar Status: ${pc.cyan(config.projectName)}`));
|
|
27
|
+
console.log(` Project: ${config.projectDir}`);
|
|
28
|
+
console.log(` State: ${config.stateDir}`);
|
|
29
|
+
console.log();
|
|
30
|
+
|
|
31
|
+
// Server status
|
|
32
|
+
const serverSessionName = `${config.projectName}-server`;
|
|
33
|
+
const tmuxPath = detectTmuxPath();
|
|
34
|
+
let serverRunning = false;
|
|
35
|
+
|
|
36
|
+
if (tmuxPath) {
|
|
37
|
+
try {
|
|
38
|
+
execSync(`${tmuxPath} has-session -t '=${serverSessionName}' 2>/dev/null`);
|
|
39
|
+
serverRunning = true;
|
|
40
|
+
} catch {
|
|
41
|
+
// not running
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(pc.bold(' Server:'));
|
|
46
|
+
if (serverRunning) {
|
|
47
|
+
console.log(` ${pc.green('●')} Running (tmux: ${serverSessionName}, port: ${config.port})`);
|
|
48
|
+
|
|
49
|
+
// Try to hit health endpoint
|
|
50
|
+
try {
|
|
51
|
+
const resp = execSync(`curl -s http://localhost:${config.port}/health`, { encoding: 'utf-8', timeout: 3000 });
|
|
52
|
+
const health = JSON.parse(resp);
|
|
53
|
+
console.log(` Uptime: ${health.uptimeHuman}`);
|
|
54
|
+
} catch {
|
|
55
|
+
console.log(` ${pc.yellow('Could not reach health endpoint')}`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(` ${pc.red('●')} Not running`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Sessions
|
|
62
|
+
const state = new StateManager(config.stateDir);
|
|
63
|
+
const runningSessions = state.listSessions({ status: 'running' });
|
|
64
|
+
const allSessions = state.listSessions();
|
|
65
|
+
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(pc.bold(' Sessions:'));
|
|
68
|
+
console.log(` Running: ${runningSessions.length} / ${config.sessions.maxSessions} max`);
|
|
69
|
+
console.log(` Total: ${allSessions.length}`);
|
|
70
|
+
|
|
71
|
+
if (runningSessions.length > 0) {
|
|
72
|
+
for (const s of runningSessions) {
|
|
73
|
+
const age = timeSince(new Date(s.startedAt));
|
|
74
|
+
console.log(` ${pc.green('●')} ${s.name} (${age}${s.jobSlug ? `, job: ${s.jobSlug}` : ''})`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Scheduler
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(pc.bold(' Scheduler:'));
|
|
81
|
+
console.log(` Enabled: ${config.scheduler.enabled ? pc.green('yes') : pc.dim('no')}`);
|
|
82
|
+
|
|
83
|
+
// Jobs
|
|
84
|
+
try {
|
|
85
|
+
const { loadJobs } = await import('../scheduler/JobLoader.js');
|
|
86
|
+
const jobs = loadJobs(config.scheduler.jobsFile);
|
|
87
|
+
const enabled = jobs.filter(j => j.enabled);
|
|
88
|
+
console.log(` Jobs: ${enabled.length} enabled / ${jobs.length} total`);
|
|
89
|
+
|
|
90
|
+
if (jobs.length > 0) {
|
|
91
|
+
for (const job of jobs) {
|
|
92
|
+
const jobState = state.getJobState(job.slug);
|
|
93
|
+
const icon = job.enabled ? pc.green('●') : pc.dim('○');
|
|
94
|
+
const lastRun = jobState?.lastRun ? timeSince(new Date(jobState.lastRun)) + ' ago' : 'never';
|
|
95
|
+
console.log(` ${icon} ${job.slug} [${job.priority}] — last: ${lastRun}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
console.log(` ${pc.dim('No jobs configured')}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Recent activity
|
|
103
|
+
const recentEvents = state.queryEvents({ limit: 5 });
|
|
104
|
+
if (recentEvents.length > 0) {
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(pc.bold(' Recent Activity:'));
|
|
107
|
+
for (const event of recentEvents) {
|
|
108
|
+
const age = timeSince(new Date(event.timestamp));
|
|
109
|
+
console.log(` ${pc.dim(age + ' ago')} ${event.type}: ${event.summary}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function timeSince(date: Date): string {
|
|
117
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
118
|
+
if (seconds < 60) return `${seconds}s`;
|
|
119
|
+
const minutes = Math.floor(seconds / 60);
|
|
120
|
+
if (minutes < 60) return `${minutes}m`;
|
|
121
|
+
const hours = Math.floor(minutes / 60);
|
|
122
|
+
if (hours < 24) return `${hours}h`;
|
|
123
|
+
const days = Math.floor(hours / 24);
|
|
124
|
+
return `${days}d`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `instar user add|list` — Manage user profiles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { loadConfig, ensureStateDir } from '../core/Config.js';
|
|
7
|
+
import { UserManager } from '../users/UserManager.js';
|
|
8
|
+
import type { UserProfile, UserChannel } from '../core/types.js';
|
|
9
|
+
|
|
10
|
+
interface UserAddOptions {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
telegram?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
slack?: string;
|
|
16
|
+
permissions?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function addUser(options: UserAddOptions): Promise<void> {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
ensureStateDir(config.stateDir);
|
|
22
|
+
const userManager = new UserManager(config.stateDir);
|
|
23
|
+
|
|
24
|
+
const channels: UserChannel[] = [];
|
|
25
|
+
if (options.telegram) {
|
|
26
|
+
channels.push({ type: 'telegram', identifier: options.telegram });
|
|
27
|
+
}
|
|
28
|
+
if (options.email) {
|
|
29
|
+
channels.push({ type: 'email', identifier: options.email });
|
|
30
|
+
}
|
|
31
|
+
if (options.slack) {
|
|
32
|
+
channels.push({ type: 'slack', identifier: options.slack });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const profile: UserProfile = {
|
|
36
|
+
id: options.id,
|
|
37
|
+
name: options.name,
|
|
38
|
+
channels,
|
|
39
|
+
permissions: options.permissions || ['user'],
|
|
40
|
+
preferences: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
userManager.upsertUser(profile);
|
|
44
|
+
|
|
45
|
+
console.log(pc.green(`User "${options.name}" (${options.id}) added.`));
|
|
46
|
+
if (channels.length > 0) {
|
|
47
|
+
console.log(` Channels: ${channels.map(c => `${c.type}:${c.identifier}`).join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
console.log(` Permissions: ${profile.permissions.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function listUsers(_options: { dir?: string }): Promise<void> {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
const userManager = new UserManager(config.stateDir);
|
|
55
|
+
const users = userManager.listUsers();
|
|
56
|
+
|
|
57
|
+
if (users.length === 0) {
|
|
58
|
+
console.log(pc.dim('No users configured.'));
|
|
59
|
+
console.log(`Add one: ${pc.cyan('instar user add --id justin --name Justin')}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(pc.bold(`Users (${users.length}):\n`));
|
|
64
|
+
for (const user of users) {
|
|
65
|
+
console.log(` ${pc.bold(user.name)} (${pc.dim(user.id)})`);
|
|
66
|
+
if (user.channels.length > 0) {
|
|
67
|
+
console.log(` Channels: ${user.channels.map(c => `${c.type}:${c.identifier}`).join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
console.log(` Permissions: ${user.permissions.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detection and configuration management.
|
|
3
|
+
*
|
|
4
|
+
* Finds tmux, Claude CLI, and project structure automatically.
|
|
5
|
+
* Adapted from dawn-server's config.ts — the battle-tested version.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import type { AgentKitConfig, SessionManagerConfig, JobSchedulerConfig } from './types.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PORT = 4040;
|
|
14
|
+
const DEFAULT_MAX_SESSIONS = 3;
|
|
15
|
+
const DEFAULT_MAX_PARALLEL_JOBS = 2;
|
|
16
|
+
|
|
17
|
+
export function detectTmuxPath(): string | null {
|
|
18
|
+
const candidates = [
|
|
19
|
+
'/opt/homebrew/bin/tmux', // macOS ARM (Homebrew)
|
|
20
|
+
'/usr/local/bin/tmux', // macOS Intel / Linux
|
|
21
|
+
'/usr/bin/tmux', // Linux system
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fallback: check PATH
|
|
29
|
+
try {
|
|
30
|
+
const result = execSync('which tmux', { encoding: 'utf-8' }).trim();
|
|
31
|
+
if (result && fs.existsSync(result)) return result;
|
|
32
|
+
} catch {
|
|
33
|
+
// tmux not found
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectClaudePath(): string | null {
|
|
40
|
+
const candidates = [
|
|
41
|
+
path.join(process.env.HOME || '', '.claude', 'local', 'claude'),
|
|
42
|
+
'/usr/local/bin/claude',
|
|
43
|
+
'/opt/homebrew/bin/claude',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fallback: check PATH
|
|
51
|
+
try {
|
|
52
|
+
const result = execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
53
|
+
if (result && fs.existsSync(result)) return result;
|
|
54
|
+
} catch {
|
|
55
|
+
// claude not found
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function detectProjectDir(startDir?: string): string {
|
|
62
|
+
let dir = startDir || process.cwd();
|
|
63
|
+
|
|
64
|
+
// Walk up to find a directory with CLAUDE.md or .git
|
|
65
|
+
while (dir !== path.dirname(dir)) {
|
|
66
|
+
if (fs.existsSync(path.join(dir, 'CLAUDE.md')) || fs.existsSync(path.join(dir, '.git'))) {
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
dir = path.dirname(dir);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return process.cwd();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadConfig(projectDir?: string): AgentKitConfig {
|
|
76
|
+
const resolvedProjectDir = projectDir || detectProjectDir();
|
|
77
|
+
const configPath = path.join(resolvedProjectDir, '.instar', 'config.json');
|
|
78
|
+
const stateDir = path.join(resolvedProjectDir, '.instar');
|
|
79
|
+
|
|
80
|
+
// Load config file if it exists
|
|
81
|
+
let fileConfig: Partial<AgentKitConfig> = {};
|
|
82
|
+
if (fs.existsSync(configPath)) {
|
|
83
|
+
fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tmuxPath = detectTmuxPath();
|
|
87
|
+
const claudePath = detectClaudePath();
|
|
88
|
+
|
|
89
|
+
if (!tmuxPath) {
|
|
90
|
+
throw new Error('tmux not found. Install with: brew install tmux (macOS) or apt install tmux (Linux)');
|
|
91
|
+
}
|
|
92
|
+
if (!claudePath) {
|
|
93
|
+
throw new Error('Claude CLI not found. Install from: https://docs.anthropic.com/en/docs/claude-code');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectName = fileConfig.projectName || path.basename(resolvedProjectDir);
|
|
97
|
+
|
|
98
|
+
const sessions: SessionManagerConfig = {
|
|
99
|
+
tmuxPath,
|
|
100
|
+
claudePath,
|
|
101
|
+
projectDir: resolvedProjectDir,
|
|
102
|
+
maxSessions: fileConfig.sessions?.maxSessions || DEFAULT_MAX_SESSIONS,
|
|
103
|
+
protectedSessions: fileConfig.sessions?.protectedSessions || [`${projectName}-server`],
|
|
104
|
+
completionPatterns: fileConfig.sessions?.completionPatterns || [
|
|
105
|
+
'has been automatically paused',
|
|
106
|
+
'Session ended',
|
|
107
|
+
'Interrupted by user',
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const scheduler: JobSchedulerConfig = {
|
|
112
|
+
jobsFile: fileConfig.scheduler?.jobsFile || path.join(stateDir, 'jobs.json'),
|
|
113
|
+
enabled: fileConfig.scheduler?.enabled ?? false,
|
|
114
|
+
maxParallelJobs: fileConfig.scheduler?.maxParallelJobs ?? DEFAULT_MAX_PARALLEL_JOBS,
|
|
115
|
+
quotaThresholds: fileConfig.scheduler?.quotaThresholds || {
|
|
116
|
+
normal: 50,
|
|
117
|
+
elevated: 70,
|
|
118
|
+
critical: 85,
|
|
119
|
+
shutdown: 95,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
projectName,
|
|
125
|
+
projectDir: resolvedProjectDir,
|
|
126
|
+
stateDir,
|
|
127
|
+
port: fileConfig.port || DEFAULT_PORT,
|
|
128
|
+
sessions,
|
|
129
|
+
scheduler,
|
|
130
|
+
users: fileConfig.users || [],
|
|
131
|
+
messaging: fileConfig.messaging || [],
|
|
132
|
+
monitoring: fileConfig.monitoring || {
|
|
133
|
+
quotaTracking: true,
|
|
134
|
+
memoryMonitoring: true,
|
|
135
|
+
healthCheckIntervalMs: 30000,
|
|
136
|
+
},
|
|
137
|
+
authToken: fileConfig.authToken,
|
|
138
|
+
relationships: fileConfig.relationships || {
|
|
139
|
+
relationshipsDir: path.join(stateDir, 'relationships'),
|
|
140
|
+
maxRecentInteractions: 20,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ensure the state directory structure exists.
|
|
147
|
+
*/
|
|
148
|
+
export function ensureStateDir(stateDir: string): void {
|
|
149
|
+
const dirs = [
|
|
150
|
+
stateDir,
|
|
151
|
+
path.join(stateDir, 'state'),
|
|
152
|
+
path.join(stateDir, 'state', 'sessions'),
|
|
153
|
+
path.join(stateDir, 'state', 'jobs'),
|
|
154
|
+
path.join(stateDir, 'relationships'),
|
|
155
|
+
path.join(stateDir, 'logs'),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const dir of dirs) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prerequisite detection and installation guidance.
|
|
3
|
+
*
|
|
4
|
+
* Checks for required software (tmux, Claude CLI, Node.js)
|
|
5
|
+
* and provides clear installation instructions when something is missing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
import { detectTmuxPath, detectClaudePath } from './Config.js';
|
|
12
|
+
|
|
13
|
+
export interface PrerequisiteResult {
|
|
14
|
+
name: string;
|
|
15
|
+
found: boolean;
|
|
16
|
+
path?: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
installHint: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PrerequisiteCheck {
|
|
22
|
+
allMet: boolean;
|
|
23
|
+
results: PrerequisiteResult[];
|
|
24
|
+
missing: PrerequisiteResult[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect the current platform for install guidance.
|
|
29
|
+
*/
|
|
30
|
+
function detectPlatform(): 'macos-arm' | 'macos-intel' | 'linux' | 'unknown' {
|
|
31
|
+
const platform = process.platform;
|
|
32
|
+
if (platform === 'darwin') {
|
|
33
|
+
const arch = process.arch;
|
|
34
|
+
return arch === 'arm64' ? 'macos-arm' : 'macos-intel';
|
|
35
|
+
}
|
|
36
|
+
if (platform === 'linux') return 'linux';
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if Homebrew is available (macOS).
|
|
42
|
+
*/
|
|
43
|
+
function hasHomebrew(): boolean {
|
|
44
|
+
try {
|
|
45
|
+
execSync('which brew', { encoding: 'utf-8', stdio: 'pipe' });
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get tmux version if installed.
|
|
54
|
+
*/
|
|
55
|
+
function getTmuxVersion(tmuxPath: string): string | undefined {
|
|
56
|
+
try {
|
|
57
|
+
const output = execSync(`${tmuxPath} -V`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
58
|
+
return output.replace('tmux ', '');
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get Claude CLI version if installed.
|
|
66
|
+
*/
|
|
67
|
+
function getClaudeVersion(claudePath: string): string | undefined {
|
|
68
|
+
try {
|
|
69
|
+
const output = execSync(`${claudePath} --version 2>/dev/null || echo unknown`, {
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
stdio: 'pipe',
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
}).trim();
|
|
74
|
+
return output || undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get Node.js version.
|
|
82
|
+
*/
|
|
83
|
+
function getNodeVersion(): { version: string; major: number } {
|
|
84
|
+
const version = process.version; // e.g., "v20.11.0"
|
|
85
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
86
|
+
return { version, major };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build install hint for tmux based on platform.
|
|
91
|
+
*/
|
|
92
|
+
function tmuxInstallHint(): string {
|
|
93
|
+
const platform = detectPlatform();
|
|
94
|
+
switch (platform) {
|
|
95
|
+
case 'macos-arm':
|
|
96
|
+
case 'macos-intel':
|
|
97
|
+
return hasHomebrew()
|
|
98
|
+
? 'Install with: brew install tmux'
|
|
99
|
+
: 'Install Homebrew first (https://brew.sh), then: brew install tmux';
|
|
100
|
+
case 'linux':
|
|
101
|
+
return 'Install with: sudo apt install tmux (Debian/Ubuntu) or sudo yum install tmux (RHEL/CentOS)';
|
|
102
|
+
default:
|
|
103
|
+
return 'Install tmux: https://github.com/tmux/tmux/wiki/Installing';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build install hint for Claude CLI based on platform.
|
|
109
|
+
*/
|
|
110
|
+
function claudeInstallHint(): string {
|
|
111
|
+
return 'Install Claude Code: npm install -g @anthropic-ai/claude-code\n Docs: https://docs.anthropic.com/en/docs/claude-code';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check all prerequisites and return a structured result.
|
|
116
|
+
*/
|
|
117
|
+
export function checkPrerequisites(): PrerequisiteCheck {
|
|
118
|
+
const results: PrerequisiteResult[] = [];
|
|
119
|
+
|
|
120
|
+
// 1. Node.js >= 18
|
|
121
|
+
const node = getNodeVersion();
|
|
122
|
+
results.push({
|
|
123
|
+
name: 'Node.js',
|
|
124
|
+
found: node.major >= 18,
|
|
125
|
+
version: node.version,
|
|
126
|
+
installHint: node.major < 18
|
|
127
|
+
? `Node.js 18+ required (found ${node.version}). Update: https://nodejs.org`
|
|
128
|
+
: '',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 2. tmux
|
|
132
|
+
const tmuxPath = detectTmuxPath();
|
|
133
|
+
results.push({
|
|
134
|
+
name: 'tmux',
|
|
135
|
+
found: !!tmuxPath,
|
|
136
|
+
path: tmuxPath || undefined,
|
|
137
|
+
version: tmuxPath ? getTmuxVersion(tmuxPath) : undefined,
|
|
138
|
+
installHint: tmuxInstallHint(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 3. Claude CLI
|
|
142
|
+
const claudePath = detectClaudePath();
|
|
143
|
+
results.push({
|
|
144
|
+
name: 'Claude CLI',
|
|
145
|
+
found: !!claudePath,
|
|
146
|
+
path: claudePath || undefined,
|
|
147
|
+
version: claudePath ? getClaudeVersion(claudePath) : undefined,
|
|
148
|
+
installHint: claudeInstallHint(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const missing = results.filter(r => !r.found);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
allMet: missing.length === 0,
|
|
155
|
+
results,
|
|
156
|
+
missing,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Print prerequisite check results to console.
|
|
162
|
+
* Returns true if all prerequisites are met.
|
|
163
|
+
*/
|
|
164
|
+
export function printPrerequisiteCheck(check: PrerequisiteCheck): boolean {
|
|
165
|
+
console.log(pc.bold(' Checking prerequisites...'));
|
|
166
|
+
console.log();
|
|
167
|
+
|
|
168
|
+
for (const result of check.results) {
|
|
169
|
+
if (result.found) {
|
|
170
|
+
const versionStr = result.version ? ` (${result.version})` : '';
|
|
171
|
+
const pathStr = result.path ? pc.dim(` ${result.path}`) : '';
|
|
172
|
+
console.log(` ${pc.green('✓')} ${result.name}${versionStr}${pathStr}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(` ${pc.red('✗')} ${result.name} — not found`);
|
|
175
|
+
console.log(` ${result.installHint}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log();
|
|
180
|
+
|
|
181
|
+
if (!check.allMet) {
|
|
182
|
+
console.log(pc.red(` ${check.missing.length} prerequisite(s) missing. Install them and try again.`));
|
|
183
|
+
console.log();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return check.allMet;
|
|
187
|
+
}
|