@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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genie Setup Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive wizard for configuring genie hooks and settings.
|
|
5
|
+
* Teaches users about hooks and lets them choose which to enable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
9
|
+
import {
|
|
10
|
+
loadGenieConfig,
|
|
11
|
+
saveGenieConfig,
|
|
12
|
+
genieConfigExists,
|
|
13
|
+
getGenieConfigPath,
|
|
14
|
+
} from '../lib/genie-config.js';
|
|
15
|
+
import {
|
|
16
|
+
GenieConfig,
|
|
17
|
+
PresetName,
|
|
18
|
+
PRESET_DESCRIPTIONS,
|
|
19
|
+
} from '../types/genie-config.js';
|
|
20
|
+
import { describeEnabledHooks } from '../lib/hooks/index.js';
|
|
21
|
+
import { installShortcuts, isShortcutsInstalled } from '../term-commands/shortcuts.js';
|
|
22
|
+
import { homedir } from 'os';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Print the header banner
|
|
27
|
+
*/
|
|
28
|
+
function printHeader(): void {
|
|
29
|
+
console.log();
|
|
30
|
+
console.log('\x1b[1m\x1b[36m' + '╔' + '═'.repeat(62) + '╗' + '\x1b[0m');
|
|
31
|
+
console.log('\x1b[1m\x1b[36m' + '║ ' + '\x1b[0m\x1b[1m🧞 Genie Setup - Configure Your AI Assistant' + ' '.repeat(16) + '\x1b[36m║\x1b[0m');
|
|
32
|
+
console.log('\x1b[1m\x1b[36m' + '╚' + '═'.repeat(62) + '╝' + '\x1b[0m');
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Print the about hooks section
|
|
38
|
+
*/
|
|
39
|
+
function printAboutHooks(): void {
|
|
40
|
+
console.log('\x1b[1m📚 ABOUT HOOKS\x1b[0m');
|
|
41
|
+
console.log('\x1b[2mHooks let you control how the AI uses tools - without wasting\x1b[0m');
|
|
42
|
+
console.log('\x1b[2mtokens on prompts! Instead of telling the AI "please use term",\x1b[0m');
|
|
43
|
+
console.log('\x1b[2mhooks automatically enforce the behavior.\x1b[0m');
|
|
44
|
+
console.log();
|
|
45
|
+
console.log('\x1b[2m' + '─'.repeat(64) + '\x1b[0m');
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Print available presets in a nice format
|
|
51
|
+
*/
|
|
52
|
+
function printPresetDescriptions(): void {
|
|
53
|
+
console.log('\x1b[1m🔧 AVAILABLE HOOK PRESETS\x1b[0m');
|
|
54
|
+
console.log();
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < PRESET_DESCRIPTIONS.length; i++) {
|
|
57
|
+
const preset = PRESET_DESCRIPTIONS[i];
|
|
58
|
+
const num = i + 1;
|
|
59
|
+
const recommended = preset.recommended ? ' \x1b[32m(Recommended)\x1b[0m' : '';
|
|
60
|
+
|
|
61
|
+
console.log(`\x1b[1m${num}. ${preset.title}\x1b[0m${recommended}`);
|
|
62
|
+
console.log(` ├─ What: ${preset.what}`);
|
|
63
|
+
console.log(` ├─ Why: ${preset.why}`);
|
|
64
|
+
console.log(` └─ How: ${preset.how}`);
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('\x1b[2m' + '─'.repeat(64) + '\x1b[0m');
|
|
69
|
+
console.log();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format preset choice for checkbox
|
|
74
|
+
*/
|
|
75
|
+
function formatPresetChoice(preset: typeof PRESET_DESCRIPTIONS[0], enabled: boolean): {
|
|
76
|
+
name: string;
|
|
77
|
+
value: PresetName;
|
|
78
|
+
checked: boolean;
|
|
79
|
+
} {
|
|
80
|
+
const recommended = preset.recommended ? ' \x1b[32m(Recommended)\x1b[0m' : '';
|
|
81
|
+
return {
|
|
82
|
+
name: `${preset.title}${recommended} - ${preset.what}`,
|
|
83
|
+
value: preset.name,
|
|
84
|
+
checked: enabled,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Print success message
|
|
90
|
+
*/
|
|
91
|
+
function printSuccess(configPath: string): void {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(`\x1b[32m✓ Configuration saved to ${configPath}\x1b[0m`);
|
|
94
|
+
console.log();
|
|
95
|
+
console.log('\x1b[1m💡 TIP:\x1b[0m Claudio will automatically use these hooks.');
|
|
96
|
+
console.log(' Run: \x1b[36mclaudio launch\x1b[0m');
|
|
97
|
+
console.log(' Watch: \x1b[36mtmux attach -t genie\x1b[0m');
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Print current configuration
|
|
103
|
+
*/
|
|
104
|
+
function printCurrentConfig(config: GenieConfig): void {
|
|
105
|
+
const descriptions = describeEnabledHooks(config);
|
|
106
|
+
|
|
107
|
+
if (descriptions.length === 0) {
|
|
108
|
+
console.log('\x1b[33mNo hooks currently enabled.\x1b[0m');
|
|
109
|
+
} else {
|
|
110
|
+
console.log('\x1b[1mCurrently enabled hooks:\x1b[0m');
|
|
111
|
+
for (const desc of descriptions) {
|
|
112
|
+
console.log(` • ${desc}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Configure sandboxed preset paths
|
|
120
|
+
*/
|
|
121
|
+
async function configureSandbox(config: GenieConfig): Promise<void> {
|
|
122
|
+
const currentPaths = config.hooks.sandboxed?.allowedPaths || ['~/projects', '/tmp'];
|
|
123
|
+
|
|
124
|
+
console.log('\x1b[1mSandbox Configuration\x1b[0m');
|
|
125
|
+
console.log('\x1b[2mEnter paths where the AI can access files (comma-separated):\x1b[0m');
|
|
126
|
+
|
|
127
|
+
const pathsInput = await input({
|
|
128
|
+
message: 'Allowed paths:',
|
|
129
|
+
default: currentPaths.join(', '),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const allowedPaths = pathsInput
|
|
133
|
+
.split(',')
|
|
134
|
+
.map((p) => p.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
|
|
137
|
+
config.hooks.sandboxed = { allowedPaths };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Configure audited preset log path
|
|
142
|
+
*/
|
|
143
|
+
async function configureAudit(config: GenieConfig): Promise<void> {
|
|
144
|
+
const currentPath = config.hooks.audited?.logPath || '~/.genie/audit.log';
|
|
145
|
+
|
|
146
|
+
console.log('\x1b[1mAudit Configuration\x1b[0m');
|
|
147
|
+
|
|
148
|
+
const logPath = await input({
|
|
149
|
+
message: 'Audit log path:',
|
|
150
|
+
default: currentPath,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
config.hooks.audited = { logPath };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Main setup wizard
|
|
158
|
+
*/
|
|
159
|
+
export async function setupCommand(): Promise<void> {
|
|
160
|
+
printHeader();
|
|
161
|
+
printAboutHooks();
|
|
162
|
+
printPresetDescriptions();
|
|
163
|
+
|
|
164
|
+
// Load existing config or defaults
|
|
165
|
+
const config = await loadGenieConfig();
|
|
166
|
+
const existingPresets = new Set(config.hooks.enabled);
|
|
167
|
+
|
|
168
|
+
// If config exists, show current state
|
|
169
|
+
if (genieConfigExists()) {
|
|
170
|
+
console.log('\x1b[1m📋 CURRENT CONFIGURATION\x1b[0m');
|
|
171
|
+
printCurrentConfig(config);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build choices for checkbox
|
|
175
|
+
const choices = PRESET_DESCRIPTIONS.map((preset) =>
|
|
176
|
+
formatPresetChoice(preset, existingPresets.has(preset.name))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Prompt for preset selection
|
|
180
|
+
const selectedPresets = await checkbox<PresetName>({
|
|
181
|
+
message: 'Select hooks to enable (space to toggle, enter to confirm):',
|
|
182
|
+
choices,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Update config with selections
|
|
186
|
+
config.hooks.enabled = selectedPresets;
|
|
187
|
+
|
|
188
|
+
// If sandboxed is enabled, configure it
|
|
189
|
+
if (selectedPresets.includes('sandboxed')) {
|
|
190
|
+
console.log();
|
|
191
|
+
await configureSandbox(config);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If audited is enabled, optionally configure it
|
|
195
|
+
if (selectedPresets.includes('audited')) {
|
|
196
|
+
console.log();
|
|
197
|
+
const customAudit = await confirm({
|
|
198
|
+
message: 'Customize audit log path?',
|
|
199
|
+
default: false,
|
|
200
|
+
});
|
|
201
|
+
if (customAudit) {
|
|
202
|
+
await configureAudit(config);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Save configuration
|
|
207
|
+
await saveGenieConfig(config);
|
|
208
|
+
|
|
209
|
+
printSuccess(getGenieConfigPath());
|
|
210
|
+
|
|
211
|
+
// Show what was configured
|
|
212
|
+
if (selectedPresets.length > 0) {
|
|
213
|
+
console.log('\x1b[1mEnabled hooks:\x1b[0m');
|
|
214
|
+
const descriptions = describeEnabledHooks(config);
|
|
215
|
+
for (const desc of descriptions) {
|
|
216
|
+
console.log(` \x1b[32m✓\x1b[0m ${desc}`);
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Offer to install tmux shortcuts
|
|
222
|
+
await offerShortcutsInstall();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Offer to install tmux keyboard shortcuts
|
|
227
|
+
*/
|
|
228
|
+
async function offerShortcutsInstall(): Promise<void> {
|
|
229
|
+
// Check if already installed
|
|
230
|
+
const home = homedir();
|
|
231
|
+
const tmuxConf = join(home, '.tmux.conf');
|
|
232
|
+
|
|
233
|
+
if (isShortcutsInstalled(tmuxConf)) {
|
|
234
|
+
console.log('\x1b[2m✓ Tmux shortcuts already installed\x1b[0m');
|
|
235
|
+
console.log();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log('\x1b[2m' + '─'.repeat(64) + '\x1b[0m');
|
|
240
|
+
console.log();
|
|
241
|
+
console.log('\x1b[1m⌨️ KEYBOARD SHORTCUTS\x1b[0m');
|
|
242
|
+
console.log('\x1b[2mOptional: Install Warp-like tmux shortcuts for quick navigation:\x1b[0m');
|
|
243
|
+
console.log(' • Ctrl+T → New tab (window)');
|
|
244
|
+
console.log(' • Ctrl+S → Vertical split');
|
|
245
|
+
console.log(' • Alt+S → Horizontal split');
|
|
246
|
+
console.log();
|
|
247
|
+
|
|
248
|
+
const installShortcutsChoice = await confirm({
|
|
249
|
+
message: 'Install tmux keyboard shortcuts?',
|
|
250
|
+
default: false,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (installShortcutsChoice) {
|
|
254
|
+
console.log();
|
|
255
|
+
await installShortcuts();
|
|
256
|
+
} else {
|
|
257
|
+
console.log();
|
|
258
|
+
console.log('\x1b[2mSkipped. Run \x1b[0m\x1b[36mgenie shortcuts install\x1b[0m\x1b[2m later to add them.\x1b[0m');
|
|
259
|
+
console.log();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Quick setup with recommended defaults
|
|
265
|
+
*/
|
|
266
|
+
export async function quickSetupCommand(): Promise<void> {
|
|
267
|
+
console.log('\x1b[1m🧞 Quick Setup - Using recommended defaults\x1b[0m');
|
|
268
|
+
console.log();
|
|
269
|
+
|
|
270
|
+
const config = await loadGenieConfig();
|
|
271
|
+
|
|
272
|
+
// Enable recommended presets
|
|
273
|
+
config.hooks.enabled = ['collaborative', 'audited'];
|
|
274
|
+
|
|
275
|
+
await saveGenieConfig(config);
|
|
276
|
+
|
|
277
|
+
console.log('\x1b[32m✓ Enabled:\x1b[0m collaborative, audited');
|
|
278
|
+
console.log();
|
|
279
|
+
console.log('Run \x1b[36mgenie setup\x1b[0m to customize further.');
|
|
280
|
+
console.log();
|
|
281
|
+
}
|
|
282
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genie Shortcuts Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands to install, uninstall, and show keyboard shortcuts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
displayShortcuts,
|
|
9
|
+
installShortcuts,
|
|
10
|
+
uninstallShortcuts,
|
|
11
|
+
isShortcutsInstalled,
|
|
12
|
+
} from '../term-commands/shortcuts.js';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Show shortcuts info (default action)
|
|
19
|
+
*/
|
|
20
|
+
export async function shortcutsShowCommand(): Promise<void> {
|
|
21
|
+
displayShortcuts();
|
|
22
|
+
|
|
23
|
+
// Also show installation status
|
|
24
|
+
const home = homedir();
|
|
25
|
+
const tmuxConf = join(home, '.tmux.conf');
|
|
26
|
+
const zshrc = join(home, '.zshrc');
|
|
27
|
+
const bashrc = join(home, '.bashrc');
|
|
28
|
+
|
|
29
|
+
console.log('Installation status:');
|
|
30
|
+
|
|
31
|
+
if (isShortcutsInstalled(tmuxConf)) {
|
|
32
|
+
console.log(' \x1b[32m✓\x1b[0m tmux.conf');
|
|
33
|
+
} else {
|
|
34
|
+
console.log(' \x1b[33m-\x1b[0m tmux.conf');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shellRc = existsSync(zshrc) ? zshrc : bashrc;
|
|
38
|
+
if (isShortcutsInstalled(shellRc)) {
|
|
39
|
+
console.log(` \x1b[32m✓\x1b[0m ${shellRc.replace(home, '~')}`);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(` \x1b[33m-\x1b[0m ${shellRc.replace(home, '~')}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log();
|
|
45
|
+
console.log('Run \x1b[36mgenie shortcuts install\x1b[0m to install shortcuts.');
|
|
46
|
+
console.log('Run \x1b[36mgenie shortcuts uninstall\x1b[0m to remove shortcuts.');
|
|
47
|
+
console.log();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Install shortcuts to config files
|
|
52
|
+
*/
|
|
53
|
+
export async function shortcutsInstallCommand(): Promise<void> {
|
|
54
|
+
await installShortcuts();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Uninstall shortcuts from config files
|
|
59
|
+
*/
|
|
60
|
+
export async function shortcutsUninstallCommand(): Promise<void> {
|
|
61
|
+
await uninstallShortcuts();
|
|
62
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { mkdir, copyFile, chmod } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
|
|
7
|
+
const GENIE_HOME = process.env.GENIE_HOME || join(homedir(), '.genie');
|
|
8
|
+
const GENIE_SRC = join(GENIE_HOME, 'src');
|
|
9
|
+
const GENIE_BIN = join(GENIE_HOME, 'bin');
|
|
10
|
+
const LOCAL_BIN = join(homedir(), '.local', 'bin');
|
|
11
|
+
|
|
12
|
+
function log(message: string): void {
|
|
13
|
+
console.log(`\x1b[32m▸\x1b[0m ${message}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function success(message: string): void {
|
|
17
|
+
console.log(`\x1b[32m✔\x1b[0m ${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function error(message: string): void {
|
|
21
|
+
console.log(`\x1b[31m✖\x1b[0m ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function warn(message: string): void {
|
|
25
|
+
console.log(`\x1b[33m⚠\x1b[0m ${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function runCommand(command: string, args: string[], cwd?: string): Promise<{ success: boolean; output: string }> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const output: string[] = [];
|
|
31
|
+
|
|
32
|
+
const child = spawn(command, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.stdout?.on('data', (data) => {
|
|
39
|
+
const str = data.toString();
|
|
40
|
+
output.push(str);
|
|
41
|
+
process.stdout.write(str);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.stderr?.on('data', (data) => {
|
|
45
|
+
const str = data.toString();
|
|
46
|
+
output.push(str);
|
|
47
|
+
process.stderr.write(str);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.on('close', (code) => {
|
|
51
|
+
resolve({ success: code === 0, output: output.join('') });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
child.on('error', (err) => {
|
|
55
|
+
error(err.message);
|
|
56
|
+
resolve({ success: false, output: err.message });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getGitInfo(cwd: string): Promise<{ branch: string; commit: string; commitDate: string } | null> {
|
|
62
|
+
try {
|
|
63
|
+
const branchResult = await runCommandSilent('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
64
|
+
const commitResult = await runCommandSilent('git', ['rev-parse', '--short', 'HEAD'], cwd);
|
|
65
|
+
const dateResult = await runCommandSilent('git', ['log', '-1', '--format=%ci'], cwd);
|
|
66
|
+
|
|
67
|
+
if (branchResult.success && commitResult.success && dateResult.success) {
|
|
68
|
+
return {
|
|
69
|
+
branch: branchResult.output.trim(),
|
|
70
|
+
commit: commitResult.output.trim(),
|
|
71
|
+
commitDate: dateResult.output.trim().split(' ')[0], // Just the date part
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore errors
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runCommandSilent(command: string, args: string[], cwd?: string): Promise<{ success: boolean; output: string }> {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const output: string[] = [];
|
|
83
|
+
|
|
84
|
+
const child = spawn(command, args, {
|
|
85
|
+
cwd,
|
|
86
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.stdout?.on('data', (data) => {
|
|
90
|
+
output.push(data.toString());
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.stderr?.on('data', (data) => {
|
|
94
|
+
output.push(data.toString());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
child.on('close', (code) => {
|
|
98
|
+
resolve({ success: code === 0, output: output.join('') });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.on('error', (err) => {
|
|
102
|
+
resolve({ success: false, output: err.message });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function symlinkOrCopy(src: string, dest: string): Promise<void> {
|
|
108
|
+
const { symlink, unlink } = await import('fs/promises');
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Remove existing symlink/file if present
|
|
112
|
+
if (existsSync(dest)) {
|
|
113
|
+
await unlink(dest);
|
|
114
|
+
}
|
|
115
|
+
await symlink(src, dest);
|
|
116
|
+
} catch {
|
|
117
|
+
// Fallback to copy if symlink fails
|
|
118
|
+
await copyFile(src, dest);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function updateCommand(): Promise<void> {
|
|
123
|
+
console.log();
|
|
124
|
+
console.log('\x1b[1m🧞 Genie CLI Update\x1b[0m');
|
|
125
|
+
console.log('\x1b[2m────────────────────────────────────\x1b[0m');
|
|
126
|
+
console.log();
|
|
127
|
+
|
|
128
|
+
// Check if installed via install.sh (has ~/.genie/src)
|
|
129
|
+
if (!existsSync(GENIE_SRC) || !existsSync(join(GENIE_SRC, '.git'))) {
|
|
130
|
+
error('Genie CLI was not installed via install.sh');
|
|
131
|
+
console.log();
|
|
132
|
+
console.log('To install Genie CLI properly, run:');
|
|
133
|
+
console.log('\x1b[36m curl -fsSL https://raw.githubusercontent.com/namastexlabs/genie-cli/main/install.sh | bash\x1b[0m');
|
|
134
|
+
console.log();
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get current version info before update
|
|
139
|
+
const beforeInfo = await getGitInfo(GENIE_SRC);
|
|
140
|
+
if (beforeInfo) {
|
|
141
|
+
console.log(`Current: \x1b[2m${beforeInfo.branch}@${beforeInfo.commit} (${beforeInfo.commitDate})\x1b[0m`);
|
|
142
|
+
console.log();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Step 1: Fetch and reset to origin/main
|
|
146
|
+
log('Fetching latest changes...');
|
|
147
|
+
const fetchResult = await runCommand('git', ['fetch', 'origin'], GENIE_SRC);
|
|
148
|
+
if (!fetchResult.success) {
|
|
149
|
+
error('Failed to fetch from origin');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log('Resetting to origin/main...');
|
|
154
|
+
const resetResult = await runCommand('git', ['reset', '--hard', 'origin/main'], GENIE_SRC);
|
|
155
|
+
if (!resetResult.success) {
|
|
156
|
+
error('Failed to reset to origin/main');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
// Get new version info
|
|
162
|
+
const afterInfo = await getGitInfo(GENIE_SRC);
|
|
163
|
+
|
|
164
|
+
// Check if anything changed
|
|
165
|
+
if (beforeInfo && afterInfo && beforeInfo.commit === afterInfo.commit) {
|
|
166
|
+
success('Already up to date!');
|
|
167
|
+
console.log();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 2: Install dependencies
|
|
172
|
+
log('Installing dependencies...');
|
|
173
|
+
const installResult = await runCommand('bun', ['install'], GENIE_SRC);
|
|
174
|
+
if (!installResult.success) {
|
|
175
|
+
error('Failed to install dependencies');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
|
|
180
|
+
// Step 3: Build
|
|
181
|
+
log('Building...');
|
|
182
|
+
const buildResult = await runCommand('bun', ['run', 'build'], GENIE_SRC);
|
|
183
|
+
if (!buildResult.success) {
|
|
184
|
+
error('Failed to build');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
// Step 4: Copy binaries and update symlinks
|
|
190
|
+
log('Installing binaries...');
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await mkdir(GENIE_BIN, { recursive: true });
|
|
194
|
+
await mkdir(LOCAL_BIN, { recursive: true });
|
|
195
|
+
|
|
196
|
+
const binaries = ['genie.js', 'term.js', 'claudio.js'];
|
|
197
|
+
const names = ['genie', 'term', 'claudio'];
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < binaries.length; i++) {
|
|
200
|
+
const src = join(GENIE_SRC, 'dist', binaries[i]);
|
|
201
|
+
const binDest = join(GENIE_BIN, binaries[i]);
|
|
202
|
+
const linkDest = join(LOCAL_BIN, names[i]);
|
|
203
|
+
|
|
204
|
+
// Copy to GENIE_BIN
|
|
205
|
+
await copyFile(src, binDest);
|
|
206
|
+
await chmod(binDest, 0o755);
|
|
207
|
+
|
|
208
|
+
// Symlink to LOCAL_BIN
|
|
209
|
+
await symlinkOrCopy(binDest, linkDest);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
success('Binaries installed');
|
|
213
|
+
} catch (err) {
|
|
214
|
+
error(`Failed to install binaries: ${err}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Print success
|
|
219
|
+
console.log();
|
|
220
|
+
console.log('\x1b[2m────────────────────────────────────\x1b[0m');
|
|
221
|
+
success('Genie CLI updated successfully!');
|
|
222
|
+
console.log();
|
|
223
|
+
|
|
224
|
+
if (afterInfo) {
|
|
225
|
+
console.log(`Version: \x1b[36m${afterInfo.branch}@${afterInfo.commit}\x1b[0m (${afterInfo.commitDate})`);
|
|
226
|
+
console.log();
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/genie.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { installCommand } from './genie-commands/install.js';
|
|
5
|
+
import { setupCommand, quickSetupCommand } from './genie-commands/setup.js';
|
|
6
|
+
import { updateCommand } from './genie-commands/update.js';
|
|
7
|
+
import {
|
|
8
|
+
showHooksCommand,
|
|
9
|
+
installHooksCommand,
|
|
10
|
+
uninstallHooksCommand,
|
|
11
|
+
testHooksCommand,
|
|
12
|
+
} from './genie-commands/hooks.js';
|
|
13
|
+
import {
|
|
14
|
+
shortcutsShowCommand,
|
|
15
|
+
shortcutsInstallCommand,
|
|
16
|
+
shortcutsUninstallCommand,
|
|
17
|
+
} from './genie-commands/shortcuts.js';
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('genie')
|
|
23
|
+
.description('Genie CLI - Setup and utilities for AI-assisted development')
|
|
24
|
+
.version('0.1.0');
|
|
25
|
+
|
|
26
|
+
// Install command - check/install prerequisites
|
|
27
|
+
program
|
|
28
|
+
.command('install')
|
|
29
|
+
.description('Verify and install prerequisites')
|
|
30
|
+
.option('--check', 'Only check prerequisites, do not install')
|
|
31
|
+
.option('--yes', 'Auto-approve all installations')
|
|
32
|
+
.action(installCommand);
|
|
33
|
+
|
|
34
|
+
// Setup command - interactive hook configuration
|
|
35
|
+
program
|
|
36
|
+
.command('setup')
|
|
37
|
+
.description('Configure hooks and settings (interactive wizard)')
|
|
38
|
+
.option('--quick', 'Use recommended defaults without prompts')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
if (options.quick) {
|
|
41
|
+
await quickSetupCommand();
|
|
42
|
+
} else {
|
|
43
|
+
await setupCommand();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Update command - pull latest and rebuild
|
|
48
|
+
program
|
|
49
|
+
.command('update')
|
|
50
|
+
.description('Update Genie CLI to the latest version')
|
|
51
|
+
.action(updateCommand);
|
|
52
|
+
|
|
53
|
+
// Hooks command group - manage Claude Code hooks
|
|
54
|
+
const hooks = program
|
|
55
|
+
.command('hooks')
|
|
56
|
+
.description('Manage Claude Code hooks');
|
|
57
|
+
|
|
58
|
+
// Make 'show' the default action for bare `genie hooks`
|
|
59
|
+
hooks.action(showHooksCommand);
|
|
60
|
+
|
|
61
|
+
hooks
|
|
62
|
+
.command('show')
|
|
63
|
+
.description('Show current hook configuration')
|
|
64
|
+
.action(showHooksCommand);
|
|
65
|
+
|
|
66
|
+
hooks
|
|
67
|
+
.command('install')
|
|
68
|
+
.description('Install hooks into Claude Code')
|
|
69
|
+
.option('--force', 'Overwrite existing hooks')
|
|
70
|
+
.action(installHooksCommand);
|
|
71
|
+
|
|
72
|
+
hooks
|
|
73
|
+
.command('uninstall')
|
|
74
|
+
.description('Remove hooks from Claude Code')
|
|
75
|
+
.option('--keep-script', 'Keep the hook script file')
|
|
76
|
+
.action(uninstallHooksCommand);
|
|
77
|
+
|
|
78
|
+
hooks
|
|
79
|
+
.command('test')
|
|
80
|
+
.description('Test the hook script')
|
|
81
|
+
.action(testHooksCommand);
|
|
82
|
+
|
|
83
|
+
// Shortcuts command group - manage tmux keyboard shortcuts
|
|
84
|
+
const shortcuts = program
|
|
85
|
+
.command('shortcuts')
|
|
86
|
+
.description('Manage tmux keyboard shortcuts');
|
|
87
|
+
|
|
88
|
+
// Make 'show' the default action for bare `genie shortcuts`
|
|
89
|
+
shortcuts.action(shortcutsShowCommand);
|
|
90
|
+
|
|
91
|
+
shortcuts
|
|
92
|
+
.command('show')
|
|
93
|
+
.description('Show available shortcuts and installation status')
|
|
94
|
+
.action(shortcutsShowCommand);
|
|
95
|
+
|
|
96
|
+
shortcuts
|
|
97
|
+
.command('install')
|
|
98
|
+
.description('Install shortcuts to config files (~/.tmux.conf, shell rc)')
|
|
99
|
+
.action(shortcutsInstallCommand);
|
|
100
|
+
|
|
101
|
+
shortcuts
|
|
102
|
+
.command('uninstall')
|
|
103
|
+
.description('Remove shortcuts from config files')
|
|
104
|
+
.action(shortcutsUninstallCommand);
|
|
105
|
+
|
|
106
|
+
program.parse();
|