@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.
Files changed (59) hide show
  1. package/.github/workflows/publish.yml +26 -0
  2. package/.worktrees/.metadata.json +3 -0
  3. package/README.md +532 -0
  4. package/bun.lock +101 -0
  5. package/dist/claudio.js +76 -0
  6. package/dist/genie.js +201 -0
  7. package/dist/term.js +136 -0
  8. package/install.sh +351 -0
  9. package/package.json +37 -0
  10. package/scripts/version.ts +48 -0
  11. package/src/claudio.ts +128 -0
  12. package/src/commands/launch.ts +245 -0
  13. package/src/commands/models.ts +43 -0
  14. package/src/commands/profiles.ts +95 -0
  15. package/src/commands/setup.ts +5 -0
  16. package/src/genie-commands/hooks.ts +317 -0
  17. package/src/genie-commands/install.ts +351 -0
  18. package/src/genie-commands/setup.ts +282 -0
  19. package/src/genie-commands/shortcuts.ts +62 -0
  20. package/src/genie-commands/update.ts +228 -0
  21. package/src/genie.ts +106 -0
  22. package/src/lib/api-client.ts +109 -0
  23. package/src/lib/claude-settings.ts +252 -0
  24. package/src/lib/config.ts +109 -0
  25. package/src/lib/genie-config.ts +164 -0
  26. package/src/lib/hook-manager.ts +130 -0
  27. package/src/lib/hook-script.ts +256 -0
  28. package/src/lib/hooks/compose.ts +72 -0
  29. package/src/lib/hooks/index.ts +163 -0
  30. package/src/lib/hooks/presets/audited.ts +191 -0
  31. package/src/lib/hooks/presets/collaborative.ts +143 -0
  32. package/src/lib/hooks/presets/sandboxed.ts +153 -0
  33. package/src/lib/hooks/presets/supervised.ts +66 -0
  34. package/src/lib/hooks/utils/escape.ts +46 -0
  35. package/src/lib/log-reader.ts +213 -0
  36. package/src/lib/picker.ts +62 -0
  37. package/src/lib/session-metadata.ts +58 -0
  38. package/src/lib/system-detect.ts +185 -0
  39. package/src/lib/tmux.ts +410 -0
  40. package/src/lib/version.ts +15 -0
  41. package/src/lib/wizard.ts +104 -0
  42. package/src/lib/worktree.ts +362 -0
  43. package/src/term-commands/attach.ts +23 -0
  44. package/src/term-commands/exec.ts +34 -0
  45. package/src/term-commands/hook.ts +42 -0
  46. package/src/term-commands/ls.ts +33 -0
  47. package/src/term-commands/new.ts +73 -0
  48. package/src/term-commands/pane.ts +81 -0
  49. package/src/term-commands/read.ts +70 -0
  50. package/src/term-commands/rm.ts +47 -0
  51. package/src/term-commands/send.ts +34 -0
  52. package/src/term-commands/shortcuts.ts +355 -0
  53. package/src/term-commands/split.ts +87 -0
  54. package/src/term-commands/status.ts +116 -0
  55. package/src/term-commands/window.ts +72 -0
  56. package/src/term.ts +192 -0
  57. package/src/types/config.ts +17 -0
  58. package/src/types/genie-config.ts +104 -0
  59. 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();