@automagik/genie 0.260201.2240
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/.github/workflows/publish.yml +26 -0
- package/.worktrees/.metadata.json +3 -0
- package/README.md +532 -0
- package/bun.lock +101 -0
- package/dist/claudio.js +76 -0
- package/dist/genie.js +201 -0
- package/dist/term.js +136 -0
- package/install.sh +351 -0
- package/package.json +37 -0
- package/scripts/version.ts +48 -0
- package/src/claudio.ts +128 -0
- package/src/commands/launch.ts +245 -0
- package/src/commands/models.ts +43 -0
- package/src/commands/profiles.ts +95 -0
- package/src/commands/setup.ts +5 -0
- package/src/genie-commands/hooks.ts +317 -0
- package/src/genie-commands/install.ts +351 -0
- package/src/genie-commands/setup.ts +282 -0
- package/src/genie-commands/shortcuts.ts +62 -0
- package/src/genie-commands/update.ts +228 -0
- package/src/genie.ts +106 -0
- package/src/lib/api-client.ts +109 -0
- package/src/lib/claude-settings.ts +252 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/genie-config.ts +164 -0
- package/src/lib/hook-manager.ts +130 -0
- package/src/lib/hook-script.ts +256 -0
- package/src/lib/hooks/compose.ts +72 -0
- package/src/lib/hooks/index.ts +163 -0
- package/src/lib/hooks/presets/audited.ts +191 -0
- package/src/lib/hooks/presets/collaborative.ts +143 -0
- package/src/lib/hooks/presets/sandboxed.ts +153 -0
- package/src/lib/hooks/presets/supervised.ts +66 -0
- package/src/lib/hooks/utils/escape.ts +46 -0
- package/src/lib/log-reader.ts +213 -0
- package/src/lib/picker.ts +62 -0
- package/src/lib/session-metadata.ts +58 -0
- package/src/lib/system-detect.ts +185 -0
- package/src/lib/tmux.ts +410 -0
- package/src/lib/version.ts +15 -0
- package/src/lib/wizard.ts +104 -0
- package/src/lib/worktree.ts +362 -0
- package/src/term-commands/attach.ts +23 -0
- package/src/term-commands/exec.ts +34 -0
- package/src/term-commands/hook.ts +42 -0
- package/src/term-commands/ls.ts +33 -0
- package/src/term-commands/new.ts +73 -0
- package/src/term-commands/pane.ts +81 -0
- package/src/term-commands/read.ts +70 -0
- package/src/term-commands/rm.ts +47 -0
- package/src/term-commands/send.ts +34 -0
- package/src/term-commands/shortcuts.ts +355 -0
- package/src/term-commands/split.ts +87 -0
- package/src/term-commands/status.ts +116 -0
- package/src/term-commands/window.ts +72 -0
- package/src/term.ts +192 -0
- package/src/types/config.ts +17 -0
- package/src/types/genie-config.ts +104 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genie Hooks Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands to install, uninstall, and test hooks in Claude Code.
|
|
5
|
+
* Bridges the gap between genie config and Claude Code settings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
loadClaudeSettings,
|
|
10
|
+
saveClaudeSettings,
|
|
11
|
+
isGenieHookInstalled,
|
|
12
|
+
addGenieHook,
|
|
13
|
+
removeGenieHook,
|
|
14
|
+
getClaudeSettingsPath,
|
|
15
|
+
contractClaudePath,
|
|
16
|
+
} from '../lib/claude-settings.js';
|
|
17
|
+
import {
|
|
18
|
+
hookScriptExists,
|
|
19
|
+
writeHookScript,
|
|
20
|
+
removeHookScript,
|
|
21
|
+
testHookScript,
|
|
22
|
+
getHookScriptDisplayPath,
|
|
23
|
+
} from '../lib/hook-script.js';
|
|
24
|
+
import {
|
|
25
|
+
loadGenieConfig,
|
|
26
|
+
getGenieConfigPath,
|
|
27
|
+
} from '../lib/genie-config.js';
|
|
28
|
+
import { describeEnabledHooks, hasEnabledHooks } from '../lib/hooks/index.js';
|
|
29
|
+
import { checkCommand } from '../lib/system-detect.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Print a boxed success message
|
|
33
|
+
*/
|
|
34
|
+
function printSuccessBox(lines: string[]): void {
|
|
35
|
+
const maxLen = Math.max(...lines.map((l) => l.length));
|
|
36
|
+
const width = maxLen + 4;
|
|
37
|
+
|
|
38
|
+
console.log();
|
|
39
|
+
console.log('\x1b[32m' + '+' + '-'.repeat(width) + '+' + '\x1b[0m');
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const padding = ' '.repeat(maxLen - line.length);
|
|
42
|
+
console.log('\x1b[32m' + '| ' + '\x1b[0m' + line + padding + '\x1b[32m |' + '\x1b[0m');
|
|
43
|
+
}
|
|
44
|
+
console.log('\x1b[32m' + '+' + '-'.repeat(width) + '+' + '\x1b[0m');
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check required dependencies
|
|
50
|
+
*/
|
|
51
|
+
async function checkDependencies(): Promise<{
|
|
52
|
+
jq: boolean;
|
|
53
|
+
tmux: boolean;
|
|
54
|
+
term: boolean;
|
|
55
|
+
errors: string[];
|
|
56
|
+
}> {
|
|
57
|
+
const errors: string[] = [];
|
|
58
|
+
|
|
59
|
+
const jqCheck = await checkCommand('jq');
|
|
60
|
+
const tmuxCheck = await checkCommand('tmux');
|
|
61
|
+
const termCheck = await checkCommand('term');
|
|
62
|
+
|
|
63
|
+
if (!jqCheck.exists) {
|
|
64
|
+
errors.push('jq is required but not installed. Install with: brew install jq (or apt install jq)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
jq: jqCheck.exists,
|
|
69
|
+
tmux: tmuxCheck.exists,
|
|
70
|
+
term: termCheck.exists,
|
|
71
|
+
errors,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Install hooks into Claude Code
|
|
77
|
+
*/
|
|
78
|
+
export async function installHooksCommand(options: { force?: boolean } = {}): Promise<void> {
|
|
79
|
+
console.log();
|
|
80
|
+
console.log('\x1b[1m Installing Genie Hooks\x1b[0m');
|
|
81
|
+
console.log();
|
|
82
|
+
|
|
83
|
+
// Step 1: Check dependencies
|
|
84
|
+
console.log('\x1b[2mChecking dependencies...\x1b[0m');
|
|
85
|
+
const deps = await checkDependencies();
|
|
86
|
+
|
|
87
|
+
if (deps.jq) {
|
|
88
|
+
console.log(' \x1b[32m+\x1b[0m jq');
|
|
89
|
+
} else {
|
|
90
|
+
console.log(' \x1b[31m-\x1b[0m jq (required)');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (deps.tmux) {
|
|
94
|
+
console.log(' \x1b[32m+\x1b[0m tmux');
|
|
95
|
+
} else {
|
|
96
|
+
console.log(' \x1b[33m-\x1b[0m tmux (recommended for collaborative mode)');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (deps.term) {
|
|
100
|
+
console.log(' \x1b[32m+\x1b[0m term');
|
|
101
|
+
} else {
|
|
102
|
+
console.log(' \x1b[33m-\x1b[0m term (required for collaborative mode)');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (deps.errors.length > 0) {
|
|
106
|
+
console.log();
|
|
107
|
+
for (const error of deps.errors) {
|
|
108
|
+
console.log('\x1b[31mError:\x1b[0m ' + error);
|
|
109
|
+
}
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log();
|
|
114
|
+
|
|
115
|
+
// Step 2: Load genie config and check presets
|
|
116
|
+
console.log('\x1b[2mLoading configuration...\x1b[0m');
|
|
117
|
+
const config = await loadGenieConfig();
|
|
118
|
+
|
|
119
|
+
if (!hasEnabledHooks(config)) {
|
|
120
|
+
console.log();
|
|
121
|
+
console.log('\x1b[33mNo hook presets are enabled.\x1b[0m');
|
|
122
|
+
console.log('Run \x1b[36mgenie setup\x1b[0m to configure hook presets first.');
|
|
123
|
+
console.log();
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const descriptions = describeEnabledHooks(config);
|
|
128
|
+
console.log(' \x1b[32m+\x1b[0m Enabled presets: ' + config.hooks.enabled.join(', '));
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
// Step 3: Check if collaborative preset needs tmux
|
|
132
|
+
if (config.hooks.enabled.includes('collaborative')) {
|
|
133
|
+
if (!deps.tmux) {
|
|
134
|
+
console.log('\x1b[33mWarning:\x1b[0m Collaborative mode enabled but tmux not found.');
|
|
135
|
+
console.log('Install tmux for the best experience: brew install tmux (or apt install tmux)');
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
if (!deps.term) {
|
|
139
|
+
console.log('\x1b[33mWarning:\x1b[0m Collaborative mode enabled but term not found.');
|
|
140
|
+
console.log('The term command is required for collaborative mode to work.');
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Step 4: Check if already installed
|
|
146
|
+
const settings = await loadClaudeSettings();
|
|
147
|
+
if (isGenieHookInstalled(settings) && !options.force) {
|
|
148
|
+
console.log('\x1b[33mGenie hooks are already installed.\x1b[0m');
|
|
149
|
+
console.log('Use \x1b[36m--force\x1b[0m to reinstall.');
|
|
150
|
+
console.log();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 5: Write the hook script
|
|
155
|
+
console.log('\x1b[2mWriting hook script...\x1b[0m');
|
|
156
|
+
await writeHookScript();
|
|
157
|
+
console.log(' \x1b[32m+\x1b[0m ' + getHookScriptDisplayPath());
|
|
158
|
+
console.log();
|
|
159
|
+
|
|
160
|
+
// Step 6: Update Claude settings
|
|
161
|
+
console.log('\x1b[2mUpdating Claude Code settings...\x1b[0m');
|
|
162
|
+
const updatedSettings = addGenieHook(settings);
|
|
163
|
+
await saveClaudeSettings(updatedSettings);
|
|
164
|
+
console.log(' \x1b[32m+\x1b[0m Hook registered in ' + contractClaudePath(getClaudeSettingsPath()));
|
|
165
|
+
console.log();
|
|
166
|
+
|
|
167
|
+
// Step 7: Print success
|
|
168
|
+
const sessionName = config.hooks.collaborative?.sessionName || 'genie';
|
|
169
|
+
printSuccessBox([
|
|
170
|
+
'\x1b[32m+\x1b[0m Installation complete!',
|
|
171
|
+
'',
|
|
172
|
+
'Restart Claude Code for changes to take effect.',
|
|
173
|
+
'',
|
|
174
|
+
'After restart, all Bash commands will run in tmux.',
|
|
175
|
+
`Watch with: tmux attach -t ${sessionName}`,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Uninstall hooks from Claude Code
|
|
181
|
+
*/
|
|
182
|
+
export async function uninstallHooksCommand(options: { keepScript?: boolean } = {}): Promise<void> {
|
|
183
|
+
console.log();
|
|
184
|
+
console.log('\x1b[1m Uninstalling Genie Hooks\x1b[0m');
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
// Step 1: Remove from Claude settings
|
|
188
|
+
console.log('\x1b[2mRemoving from Claude Code settings...\x1b[0m');
|
|
189
|
+
const settings = await loadClaudeSettings();
|
|
190
|
+
|
|
191
|
+
if (!isGenieHookInstalled(settings)) {
|
|
192
|
+
console.log(' \x1b[33m-\x1b[0m No genie hooks found in settings');
|
|
193
|
+
} else {
|
|
194
|
+
const updatedSettings = removeGenieHook(settings);
|
|
195
|
+
await saveClaudeSettings(updatedSettings);
|
|
196
|
+
console.log(' \x1b[32m+\x1b[0m Hook removed from ' + contractClaudePath(getClaudeSettingsPath()));
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
|
|
200
|
+
// Step 2: Remove hook script (unless --keep-script)
|
|
201
|
+
if (!options.keepScript) {
|
|
202
|
+
console.log('\x1b[2mRemoving hook script...\x1b[0m');
|
|
203
|
+
if (hookScriptExists()) {
|
|
204
|
+
removeHookScript();
|
|
205
|
+
console.log(' \x1b[32m+\x1b[0m Deleted ' + getHookScriptDisplayPath());
|
|
206
|
+
} else {
|
|
207
|
+
console.log(' \x1b[33m-\x1b[0m Hook script not found');
|
|
208
|
+
}
|
|
209
|
+
console.log();
|
|
210
|
+
} else {
|
|
211
|
+
console.log('\x1b[2mKeeping hook script (--keep-script)\x1b[0m');
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Step 3: Print success
|
|
216
|
+
printSuccessBox([
|
|
217
|
+
'\x1b[32m+\x1b[0m Uninstallation complete!',
|
|
218
|
+
'',
|
|
219
|
+
'Restart Claude Code for changes to take effect.',
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Test the hook script
|
|
225
|
+
*/
|
|
226
|
+
export async function testHooksCommand(): Promise<void> {
|
|
227
|
+
console.log();
|
|
228
|
+
console.log('\x1b[1m Testing Genie Hooks\x1b[0m');
|
|
229
|
+
console.log();
|
|
230
|
+
|
|
231
|
+
// Check if script exists
|
|
232
|
+
if (!hookScriptExists()) {
|
|
233
|
+
console.log('\x1b[31mError:\x1b[0m Hook script not found.');
|
|
234
|
+
console.log('Run \x1b[36mgenie hooks install\x1b[0m first.');
|
|
235
|
+
console.log();
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('Script location: ' + getHookScriptDisplayPath());
|
|
240
|
+
console.log();
|
|
241
|
+
|
|
242
|
+
// Run test
|
|
243
|
+
console.log('\x1b[2mTesting with sample Bash input...\x1b[0m');
|
|
244
|
+
const result = await testHookScript();
|
|
245
|
+
|
|
246
|
+
if (result.success) {
|
|
247
|
+
console.log('\x1b[32m+\x1b[0m Hook script works correctly!');
|
|
248
|
+
console.log();
|
|
249
|
+
console.log('\x1b[2mSample output:\x1b[0m');
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(result.output || '{}');
|
|
252
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
253
|
+
} catch {
|
|
254
|
+
console.log(result.output);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
console.log('\x1b[31m-\x1b[0m Hook script test failed!');
|
|
258
|
+
console.log();
|
|
259
|
+
console.log('\x1b[31mError:\x1b[0m ' + result.error);
|
|
260
|
+
if (result.output) {
|
|
261
|
+
console.log();
|
|
262
|
+
console.log('\x1b[2mOutput:\x1b[0m');
|
|
263
|
+
console.log(result.output);
|
|
264
|
+
}
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Show current hook configuration (moved from setup.ts)
|
|
273
|
+
*/
|
|
274
|
+
export async function showHooksCommand(): Promise<void> {
|
|
275
|
+
const config = await loadGenieConfig();
|
|
276
|
+
const descriptions = describeEnabledHooks(config);
|
|
277
|
+
|
|
278
|
+
console.log();
|
|
279
|
+
console.log('\x1b[1m Current Hook Configuration\x1b[0m');
|
|
280
|
+
console.log(` Genie config: ${contractClaudePath(getGenieConfigPath())}`);
|
|
281
|
+
console.log(` Claude settings: ${contractClaudePath(getClaudeSettingsPath())}`);
|
|
282
|
+
console.log();
|
|
283
|
+
|
|
284
|
+
// Show enabled presets
|
|
285
|
+
if (descriptions.length === 0) {
|
|
286
|
+
console.log('\x1b[33m No hook presets enabled.\x1b[0m');
|
|
287
|
+
console.log(' Run \x1b[36mgenie setup\x1b[0m to configure hooks.');
|
|
288
|
+
} else {
|
|
289
|
+
console.log('\x1b[2m Enabled presets:\x1b[0m');
|
|
290
|
+
for (const desc of descriptions) {
|
|
291
|
+
console.log(` \x1b[32m+\x1b[0m ${desc}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
console.log();
|
|
295
|
+
|
|
296
|
+
// Show installation status
|
|
297
|
+
const settings = await loadClaudeSettings();
|
|
298
|
+
const installed = isGenieHookInstalled(settings);
|
|
299
|
+
const scriptExists = hookScriptExists();
|
|
300
|
+
|
|
301
|
+
console.log('\x1b[2m Installation status:\x1b[0m');
|
|
302
|
+
if (installed && scriptExists) {
|
|
303
|
+
console.log(' \x1b[32m+\x1b[0m Hooks installed in Claude Code');
|
|
304
|
+
console.log(' \x1b[32m+\x1b[0m Hook script exists');
|
|
305
|
+
} else if (installed && !scriptExists) {
|
|
306
|
+
console.log(' \x1b[33m!\x1b[0m Hook registered but script missing');
|
|
307
|
+
console.log(' Run \x1b[36mgenie hooks install --force\x1b[0m to fix');
|
|
308
|
+
} else if (!installed && scriptExists) {
|
|
309
|
+
console.log(' \x1b[33m!\x1b[0m Script exists but hook not registered');
|
|
310
|
+
console.log(' Run \x1b[36mgenie hooks install\x1b[0m to register');
|
|
311
|
+
} else {
|
|
312
|
+
console.log(' \x1b[33m-\x1b[0m Hooks not installed');
|
|
313
|
+
console.log(' Run \x1b[36mgenie hooks install\x1b[0m to install');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log();
|
|
317
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import {
|
|
4
|
+
detectSystem,
|
|
5
|
+
checkAllPrerequisites,
|
|
6
|
+
checkCommand,
|
|
7
|
+
getDistroDisplayName,
|
|
8
|
+
type SystemInfo,
|
|
9
|
+
type PrerequisiteStatus,
|
|
10
|
+
type PackageManager,
|
|
11
|
+
} from '../lib/system-detect.js';
|
|
12
|
+
import { genieConfigExists } from '../lib/genie-config.js';
|
|
13
|
+
import { setupCommand } from './setup.js';
|
|
14
|
+
|
|
15
|
+
export interface InstallOptions {
|
|
16
|
+
check?: boolean;
|
|
17
|
+
yes?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface InstallStrategy {
|
|
21
|
+
brew?: string;
|
|
22
|
+
apt?: string;
|
|
23
|
+
dnf?: string;
|
|
24
|
+
yum?: string;
|
|
25
|
+
pacman?: string;
|
|
26
|
+
all?: string;
|
|
27
|
+
none?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STRATEGIES: Record<string, InstallStrategy> = {
|
|
31
|
+
tmux: {
|
|
32
|
+
brew: 'brew install tmux',
|
|
33
|
+
apt: 'sudo apt update && sudo apt install -y tmux',
|
|
34
|
+
dnf: 'sudo dnf install -y tmux',
|
|
35
|
+
yum: 'sudo yum install -y tmux',
|
|
36
|
+
pacman: 'sudo pacman -S --noconfirm tmux',
|
|
37
|
+
none: null,
|
|
38
|
+
},
|
|
39
|
+
bun: {
|
|
40
|
+
all: 'curl -fsSL https://bun.sh/install | bash',
|
|
41
|
+
},
|
|
42
|
+
claude: {
|
|
43
|
+
all: 'npm install -g @anthropic-ai/claude-code',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function getInstallCommand(name: string, pm: PackageManager): string | null {
|
|
48
|
+
const strategy = STRATEGIES[name];
|
|
49
|
+
if (!strategy) return null;
|
|
50
|
+
|
|
51
|
+
// Check for universal installer first
|
|
52
|
+
if (strategy.all) return strategy.all;
|
|
53
|
+
|
|
54
|
+
// Check for package manager specific
|
|
55
|
+
if (pm !== 'none' && strategy[pm]) return strategy[pm];
|
|
56
|
+
|
|
57
|
+
// Check if there's a none/manual fallback
|
|
58
|
+
if (strategy.none !== undefined) return strategy.none;
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getManualInstructions(name: string): string {
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'tmux':
|
|
66
|
+
return 'Visit https://github.com/tmux/tmux/wiki/Installing for installation instructions';
|
|
67
|
+
case 'bun':
|
|
68
|
+
return 'Visit https://bun.sh for installation instructions';
|
|
69
|
+
case 'claude':
|
|
70
|
+
return 'Run: npm install -g @anthropic-ai/claude-code (requires Node.js/npm)';
|
|
71
|
+
default:
|
|
72
|
+
return `Search for "${name} install" for installation instructions`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runCommand(command: string): Promise<boolean> {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
console.log(`\x1b[2m$ ${command}\x1b[0m\n`);
|
|
79
|
+
|
|
80
|
+
const child = spawn('bash', ['-c', command], {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
child.on('close', (code) => {
|
|
86
|
+
resolve(code === 0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.on('error', (err) => {
|
|
90
|
+
console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
|
|
91
|
+
resolve(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printHeader() {
|
|
97
|
+
console.log();
|
|
98
|
+
console.log('\x1b[1m🔧 Genie Prerequisites Check\x1b[0m');
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printSystemInfo(system: SystemInfo) {
|
|
103
|
+
let osDisplay = system.os === 'macos' ? 'macOS' : 'Linux';
|
|
104
|
+
if (system.linuxDistro && system.linuxDistro !== 'unknown') {
|
|
105
|
+
osDisplay = `Linux (${getDistroDisplayName(system.linuxDistro)})`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`System: ${osDisplay} (${system.arch})`);
|
|
109
|
+
console.log(`Package Manager: ${system.preferredPM === 'none' ? 'none detected' : system.preferredPM}`);
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function printPrerequisiteStatus(prereqs: PrerequisiteStatus[]) {
|
|
114
|
+
console.log('Checking prerequisites...');
|
|
115
|
+
console.log();
|
|
116
|
+
|
|
117
|
+
for (const p of prereqs) {
|
|
118
|
+
if (p.installed) {
|
|
119
|
+
const versionInfo = p.version ? ` ${p.version}` : '';
|
|
120
|
+
const pathInfo = p.path ? `\x1b[2m (${p.path})\x1b[0m` : '';
|
|
121
|
+
console.log(` \x1b[32m✅\x1b[0m ${p.name}${versionInfo}${pathInfo}`);
|
|
122
|
+
} else if (p.required) {
|
|
123
|
+
console.log(` \x1b[31m❌\x1b[0m ${p.name} \x1b[31mnot found\x1b[0m`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` \x1b[33m⚠️\x1b[0m ${p.name} \x1b[33mnot found\x1b[0m \x1b[2m(optional)\x1b[0m`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function printSeparator() {
|
|
132
|
+
console.log('\x1b[2m' + '─'.repeat(40) + '\x1b[0m');
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function promptAndInstall(
|
|
137
|
+
prereq: PrerequisiteStatus,
|
|
138
|
+
command: string | null,
|
|
139
|
+
options: InstallOptions
|
|
140
|
+
): Promise<'installed' | 'skipped' | 'failed'> {
|
|
141
|
+
const typeLabel = prereq.required ? '\x1b[31mrequired\x1b[0m' : '\x1b[2moptional, recommended\x1b[0m';
|
|
142
|
+
|
|
143
|
+
if (!command) {
|
|
144
|
+
console.log(`\x1b[33mManual installation required for\x1b[0m ${prereq.name} (${typeLabel})`);
|
|
145
|
+
console.log(` ${getManualInstructions(prereq.name)}`);
|
|
146
|
+
console.log();
|
|
147
|
+
return 'skipped';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`Install \x1b[1m${prereq.name}\x1b[0m? (${typeLabel})`);
|
|
151
|
+
console.log(` Command: \x1b[36m${command}\x1b[0m`);
|
|
152
|
+
|
|
153
|
+
let proceed: boolean;
|
|
154
|
+
if (options.yes) {
|
|
155
|
+
proceed = true;
|
|
156
|
+
console.log('\x1b[2mAuto-approved with --yes\x1b[0m');
|
|
157
|
+
} else {
|
|
158
|
+
// Default to yes for required, no for optional
|
|
159
|
+
proceed = await confirm({
|
|
160
|
+
message: 'Proceed',
|
|
161
|
+
default: prereq.required,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!proceed) {
|
|
166
|
+
console.log(`\x1b[33mSkipped: ${prereq.name}\x1b[0m`);
|
|
167
|
+
console.log();
|
|
168
|
+
return 'skipped';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(`Installing ${prereq.name}...`);
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
const success = await runCommand(command);
|
|
176
|
+
|
|
177
|
+
if (success) {
|
|
178
|
+
// Verify installation
|
|
179
|
+
const check = await checkCommand(prereq.name);
|
|
180
|
+
if (check.exists) {
|
|
181
|
+
const versionInfo = check.version ? ` ${check.version}` : '';
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(`\x1b[32m✅ ${prereq.name}${versionInfo} installed\x1b[0m`);
|
|
184
|
+
console.log();
|
|
185
|
+
return 'installed';
|
|
186
|
+
} else {
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(`\x1b[33m⚠️ ${prereq.name} installed but not found in PATH\x1b[0m`);
|
|
189
|
+
console.log('\x1b[2m You may need to restart your shell or source your profile\x1b[0m');
|
|
190
|
+
console.log();
|
|
191
|
+
return 'installed';
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(`\x1b[31m❌ Failed to install ${prereq.name}\x1b[0m`);
|
|
196
|
+
console.log(`\x1b[2m ${getManualInstructions(prereq.name)}\x1b[0m`);
|
|
197
|
+
console.log();
|
|
198
|
+
return 'failed';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Prompt to run genie setup after successful installation
|
|
204
|
+
*/
|
|
205
|
+
async function promptForSetup(options: InstallOptions): Promise<void> {
|
|
206
|
+
// Skip if genie config already exists
|
|
207
|
+
if (genieConfigExists()) {
|
|
208
|
+
console.log('\x1b[2m✓ Genie hooks already configured (~/.genie/config.json)\x1b[0m');
|
|
209
|
+
console.log(' Run \x1b[36mgenie setup\x1b[0m to reconfigure.');
|
|
210
|
+
console.log();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log();
|
|
215
|
+
printSeparator();
|
|
216
|
+
console.log('\x1b[1m🧞 Configure Genie Hooks?\x1b[0m');
|
|
217
|
+
console.log();
|
|
218
|
+
console.log('\x1b[2mHooks let you control how AI tools execute - without wasting tokens!\x1b[0m');
|
|
219
|
+
console.log('\x1b[2mFor example, the "collaborative" hook routes all bash commands through\x1b[0m');
|
|
220
|
+
console.log('\x1b[2mtmux so you can watch the AI work in real-time.\x1b[0m');
|
|
221
|
+
console.log();
|
|
222
|
+
|
|
223
|
+
let runSetup: boolean;
|
|
224
|
+
if (options.yes) {
|
|
225
|
+
runSetup = true;
|
|
226
|
+
console.log('\x1b[2mAuto-approved with --yes\x1b[0m');
|
|
227
|
+
} else {
|
|
228
|
+
runSetup = await confirm({
|
|
229
|
+
message: 'Would you like to configure genie hooks now?',
|
|
230
|
+
default: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (runSetup) {
|
|
235
|
+
console.log();
|
|
236
|
+
await setupCommand();
|
|
237
|
+
} else {
|
|
238
|
+
console.log();
|
|
239
|
+
console.log('\x1b[2mSkipped. Run \x1b[0m\x1b[36mgenie setup\x1b[0m\x1b[2m anytime to configure hooks.\x1b[0m');
|
|
240
|
+
console.log();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function installCommand(options: InstallOptions): Promise<void> {
|
|
245
|
+
printHeader();
|
|
246
|
+
|
|
247
|
+
const system = await detectSystem();
|
|
248
|
+
printSystemInfo(system);
|
|
249
|
+
|
|
250
|
+
const prereqs = await checkAllPrerequisites();
|
|
251
|
+
printPrerequisiteStatus(prereqs);
|
|
252
|
+
|
|
253
|
+
const missing = prereqs.filter((p) => !p.installed);
|
|
254
|
+
const missingRequired = missing.filter((p) => p.required);
|
|
255
|
+
const missingOptional = missing.filter((p) => !p.required);
|
|
256
|
+
|
|
257
|
+
if (missing.length === 0) {
|
|
258
|
+
console.log('\x1b[32m✅ All prerequisites are installed!\x1b[0m');
|
|
259
|
+
console.log();
|
|
260
|
+
|
|
261
|
+
// Offer to run setup if not already configured
|
|
262
|
+
await promptForSetup(options);
|
|
263
|
+
|
|
264
|
+
console.log(`Run \x1b[36mterm --help\x1b[0m or \x1b[36mclaudio --help\x1b[0m to get started.`);
|
|
265
|
+
console.log();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(
|
|
270
|
+
`Missing: ${missingRequired.length} required, ${missingOptional.length} optional`
|
|
271
|
+
);
|
|
272
|
+
console.log();
|
|
273
|
+
|
|
274
|
+
if (options.check) {
|
|
275
|
+
if (missingRequired.length > 0) {
|
|
276
|
+
console.log('\x1b[31m❌ Missing required prerequisites\x1b[0m');
|
|
277
|
+
console.log(`Run \x1b[36mgenie install\x1b[0m to install them.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
printSeparator();
|
|
284
|
+
|
|
285
|
+
const results = {
|
|
286
|
+
installed: [] as string[],
|
|
287
|
+
skipped: [] as string[],
|
|
288
|
+
failed: [] as string[],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Install missing prerequisites
|
|
292
|
+
for (const prereq of missing) {
|
|
293
|
+
const command = getInstallCommand(prereq.name, system.preferredPM);
|
|
294
|
+
const result = await promptAndInstall(prereq, command, options);
|
|
295
|
+
|
|
296
|
+
if (result === 'installed') {
|
|
297
|
+
results.installed.push(prereq.name);
|
|
298
|
+
} else if (result === 'skipped') {
|
|
299
|
+
results.skipped.push(prereq.name);
|
|
300
|
+
} else {
|
|
301
|
+
results.failed.push(prereq.name);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (prereq !== missing[missing.length - 1]) {
|
|
305
|
+
printSeparator();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Print summary
|
|
310
|
+
printSeparator();
|
|
311
|
+
console.log('\x1b[1mSummary:\x1b[0m');
|
|
312
|
+
|
|
313
|
+
const requiredInstalled = results.installed.filter((name) =>
|
|
314
|
+
missingRequired.some((p) => p.name === name)
|
|
315
|
+
);
|
|
316
|
+
const requiredFailed = results.failed.filter((name) =>
|
|
317
|
+
missingRequired.some((p) => p.name === name)
|
|
318
|
+
);
|
|
319
|
+
const optionalSkipped = results.skipped.filter((name) =>
|
|
320
|
+
missingOptional.some((p) => p.name === name)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (requiredFailed.length > 0) {
|
|
324
|
+
console.log(`\x1b[31m ❌ ${requiredFailed.length} required failed: ${requiredFailed.join(', ')}\x1b[0m`);
|
|
325
|
+
} else if (requiredInstalled.length > 0 || missingRequired.length === 0) {
|
|
326
|
+
console.log('\x1b[32m ✅ All required prerequisites installed\x1b[0m');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (results.installed.length > 0) {
|
|
330
|
+
console.log(`\x1b[32m ✅ Installed: ${results.installed.join(', ')}\x1b[0m`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (optionalSkipped.length > 0) {
|
|
334
|
+
console.log(`\x1b[33m ⚠️ ${optionalSkipped.length} optional skipped: ${optionalSkipped.join(', ')}\x1b[0m`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log();
|
|
338
|
+
|
|
339
|
+
if (requiredFailed.length === 0) {
|
|
340
|
+
// Offer to run setup after successful installation
|
|
341
|
+
await promptForSetup(options);
|
|
342
|
+
|
|
343
|
+
console.log(`Run \x1b[36mterm --help\x1b[0m or \x1b[36mclaudio --help\x1b[0m to get started.`);
|
|
344
|
+
} else {
|
|
345
|
+
console.log('\x1b[31mSome required prerequisites could not be installed.\x1b[0m');
|
|
346
|
+
console.log('Please install them manually and run this command again.');
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log();
|
|
351
|
+
}
|