@automagik/genie 0.260202.1607 → 0.260202.1901
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/.beads/README.md +81 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +0 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/AGENTS.md +40 -0
- package/dist/claudio.js +5 -5
- package/dist/genie.js +6 -6
- package/dist/term.js +116 -53
- package/package.json +1 -1
- package/src/lib/beads-registry.ts +546 -0
- package/src/lib/orchestrator/completion.ts +392 -0
- package/src/lib/orchestrator/event-monitor.ts +442 -0
- package/src/lib/orchestrator/index.ts +12 -0
- package/src/lib/orchestrator/patterns.ts +277 -0
- package/src/lib/orchestrator/state-detector.ts +339 -0
- package/src/lib/tmux.ts +15 -1
- package/src/lib/version.ts +1 -1
- package/src/lib/worker-registry.ts +229 -0
- package/src/term-commands/close.ts +256 -0
- package/src/term-commands/daemon.ts +176 -0
- package/src/term-commands/kill.ts +186 -0
- package/src/term-commands/orchestrate.ts +844 -0
- package/src/term-commands/split.ts +8 -7
- package/src/term-commands/work.ts +497 -0
- package/src/term-commands/workers.ts +298 -0
- package/src/term.ts +227 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Registry - Manages worker state persistence
|
|
3
|
+
*
|
|
4
|
+
* Tracks Claude workers bound to beads issues, storing state in
|
|
5
|
+
* ~/.config/term/workers.json
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export type WorkerState =
|
|
17
|
+
| 'spawning' // Worker being created
|
|
18
|
+
| 'working' // Actively producing output
|
|
19
|
+
| 'idle' // At prompt, waiting for input
|
|
20
|
+
| 'permission' // Waiting for permission approval
|
|
21
|
+
| 'question' // Waiting for question answer
|
|
22
|
+
| 'done' // Task completed, ready for close
|
|
23
|
+
| 'error'; // Encountered error
|
|
24
|
+
|
|
25
|
+
export interface Worker {
|
|
26
|
+
/** Unique worker ID (usually matches taskId, e.g., "bd-42") */
|
|
27
|
+
id: string;
|
|
28
|
+
/** tmux pane ID (e.g., "%16") */
|
|
29
|
+
paneId: string;
|
|
30
|
+
/** tmux session name */
|
|
31
|
+
session: string;
|
|
32
|
+
/** Path to git worktree, null if using shared repo */
|
|
33
|
+
worktree: string | null;
|
|
34
|
+
/** Beads issue ID this worker is bound to */
|
|
35
|
+
taskId: string;
|
|
36
|
+
/** Task title from beads */
|
|
37
|
+
taskTitle?: string;
|
|
38
|
+
/** Associated wish slug (if from decompose) */
|
|
39
|
+
wishSlug?: string;
|
|
40
|
+
/** Execution group number within wish */
|
|
41
|
+
groupNumber?: number;
|
|
42
|
+
/** ISO timestamp when worker was started */
|
|
43
|
+
startedAt: string;
|
|
44
|
+
/** Current worker state */
|
|
45
|
+
state: WorkerState;
|
|
46
|
+
/** Last state change timestamp */
|
|
47
|
+
lastStateChange: string;
|
|
48
|
+
/** Repository path where worker operates */
|
|
49
|
+
repoPath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkerRegistry {
|
|
53
|
+
workers: Record<string, Worker>;
|
|
54
|
+
lastUpdated: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Configuration
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
const CONFIG_DIR = join(homedir(), '.config', 'term');
|
|
62
|
+
const REGISTRY_FILE = join(CONFIG_DIR, 'workers.json');
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Private Functions
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
async function ensureConfigDir(): Promise<void> {
|
|
69
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadRegistry(): Promise<WorkerRegistry> {
|
|
73
|
+
try {
|
|
74
|
+
const content = await readFile(REGISTRY_FILE, 'utf-8');
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch {
|
|
77
|
+
return { workers: {}, lastUpdated: new Date().toISOString() };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function saveRegistry(registry: WorkerRegistry): Promise<void> {
|
|
82
|
+
await ensureConfigDir();
|
|
83
|
+
registry.lastUpdated = new Date().toISOString();
|
|
84
|
+
await writeFile(REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Public API
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register a new worker in the registry
|
|
93
|
+
*/
|
|
94
|
+
export async function register(worker: Worker): Promise<void> {
|
|
95
|
+
const registry = await loadRegistry();
|
|
96
|
+
registry.workers[worker.id] = worker;
|
|
97
|
+
await saveRegistry(registry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Unregister (remove) a worker from the registry
|
|
102
|
+
*/
|
|
103
|
+
export async function unregister(id: string): Promise<void> {
|
|
104
|
+
const registry = await loadRegistry();
|
|
105
|
+
delete registry.workers[id];
|
|
106
|
+
await saveRegistry(registry);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a worker by ID
|
|
111
|
+
*/
|
|
112
|
+
export async function get(id: string): Promise<Worker | null> {
|
|
113
|
+
const registry = await loadRegistry();
|
|
114
|
+
return registry.workers[id] || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List all workers
|
|
119
|
+
*/
|
|
120
|
+
export async function list(): Promise<Worker[]> {
|
|
121
|
+
const registry = await loadRegistry();
|
|
122
|
+
return Object.values(registry.workers);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update a worker's state
|
|
127
|
+
*/
|
|
128
|
+
export async function updateState(id: string, state: WorkerState): Promise<void> {
|
|
129
|
+
const registry = await loadRegistry();
|
|
130
|
+
const worker = registry.workers[id];
|
|
131
|
+
if (worker) {
|
|
132
|
+
worker.state = state;
|
|
133
|
+
worker.lastStateChange = new Date().toISOString();
|
|
134
|
+
await saveRegistry(registry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update multiple worker fields
|
|
140
|
+
*/
|
|
141
|
+
export async function update(id: string, updates: Partial<Worker>): Promise<void> {
|
|
142
|
+
const registry = await loadRegistry();
|
|
143
|
+
const worker = registry.workers[id];
|
|
144
|
+
if (worker) {
|
|
145
|
+
Object.assign(worker, updates);
|
|
146
|
+
if (updates.state) {
|
|
147
|
+
worker.lastStateChange = new Date().toISOString();
|
|
148
|
+
}
|
|
149
|
+
await saveRegistry(registry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Find worker by tmux pane ID
|
|
155
|
+
*/
|
|
156
|
+
export async function findByPane(paneId: string): Promise<Worker | null> {
|
|
157
|
+
const workers = await list();
|
|
158
|
+
// Normalize pane ID (with or without % prefix)
|
|
159
|
+
const normalizedPaneId = paneId.startsWith('%') ? paneId : `%${paneId}`;
|
|
160
|
+
return workers.find(w => w.paneId === normalizedPaneId) || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Find worker by beads task ID
|
|
165
|
+
*/
|
|
166
|
+
export async function findByTask(taskId: string): Promise<Worker | null> {
|
|
167
|
+
const workers = await list();
|
|
168
|
+
return workers.find(w => w.taskId === taskId) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find workers by wish slug
|
|
173
|
+
*/
|
|
174
|
+
export async function findByWish(wishSlug: string): Promise<Worker[]> {
|
|
175
|
+
const workers = await list();
|
|
176
|
+
return workers.filter(w => w.wishSlug === wishSlug);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if a worker exists for a given task
|
|
181
|
+
*/
|
|
182
|
+
export async function hasWorkerForTask(taskId: string): Promise<boolean> {
|
|
183
|
+
const worker = await findByTask(taskId);
|
|
184
|
+
return worker !== null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get workers in a specific state
|
|
189
|
+
*/
|
|
190
|
+
export async function getByState(state: WorkerState): Promise<Worker[]> {
|
|
191
|
+
const workers = await list();
|
|
192
|
+
return workers.filter(w => w.state === state);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Calculate elapsed time for a worker
|
|
197
|
+
*/
|
|
198
|
+
export function getElapsedTime(worker: Worker): { ms: number; formatted: string } {
|
|
199
|
+
const startTime = new Date(worker.startedAt).getTime();
|
|
200
|
+
const ms = Date.now() - startTime;
|
|
201
|
+
|
|
202
|
+
const minutes = Math.floor(ms / 60000);
|
|
203
|
+
const hours = Math.floor(minutes / 60);
|
|
204
|
+
|
|
205
|
+
let formatted: string;
|
|
206
|
+
if (hours > 0) {
|
|
207
|
+
formatted = `${hours}h ${minutes % 60}m`;
|
|
208
|
+
} else if (minutes > 0) {
|
|
209
|
+
formatted = `${minutes}m`;
|
|
210
|
+
} else {
|
|
211
|
+
formatted = '<1m';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { ms, formatted };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the config directory path
|
|
219
|
+
*/
|
|
220
|
+
export function getConfigDir(): string {
|
|
221
|
+
return CONFIG_DIR;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get the registry file path
|
|
226
|
+
*/
|
|
227
|
+
export function getRegistryPath(): string {
|
|
228
|
+
return REGISTRY_FILE;
|
|
229
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close command - Close beads issue and cleanup worker
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* term close <bd-id> - Close issue, cleanup worktree, kill worker
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* --no-sync - Skip bd sync
|
|
9
|
+
* --keep-worktree - Don't remove the worktree
|
|
10
|
+
* --merge - Merge worktree changes to main branch
|
|
11
|
+
* -y, --yes - Skip confirmation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { $ } from 'bun';
|
|
15
|
+
import { confirm } from '@inquirer/prompts';
|
|
16
|
+
import * as tmux from '../lib/tmux.js';
|
|
17
|
+
import * as registry from '../lib/worker-registry.js';
|
|
18
|
+
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
19
|
+
import { WorktreeManager } from '../lib/worktree.js';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
|
|
23
|
+
// Use beads registry when enabled
|
|
24
|
+
const useBeads = beadsRegistry.isBeadsRegistryEnabled();
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Types
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export interface CloseOptions {
|
|
31
|
+
noSync?: boolean;
|
|
32
|
+
keepWorktree?: boolean;
|
|
33
|
+
merge?: boolean;
|
|
34
|
+
yes?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Configuration
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Helper Functions
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run bd command
|
|
49
|
+
*/
|
|
50
|
+
async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
|
|
51
|
+
try {
|
|
52
|
+
const result = await $`bd ${args}`.quiet();
|
|
53
|
+
return { stdout: result.stdout.toString().trim(), exitCode: 0 };
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
return { stdout: error.stdout?.toString().trim() || '', exitCode: error.exitCode || 1 };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Close beads issue
|
|
61
|
+
*/
|
|
62
|
+
async function closeBeadsIssue(taskId: string): Promise<boolean> {
|
|
63
|
+
const { exitCode } = await runBd(['close', taskId]);
|
|
64
|
+
return exitCode === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sync beads to git
|
|
69
|
+
*/
|
|
70
|
+
async function syncBeads(): Promise<boolean> {
|
|
71
|
+
const { exitCode } = await runBd(['sync']);
|
|
72
|
+
return exitCode === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Merge worktree branch to main
|
|
77
|
+
*/
|
|
78
|
+
async function mergeToMain(
|
|
79
|
+
repoPath: string,
|
|
80
|
+
branchName: string
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
// Get current branch
|
|
84
|
+
const currentResult = await $`git -C ${repoPath} branch --show-current`.quiet();
|
|
85
|
+
const currentBranch = currentResult.stdout.toString().trim();
|
|
86
|
+
|
|
87
|
+
if (currentBranch === branchName) {
|
|
88
|
+
console.log(`⚠️ Already on branch ${branchName}. Skipping merge.`);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Checkout main and merge
|
|
93
|
+
console.log(` Switching to ${currentBranch}...`);
|
|
94
|
+
await $`git -C ${repoPath} checkout ${currentBranch}`.quiet();
|
|
95
|
+
|
|
96
|
+
console.log(` Merging ${branchName}...`);
|
|
97
|
+
await $`git -C ${repoPath} merge ${branchName} --no-edit`.quiet();
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
console.error(`⚠️ Merge failed: ${error.message}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove worktree
|
|
108
|
+
* Uses bd worktree when beads registry is enabled
|
|
109
|
+
* Falls back to WorktreeManager otherwise
|
|
110
|
+
*/
|
|
111
|
+
async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
|
|
112
|
+
// Try bd worktree first when beads is enabled
|
|
113
|
+
if (useBeads) {
|
|
114
|
+
try {
|
|
115
|
+
const removed = await beadsRegistry.removeWorktree(taskId);
|
|
116
|
+
if (removed) return true;
|
|
117
|
+
// Fall through to WorktreeManager if bd worktree fails
|
|
118
|
+
} catch {
|
|
119
|
+
// Fall through
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback to WorktreeManager
|
|
124
|
+
try {
|
|
125
|
+
const manager = new WorktreeManager({
|
|
126
|
+
baseDir: WORKTREE_BASE,
|
|
127
|
+
repoPath,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (await manager.worktreeExists(taskId)) {
|
|
131
|
+
await manager.removeWorktree(taskId);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return true; // Already doesn't exist
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
console.error(`⚠️ Failed to remove worktree: ${error.message}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Kill worker pane
|
|
143
|
+
*/
|
|
144
|
+
async function killWorkerPane(paneId: string): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
await tmux.killPane(paneId);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false; // Pane may already be gone
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Main Command
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
export async function closeCommand(
|
|
158
|
+
taskId: string,
|
|
159
|
+
options: CloseOptions = {}
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
// Find worker in registry (check both during transition)
|
|
163
|
+
let worker = useBeads
|
|
164
|
+
? await beadsRegistry.findByTask(taskId)
|
|
165
|
+
: null;
|
|
166
|
+
if (!worker) {
|
|
167
|
+
worker = await registry.findByTask(taskId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!worker) {
|
|
171
|
+
console.log(`ℹ️ No active worker for ${taskId}. Closing issue only.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Confirm with user
|
|
175
|
+
if (!options.yes) {
|
|
176
|
+
const confirmed = await confirm({
|
|
177
|
+
message: `Close ${taskId}${worker ? ` and kill worker (pane ${worker.paneId})` : ''}?`,
|
|
178
|
+
default: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!confirmed) {
|
|
182
|
+
console.log('Cancelled.');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 1. Close beads issue
|
|
188
|
+
console.log(`📝 Closing ${taskId}...`);
|
|
189
|
+
const closed = await closeBeadsIssue(taskId);
|
|
190
|
+
if (!closed) {
|
|
191
|
+
console.error(`❌ Failed to close ${taskId}. Check \`bd show ${taskId}\`.`);
|
|
192
|
+
// Continue with cleanup anyway
|
|
193
|
+
} else {
|
|
194
|
+
console.log(` ✅ Issue closed`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 2. Sync beads (unless --no-sync)
|
|
198
|
+
if (!options.noSync) {
|
|
199
|
+
console.log(`🔄 Syncing beads...`);
|
|
200
|
+
const synced = await syncBeads();
|
|
201
|
+
if (synced) {
|
|
202
|
+
console.log(` ✅ Synced to git`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` ⚠️ Sync failed (non-fatal)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 3. Handle worktree
|
|
209
|
+
if (worker?.worktree && !options.keepWorktree) {
|
|
210
|
+
// Merge if requested
|
|
211
|
+
if (options.merge) {
|
|
212
|
+
console.log(`🔀 Merging changes...`);
|
|
213
|
+
const merged = await mergeToMain(worker.repoPath, taskId);
|
|
214
|
+
if (merged) {
|
|
215
|
+
console.log(` ✅ Merged to main`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Remove worktree
|
|
220
|
+
console.log(`🌳 Removing worktree...`);
|
|
221
|
+
const removed = await removeWorktree(taskId, worker.repoPath);
|
|
222
|
+
if (removed) {
|
|
223
|
+
console.log(` ✅ Worktree removed`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 4. Kill worker pane
|
|
228
|
+
if (worker) {
|
|
229
|
+
console.log(`💀 Killing worker pane...`);
|
|
230
|
+
await killWorkerPane(worker.paneId);
|
|
231
|
+
console.log(` ✅ Pane killed`);
|
|
232
|
+
|
|
233
|
+
// 5. Unregister worker from both registries
|
|
234
|
+
if (useBeads) {
|
|
235
|
+
try {
|
|
236
|
+
// Unbind work from agent
|
|
237
|
+
await beadsRegistry.unbindWork(worker.id);
|
|
238
|
+
// Set agent state to done
|
|
239
|
+
await beadsRegistry.setAgentState(worker.id, 'done');
|
|
240
|
+
// Delete agent bead
|
|
241
|
+
await beadsRegistry.deleteAgent(worker.id);
|
|
242
|
+
} catch {
|
|
243
|
+
// Non-fatal if beads cleanup fails
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await registry.unregister(worker.id);
|
|
247
|
+
console.log(` ✅ Worker unregistered`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(`\n✅ ${taskId} closed successfully`);
|
|
251
|
+
|
|
252
|
+
} catch (error: any) {
|
|
253
|
+
console.error(`❌ Error: ${error.message}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon command - Manage beads daemon
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* term daemon start - Start beads daemon (auto-commit, auto-sync)
|
|
6
|
+
* term daemon stop - Stop beads daemon
|
|
7
|
+
* term daemon status - Show daemon status
|
|
8
|
+
* term daemon restart - Restart daemon
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --auto-commit - Enable auto-commit (default: true for start)
|
|
12
|
+
* --auto-push - Enable auto-push to remote
|
|
13
|
+
* --json - Output as JSON
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface DaemonStartOptions {
|
|
23
|
+
autoCommit?: boolean;
|
|
24
|
+
autoPush?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DaemonStatusOptions {
|
|
28
|
+
json?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Commands
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start the beads daemon
|
|
37
|
+
*/
|
|
38
|
+
export async function startCommand(options: DaemonStartOptions = {}): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
// Check if already running
|
|
41
|
+
const status = await beadsRegistry.checkDaemonStatus();
|
|
42
|
+
if (status.running) {
|
|
43
|
+
console.log('ℹ️ Daemon is already running');
|
|
44
|
+
if (status.pid) {
|
|
45
|
+
console.log(` PID: ${status.pid}`);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('🚀 Starting beads daemon...');
|
|
51
|
+
const started = await beadsRegistry.startDaemon({
|
|
52
|
+
autoCommit: options.autoCommit !== false, // Default to true
|
|
53
|
+
autoPush: options.autoPush,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (started) {
|
|
57
|
+
console.log(' ✅ Daemon started');
|
|
58
|
+
|
|
59
|
+
// Show updated status
|
|
60
|
+
const newStatus = await beadsRegistry.checkDaemonStatus();
|
|
61
|
+
if (newStatus.pid) {
|
|
62
|
+
console.log(` PID: ${newStatus.pid}`);
|
|
63
|
+
}
|
|
64
|
+
if (newStatus.autoCommit) {
|
|
65
|
+
console.log(' Auto-commit: enabled');
|
|
66
|
+
}
|
|
67
|
+
if (newStatus.autoPush) {
|
|
68
|
+
console.log(' Auto-push: enabled');
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
console.error('❌ Failed to start daemon');
|
|
72
|
+
console.log(' Check `bd daemon status` for details');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
console.error(`❌ Error: ${error.message}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Stop the beads daemon
|
|
83
|
+
*/
|
|
84
|
+
export async function stopCommand(): Promise<void> {
|
|
85
|
+
try {
|
|
86
|
+
// Check if running
|
|
87
|
+
const status = await beadsRegistry.checkDaemonStatus();
|
|
88
|
+
if (!status.running) {
|
|
89
|
+
console.log('ℹ️ Daemon is not running');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('🛑 Stopping beads daemon...');
|
|
94
|
+
const stopped = await beadsRegistry.stopDaemon();
|
|
95
|
+
|
|
96
|
+
if (stopped) {
|
|
97
|
+
console.log(' ✅ Daemon stopped');
|
|
98
|
+
} else {
|
|
99
|
+
console.error('❌ Failed to stop daemon');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
console.error(`❌ Error: ${error.message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show daemon status
|
|
110
|
+
*/
|
|
111
|
+
export async function statusCommand(options: DaemonStatusOptions = {}): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
const status = await beadsRegistry.checkDaemonStatus();
|
|
114
|
+
|
|
115
|
+
if (options.json) {
|
|
116
|
+
console.log(JSON.stringify(status, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('Beads Daemon Status');
|
|
121
|
+
console.log('───────────────────');
|
|
122
|
+
console.log(`Running: ${status.running ? '✅ yes' : '❌ no'}`);
|
|
123
|
+
|
|
124
|
+
if (status.running) {
|
|
125
|
+
if (status.pid) {
|
|
126
|
+
console.log(`PID: ${status.pid}`);
|
|
127
|
+
}
|
|
128
|
+
if (status.lastSync) {
|
|
129
|
+
console.log(`Last sync: ${status.lastSync}`);
|
|
130
|
+
}
|
|
131
|
+
if (status.autoCommit !== undefined) {
|
|
132
|
+
console.log(`Auto-commit: ${status.autoCommit ? 'enabled' : 'disabled'}`);
|
|
133
|
+
}
|
|
134
|
+
if (status.autoPush !== undefined) {
|
|
135
|
+
console.log(`Auto-push: ${status.autoPush ? 'enabled' : 'disabled'}`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log('\nRun `term daemon start` to start the daemon');
|
|
139
|
+
}
|
|
140
|
+
} catch (error: any) {
|
|
141
|
+
console.error(`❌ Error: ${error.message}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Restart the beads daemon
|
|
148
|
+
*/
|
|
149
|
+
export async function restartCommand(options: DaemonStartOptions = {}): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
// Check if running and stop
|
|
152
|
+
const status = await beadsRegistry.checkDaemonStatus();
|
|
153
|
+
if (status.running) {
|
|
154
|
+
console.log('🛑 Stopping beads daemon...');
|
|
155
|
+
await beadsRegistry.stopDaemon();
|
|
156
|
+
console.log(' ✅ Stopped');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Start with new options
|
|
160
|
+
console.log('🚀 Starting beads daemon...');
|
|
161
|
+
const started = await beadsRegistry.startDaemon({
|
|
162
|
+
autoCommit: options.autoCommit !== false,
|
|
163
|
+
autoPush: options.autoPush,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (started) {
|
|
167
|
+
console.log(' ✅ Daemon restarted');
|
|
168
|
+
} else {
|
|
169
|
+
console.error('❌ Failed to restart daemon');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
console.error(`❌ Error: ${error.message}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|