@automagik/genie 0.260202.530 → 0.260202.1833

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/claudio.js +44 -45
  2. package/dist/genie.js +58 -135
  3. package/dist/term.js +134 -67
  4. package/install.sh +43 -7
  5. package/package.json +1 -1
  6. package/src/claudio.ts +31 -21
  7. package/src/commands/launch.ts +12 -68
  8. package/src/genie-commands/doctor.ts +327 -0
  9. package/src/genie-commands/setup.ts +317 -199
  10. package/src/genie-commands/uninstall.ts +176 -0
  11. package/src/genie.ts +24 -44
  12. package/src/lib/claude-settings.ts +22 -64
  13. package/src/lib/genie-config.ts +169 -57
  14. package/src/lib/orchestrator/completion.ts +392 -0
  15. package/src/lib/orchestrator/event-monitor.ts +442 -0
  16. package/src/lib/orchestrator/index.ts +12 -0
  17. package/src/lib/orchestrator/patterns.ts +277 -0
  18. package/src/lib/orchestrator/state-detector.ts +339 -0
  19. package/src/lib/version.ts +1 -1
  20. package/src/lib/worker-registry.ts +229 -0
  21. package/src/term-commands/close.ts +221 -0
  22. package/src/term-commands/exec.ts +28 -6
  23. package/src/term-commands/kill.ts +143 -0
  24. package/src/term-commands/orchestrate.ts +844 -0
  25. package/src/term-commands/read.ts +6 -1
  26. package/src/term-commands/shortcuts.ts +14 -14
  27. package/src/term-commands/work.ts +415 -0
  28. package/src/term-commands/workers.ts +264 -0
  29. package/src/term.ts +201 -3
  30. package/src/types/genie-config.ts +49 -81
  31. package/src/genie-commands/hooks.ts +0 -317
  32. package/src/lib/hook-script.ts +0 -263
  33. package/src/lib/hooks/compose.ts +0 -72
  34. package/src/lib/hooks/index.ts +0 -163
  35. package/src/lib/hooks/presets/audited.ts +0 -191
  36. package/src/lib/hooks/presets/collaborative.ts +0 -143
  37. package/src/lib/hooks/presets/sandboxed.ts +0 -153
  38. package/src/lib/hooks/presets/supervised.ts +0 -66
  39. package/src/lib/hooks/utils/escape.ts +0 -46
@@ -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,221 @@
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 { WorktreeManager } from '../lib/worktree.js';
19
+ import { join } from 'path';
20
+ import { homedir } from 'os';
21
+
22
+ // ============================================================================
23
+ // Types
24
+ // ============================================================================
25
+
26
+ export interface CloseOptions {
27
+ noSync?: boolean;
28
+ keepWorktree?: boolean;
29
+ merge?: boolean;
30
+ yes?: boolean;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Configuration
35
+ // ============================================================================
36
+
37
+ const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
38
+
39
+ // ============================================================================
40
+ // Helper Functions
41
+ // ============================================================================
42
+
43
+ /**
44
+ * Run bd command
45
+ */
46
+ async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
47
+ try {
48
+ const result = await $`bd ${args}`.quiet();
49
+ return { stdout: result.stdout.toString().trim(), exitCode: 0 };
50
+ } catch (error: any) {
51
+ return { stdout: error.stdout?.toString().trim() || '', exitCode: error.exitCode || 1 };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Close beads issue
57
+ */
58
+ async function closeBeadsIssue(taskId: string): Promise<boolean> {
59
+ const { exitCode } = await runBd(['close', taskId]);
60
+ return exitCode === 0;
61
+ }
62
+
63
+ /**
64
+ * Sync beads to git
65
+ */
66
+ async function syncBeads(): Promise<boolean> {
67
+ const { exitCode } = await runBd(['sync']);
68
+ return exitCode === 0;
69
+ }
70
+
71
+ /**
72
+ * Merge worktree branch to main
73
+ */
74
+ async function mergeToMain(
75
+ repoPath: string,
76
+ branchName: string
77
+ ): Promise<boolean> {
78
+ try {
79
+ // Get current branch
80
+ const currentResult = await $`git -C ${repoPath} branch --show-current`.quiet();
81
+ const currentBranch = currentResult.stdout.toString().trim();
82
+
83
+ if (currentBranch === branchName) {
84
+ console.log(`⚠️ Already on branch ${branchName}. Skipping merge.`);
85
+ return true;
86
+ }
87
+
88
+ // Checkout main and merge
89
+ console.log(` Switching to ${currentBranch}...`);
90
+ await $`git -C ${repoPath} checkout ${currentBranch}`.quiet();
91
+
92
+ console.log(` Merging ${branchName}...`);
93
+ await $`git -C ${repoPath} merge ${branchName} --no-edit`.quiet();
94
+
95
+ return true;
96
+ } catch (error: any) {
97
+ console.error(`⚠️ Merge failed: ${error.message}`);
98
+ return false;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Remove worktree
104
+ */
105
+ async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
106
+ try {
107
+ const manager = new WorktreeManager({
108
+ baseDir: WORKTREE_BASE,
109
+ repoPath,
110
+ });
111
+
112
+ if (await manager.worktreeExists(taskId)) {
113
+ await manager.removeWorktree(taskId);
114
+ return true;
115
+ }
116
+ return true; // Already doesn't exist
117
+ } catch (error: any) {
118
+ console.error(`⚠️ Failed to remove worktree: ${error.message}`);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Kill worker pane
125
+ */
126
+ async function killWorkerPane(paneId: string): Promise<boolean> {
127
+ try {
128
+ await tmux.killPane(paneId);
129
+ return true;
130
+ } catch {
131
+ return false; // Pane may already be gone
132
+ }
133
+ }
134
+
135
+ // ============================================================================
136
+ // Main Command
137
+ // ============================================================================
138
+
139
+ export async function closeCommand(
140
+ taskId: string,
141
+ options: CloseOptions = {}
142
+ ): Promise<void> {
143
+ try {
144
+ // Find worker in registry
145
+ const worker = await registry.findByTask(taskId);
146
+
147
+ if (!worker) {
148
+ console.log(`ℹ️ No active worker for ${taskId}. Closing issue only.`);
149
+ }
150
+
151
+ // Confirm with user
152
+ if (!options.yes) {
153
+ const confirmed = await confirm({
154
+ message: `Close ${taskId}${worker ? ` and kill worker (pane ${worker.paneId})` : ''}?`,
155
+ default: true,
156
+ });
157
+
158
+ if (!confirmed) {
159
+ console.log('Cancelled.');
160
+ return;
161
+ }
162
+ }
163
+
164
+ // 1. Close beads issue
165
+ console.log(`📝 Closing ${taskId}...`);
166
+ const closed = await closeBeadsIssue(taskId);
167
+ if (!closed) {
168
+ console.error(`❌ Failed to close ${taskId}. Check \`bd show ${taskId}\`.`);
169
+ // Continue with cleanup anyway
170
+ } else {
171
+ console.log(` ✅ Issue closed`);
172
+ }
173
+
174
+ // 2. Sync beads (unless --no-sync)
175
+ if (!options.noSync) {
176
+ console.log(`🔄 Syncing beads...`);
177
+ const synced = await syncBeads();
178
+ if (synced) {
179
+ console.log(` ✅ Synced to git`);
180
+ } else {
181
+ console.log(` ⚠️ Sync failed (non-fatal)`);
182
+ }
183
+ }
184
+
185
+ // 3. Handle worktree
186
+ if (worker?.worktree && !options.keepWorktree) {
187
+ // Merge if requested
188
+ if (options.merge) {
189
+ console.log(`🔀 Merging changes...`);
190
+ const merged = await mergeToMain(worker.repoPath, taskId);
191
+ if (merged) {
192
+ console.log(` ✅ Merged to main`);
193
+ }
194
+ }
195
+
196
+ // Remove worktree
197
+ console.log(`🌳 Removing worktree...`);
198
+ const removed = await removeWorktree(taskId, worker.repoPath);
199
+ if (removed) {
200
+ console.log(` ✅ Worktree removed`);
201
+ }
202
+ }
203
+
204
+ // 4. Kill worker pane
205
+ if (worker) {
206
+ console.log(`💀 Killing worker pane...`);
207
+ await killWorkerPane(worker.paneId);
208
+ console.log(` ✅ Pane killed`);
209
+
210
+ // 5. Unregister worker
211
+ await registry.unregister(worker.id);
212
+ console.log(` ✅ Worker unregistered`);
213
+ }
214
+
215
+ console.log(`\n✅ ${taskId} closed successfully`);
216
+
217
+ } catch (error: any) {
218
+ console.error(`❌ Error: ${error.message}`);
219
+ process.exit(1);
220
+ }
221
+ }
@@ -1,6 +1,16 @@
1
1
  import * as tmux from '../lib/tmux.js';
2
+ import { getTerminalConfig } from '../lib/genie-config.js';
2
3
 
3
- export async function executeInSession(target: string, command: string): Promise<void> {
4
+ export interface ExecOptions {
5
+ quiet?: boolean;
6
+ timeout?: number;
7
+ }
8
+
9
+ export async function executeInSession(
10
+ target: string,
11
+ command: string,
12
+ options: ExecOptions = {}
13
+ ): Promise<void> {
4
14
  // Parse target: "session:window" or just "session"
5
15
  const [sessionName, windowName] = target.includes(':')
6
16
  ? target.split(':')
@@ -10,7 +20,9 @@ export async function executeInSession(target: string, command: string): Promise
10
20
  // Find or create session
11
21
  let session = await tmux.findSessionByName(sessionName);
12
22
  if (!session) {
13
- console.error(`Session "${sessionName}" not found, creating...`);
23
+ if (!options.quiet) {
24
+ console.error(`Session "${sessionName}" not found, creating...`);
25
+ }
14
26
  session = await tmux.createSession(sessionName);
15
27
  if (!session) {
16
28
  console.error(`Failed to create session "${sessionName}"`);
@@ -22,7 +34,9 @@ export async function executeInSession(target: string, command: string): Promise
22
34
  let windows = await tmux.listWindows(session.id);
23
35
  let targetWindow = windows.find(w => w.name === windowName);
24
36
  if (!targetWindow) {
25
- console.error(`Window "${windowName}" not found, creating...`);
37
+ if (!options.quiet) {
38
+ console.error(`Window "${windowName}" not found, creating...`);
39
+ }
26
40
  targetWindow = await tmux.createWindow(session.id, windowName);
27
41
  if (!targetWindow) {
28
42
  console.error(`Failed to create window "${windowName}"`);
@@ -37,11 +51,19 @@ export async function executeInSession(target: string, command: string): Promise
37
51
  process.exit(1);
38
52
  }
39
53
 
54
+ // Use config default if no timeout specified
55
+ const termConfig = getTerminalConfig();
56
+ const timeout = options.timeout ?? termConfig.execTimeout;
57
+
40
58
  // Run command synchronously using wait-for (no polling, no ugly markers)
41
- const { output, exitCode } = await tmux.runCommandSync(panes[0].id, command);
59
+ const { output, exitCode } = await tmux.runCommandSync(
60
+ panes[0].id,
61
+ command,
62
+ timeout
63
+ );
42
64
 
43
- // Output the result
44
- if (output) {
65
+ // Output the result (unless quiet mode)
66
+ if (output && !options.quiet) {
45
67
  console.log(output);
46
68
  }
47
69
 
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Kill command - Force kill a worker
3
+ *
4
+ * Usage:
5
+ * term kill <worker> - Kill worker by ID or pane
6
+ *
7
+ * Options:
8
+ * -y, --yes - Skip confirmation
9
+ * --keep-worktree - Don't remove the worktree
10
+ */
11
+
12
+ import { confirm } from '@inquirer/prompts';
13
+ import * as tmux from '../lib/tmux.js';
14
+ import * as registry from '../lib/worker-registry.js';
15
+ import { WorktreeManager } from '../lib/worktree.js';
16
+ import { join } from 'path';
17
+ import { homedir } from 'os';
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface KillOptions {
24
+ yes?: boolean;
25
+ keepWorktree?: boolean;
26
+ }
27
+
28
+ // ============================================================================
29
+ // Configuration
30
+ // ============================================================================
31
+
32
+ const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
33
+
34
+ // ============================================================================
35
+ // Helper Functions
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Kill worker pane
40
+ */
41
+ async function killWorkerPane(paneId: string): Promise<boolean> {
42
+ try {
43
+ await tmux.killPane(paneId);
44
+ return true;
45
+ } catch {
46
+ return false; // Pane may already be gone
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Remove worktree
52
+ */
53
+ async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
54
+ try {
55
+ const manager = new WorktreeManager({
56
+ baseDir: WORKTREE_BASE,
57
+ repoPath,
58
+ });
59
+
60
+ if (await manager.worktreeExists(taskId)) {
61
+ await manager.removeWorktree(taskId);
62
+ return true;
63
+ }
64
+ return true; // Already doesn't exist
65
+ } catch (error: any) {
66
+ console.error(`⚠️ Failed to remove worktree: ${error.message}`);
67
+ return false;
68
+ }
69
+ }
70
+
71
+ // ============================================================================
72
+ // Main Command
73
+ // ============================================================================
74
+
75
+ export async function killCommand(
76
+ target: string,
77
+ options: KillOptions = {}
78
+ ): Promise<void> {
79
+ try {
80
+ // Find worker by ID or pane
81
+ let worker = await registry.get(target);
82
+
83
+ if (!worker) {
84
+ // Try finding by pane ID
85
+ worker = await registry.findByPane(target);
86
+ }
87
+
88
+ if (!worker) {
89
+ // Try finding by task ID
90
+ worker = await registry.findByTask(target);
91
+ }
92
+
93
+ if (!worker) {
94
+ console.error(`❌ Worker "${target}" not found.`);
95
+ console.log(` Run \`term workers\` to see active workers.`);
96
+ process.exit(1);
97
+ }
98
+
99
+ // Confirm with user
100
+ if (!options.yes) {
101
+ const confirmed = await confirm({
102
+ message: `Kill worker ${worker.id} (pane ${worker.paneId})?`,
103
+ default: true,
104
+ });
105
+
106
+ if (!confirmed) {
107
+ console.log('Cancelled.');
108
+ return;
109
+ }
110
+ }
111
+
112
+ // 1. Kill worker pane
113
+ console.log(`💀 Killing worker pane ${worker.paneId}...`);
114
+ const killed = await killWorkerPane(worker.paneId);
115
+ if (killed) {
116
+ console.log(` ✅ Pane killed`);
117
+ } else {
118
+ console.log(` ℹ️ Pane already gone`);
119
+ }
120
+
121
+ // 2. Remove worktree (unless --keep-worktree)
122
+ if (worker.worktree && !options.keepWorktree) {
123
+ console.log(`🌳 Removing worktree...`);
124
+ const removed = await removeWorktree(worker.taskId, worker.repoPath);
125
+ if (removed) {
126
+ console.log(` ✅ Worktree removed`);
127
+ }
128
+ }
129
+
130
+ // 3. Unregister worker
131
+ await registry.unregister(worker.id);
132
+ console.log(` ✅ Worker unregistered`);
133
+
134
+ // 4. Note about task status
135
+ console.log(`\n⚠️ Task ${worker.taskId} is still in_progress in beads.`);
136
+ console.log(` Run \`bd update ${worker.taskId} --status open\` to reopen,`);
137
+ console.log(` or \`term work ${worker.taskId}\` to start a new worker.`);
138
+
139
+ } catch (error: any) {
140
+ console.error(`❌ Error: ${error.message}`);
141
+ process.exit(1);
142
+ }
143
+ }