@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,70 @@
|
|
|
1
|
+
import * as logReader from '../lib/log-reader.js';
|
|
2
|
+
|
|
3
|
+
export interface ReadOptions {
|
|
4
|
+
lines?: string;
|
|
5
|
+
from?: string;
|
|
6
|
+
to?: string;
|
|
7
|
+
range?: string;
|
|
8
|
+
search?: string;
|
|
9
|
+
grep?: string;
|
|
10
|
+
follow?: boolean;
|
|
11
|
+
all?: boolean;
|
|
12
|
+
reverse?: boolean;
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readSessionLogs(sessionName: string, options: ReadOptions): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
// Parse options
|
|
19
|
+
const readOptions: logReader.ReadOptions = {
|
|
20
|
+
lines: options.lines ? parseInt(options.lines, 10) : 100,
|
|
21
|
+
from: options.from ? parseInt(options.from, 10) : undefined,
|
|
22
|
+
to: options.to ? parseInt(options.to, 10) : undefined,
|
|
23
|
+
range: options.range,
|
|
24
|
+
search: options.search,
|
|
25
|
+
grep: options.grep,
|
|
26
|
+
follow: options.follow,
|
|
27
|
+
all: options.all,
|
|
28
|
+
reverse: options.reverse,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Handle follow mode
|
|
32
|
+
if (options.follow) {
|
|
33
|
+
console.log(`Following session "${sessionName}" (Ctrl+C to stop)...`);
|
|
34
|
+
console.log('');
|
|
35
|
+
|
|
36
|
+
const stopFollowing = await logReader.followSessionLogs(sessionName, (line) => {
|
|
37
|
+
console.log(line);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Handle Ctrl+C
|
|
41
|
+
process.on('SIGINT', () => {
|
|
42
|
+
stopFollowing();
|
|
43
|
+
console.log('\nStopped following');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Keep process running
|
|
48
|
+
await new Promise(() => {});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Regular read mode
|
|
53
|
+
const content = await logReader.readSessionLogs(sessionName, readOptions);
|
|
54
|
+
|
|
55
|
+
if (options.json) {
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
session: sessionName,
|
|
59
|
+
lineCount: lines.length,
|
|
60
|
+
content: lines,
|
|
61
|
+
}, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(content);
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
console.error(`Error reading session logs: ${error.message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import * as tmux from '../lib/tmux.js';
|
|
3
|
+
import { createWorktreeManager } from '../lib/worktree.js';
|
|
4
|
+
import { loadSessionMetadata, deleteSessionMetadata } from '../lib/session-metadata.js';
|
|
5
|
+
|
|
6
|
+
export interface RemoveSessionOptions {
|
|
7
|
+
keepWorktree?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function removeSession(name: string, options: RemoveSessionOptions = {}): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
// Check if session exists
|
|
13
|
+
const session = await tmux.findSessionByName(name);
|
|
14
|
+
if (!session) {
|
|
15
|
+
console.error(`❌ Session "${name}" not found`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if session has associated worktree
|
|
20
|
+
const metadata = await loadSessionMetadata(name);
|
|
21
|
+
|
|
22
|
+
// Kill session
|
|
23
|
+
await tmux.killSession(session.id);
|
|
24
|
+
|
|
25
|
+
// Remove worktree unless --keep-worktree
|
|
26
|
+
if (metadata?.worktreePath && metadata?.workspace && !options.keepWorktree) {
|
|
27
|
+
const manager = createWorktreeManager({
|
|
28
|
+
baseDir: join(metadata.workspace, '.worktrees'),
|
|
29
|
+
repoPath: metadata.workspace
|
|
30
|
+
});
|
|
31
|
+
await manager.removeWorktree(name);
|
|
32
|
+
console.log(`✅ Session "${name}" and worktree removed`);
|
|
33
|
+
} else if (metadata?.worktreePath && options.keepWorktree) {
|
|
34
|
+
console.log(`✅ Session "${name}" removed (worktree kept at ${metadata.worktreePath})`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log(`✅ Session "${name}" removed`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Clean up metadata
|
|
40
|
+
if (metadata) {
|
|
41
|
+
await deleteSessionMetadata(name);
|
|
42
|
+
}
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
console.error(`❌ Error removing session: ${error.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as tmux from '../lib/tmux.js';
|
|
2
|
+
|
|
3
|
+
export async function sendKeysToSession(sessionName: string, keys: string): Promise<void> {
|
|
4
|
+
try {
|
|
5
|
+
// Find session
|
|
6
|
+
const session = await tmux.findSessionByName(sessionName);
|
|
7
|
+
if (!session) {
|
|
8
|
+
console.error(`❌ Session "${sessionName}" not found`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Get first window and pane
|
|
13
|
+
const windows = await tmux.listWindows(session.id);
|
|
14
|
+
if (!windows || windows.length === 0) {
|
|
15
|
+
console.error(`❌ No windows found in session "${sessionName}"`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
20
|
+
if (!panes || panes.length === 0) {
|
|
21
|
+
console.error(`❌ No panes found in session "${sessionName}"`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const paneId = panes[0].id;
|
|
26
|
+
|
|
27
|
+
// Send keys without Enter
|
|
28
|
+
await tmux.executeCommand(paneId, keys, false, true);
|
|
29
|
+
console.log(`✅ Keys sent to session "${sessionName}"`);
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
console.error(`❌ Error sending keys: ${error.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
4
|
+
import * as readline from 'readline';
|
|
5
|
+
|
|
6
|
+
export interface ShortcutsOptions {
|
|
7
|
+
tmux?: boolean;
|
|
8
|
+
termux?: boolean;
|
|
9
|
+
install?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Generate tmux.conf snippet for Warp-like shortcuts
|
|
13
|
+
export function generateTmuxConfig(): string {
|
|
14
|
+
return `# Warp-like keyboard shortcuts (generated by genie-cli)
|
|
15
|
+
# To use: add to ~/.tmux.conf or source this file
|
|
16
|
+
|
|
17
|
+
# Ctrl+T: New window (tab) in current session
|
|
18
|
+
bind-key -n C-t new-window
|
|
19
|
+
|
|
20
|
+
# Ctrl+S: Vertical split (requires stty -ixon in shell rc)
|
|
21
|
+
bind-key -n C-s split-window -v
|
|
22
|
+
|
|
23
|
+
# Alt+S: Horizontal split
|
|
24
|
+
bind-key -n M-s split-window -h
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Generate termux.properties snippet for extra keys
|
|
29
|
+
export function generateTermuxConfig(): string {
|
|
30
|
+
return `# Warp-like extra keys (generated by genie-cli)
|
|
31
|
+
# To use: add to ~/.termux/termux.properties
|
|
32
|
+
|
|
33
|
+
# Extra keys row with F-keys for shortcuts
|
|
34
|
+
# F1=New Tab, F2=VSplit, F3=HSplit
|
|
35
|
+
extra-keys = [['ESC','TAB','CTRL','ALT','F1','F2','F3']]
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Generate shell functions for bashrc/zshrc
|
|
40
|
+
export function generateShellFunctions(): string {
|
|
41
|
+
return `# Warp-like shortcuts (generated by genie-cli)
|
|
42
|
+
# To use: add to ~/.bashrc or ~/.zshrc
|
|
43
|
+
|
|
44
|
+
# Disable Ctrl+S flow control (required for Ctrl+S to work in tmux)
|
|
45
|
+
stty -ixon 2>/dev/null
|
|
46
|
+
|
|
47
|
+
# Genie shortcuts (for Termux extra keys F1-F3)
|
|
48
|
+
genie-new-tab() {
|
|
49
|
+
local session
|
|
50
|
+
session=$(tmux display-message -p '#S' 2>/dev/null)
|
|
51
|
+
if [ -n "$session" ]; then
|
|
52
|
+
tmux new-window
|
|
53
|
+
else
|
|
54
|
+
echo "Not in a tmux session"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
genie-vsplit() {
|
|
59
|
+
if tmux display-message -p '#S' >/dev/null 2>&1; then
|
|
60
|
+
tmux split-window -v
|
|
61
|
+
else
|
|
62
|
+
echo "Not in a tmux session"
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
genie-hsplit() {
|
|
67
|
+
if tmux display-message -p '#S' >/dev/null 2>&1; then
|
|
68
|
+
tmux split-window -h
|
|
69
|
+
else
|
|
70
|
+
echo "Not in a tmux session"
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Bind F1-F3 to genie shortcuts (works in Termux)
|
|
75
|
+
bind -x '"\eOP":"genie-new-tab"' 2>/dev/null # F1
|
|
76
|
+
bind -x '"\eOQ":"genie-vsplit"' 2>/dev/null # F2
|
|
77
|
+
bind -x '"\eOR":"genie-hsplit"' 2>/dev/null # F3
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Display all shortcuts in a readable format
|
|
82
|
+
export function displayShortcuts(): void {
|
|
83
|
+
console.log(`
|
|
84
|
+
Warp-like Terminal Shortcuts for tmux + Termux
|
|
85
|
+
|
|
86
|
+
┌────────────┬────────────────────────────────────────┐
|
|
87
|
+
│ Shortcut │ Action │
|
|
88
|
+
├────────────┼────────────────────────────────────────┤
|
|
89
|
+
│ Ctrl+T │ New tab (window) in current session │
|
|
90
|
+
│ Ctrl+S │ Vertical split in current session │
|
|
91
|
+
│ Alt+S │ Horizontal split in current session │
|
|
92
|
+
└────────────┴────────────────────────────────────────┘
|
|
93
|
+
|
|
94
|
+
Termux Extra Keys (F1-F3):
|
|
95
|
+
F1 → New tab F2 → Vertical split
|
|
96
|
+
F3 → Horizontal split
|
|
97
|
+
|
|
98
|
+
Commands:
|
|
99
|
+
term shortcuts --tmux Output tmux.conf snippet
|
|
100
|
+
term shortcuts --termux Output termux.properties snippet
|
|
101
|
+
term shortcuts --install Install to config files
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Helper to prompt user
|
|
106
|
+
async function prompt(question: string): Promise<string> {
|
|
107
|
+
const rl = readline.createInterface({
|
|
108
|
+
input: process.stdin,
|
|
109
|
+
output: process.stdout
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
rl.question(question, (answer) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
resolve(answer.trim().toLowerCase());
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check if content already exists in file
|
|
121
|
+
function contentExists(filePath: string, marker: string): boolean {
|
|
122
|
+
if (!existsSync(filePath)) return false;
|
|
123
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
124
|
+
return content.includes(marker);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Install shortcuts to config files
|
|
128
|
+
export async function installShortcuts(): Promise<void> {
|
|
129
|
+
const home = homedir();
|
|
130
|
+
const marker = 'generated by genie-cli';
|
|
131
|
+
|
|
132
|
+
console.log('Installing Warp-like shortcuts...\n');
|
|
133
|
+
|
|
134
|
+
// 1. Install to tmux.conf
|
|
135
|
+
const tmuxConf = join(home, '.tmux.conf');
|
|
136
|
+
if (contentExists(tmuxConf, marker)) {
|
|
137
|
+
console.log('✓ tmux.conf already has genie shortcuts');
|
|
138
|
+
} else {
|
|
139
|
+
const answer = await prompt(`Add shortcuts to ${tmuxConf}? [Y/n] `);
|
|
140
|
+
if (answer !== 'n') {
|
|
141
|
+
const content = '\n' + generateTmuxConfig();
|
|
142
|
+
appendFileSync(tmuxConf, content);
|
|
143
|
+
console.log(`✅ Added to ${tmuxConf}`);
|
|
144
|
+
} else {
|
|
145
|
+
console.log('⏭️ Skipped tmux.conf');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 2. Install to bashrc/zshrc
|
|
150
|
+
const shellRc = existsSync(join(home, '.zshrc'))
|
|
151
|
+
? join(home, '.zshrc')
|
|
152
|
+
: join(home, '.bashrc');
|
|
153
|
+
|
|
154
|
+
if (contentExists(shellRc, marker)) {
|
|
155
|
+
console.log(`✓ ${shellRc} already has genie shortcuts`);
|
|
156
|
+
} else {
|
|
157
|
+
const answer = await prompt(`Add shell functions to ${shellRc}? [Y/n] `);
|
|
158
|
+
if (answer !== 'n') {
|
|
159
|
+
const content = '\n' + generateShellFunctions();
|
|
160
|
+
appendFileSync(shellRc, content);
|
|
161
|
+
console.log(`✅ Added to ${shellRc}`);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`⏭️ Skipped ${shellRc}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. Install to termux.properties (if Termux detected)
|
|
168
|
+
const termuxDir = join(home, '.termux');
|
|
169
|
+
const termuxProps = join(termuxDir, 'termux.properties');
|
|
170
|
+
const isTermux = existsSync(termuxDir) || process.env.TERMUX_VERSION;
|
|
171
|
+
|
|
172
|
+
if (isTermux) {
|
|
173
|
+
if (contentExists(termuxProps, marker)) {
|
|
174
|
+
console.log('✓ termux.properties already has genie shortcuts');
|
|
175
|
+
} else {
|
|
176
|
+
const answer = await prompt(`Add extra keys to ${termuxProps}? [Y/n] `);
|
|
177
|
+
if (answer !== 'n') {
|
|
178
|
+
if (!existsSync(termuxDir)) {
|
|
179
|
+
mkdirSync(termuxDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
const content = '\n' + generateTermuxConfig();
|
|
182
|
+
appendFileSync(termuxProps, content);
|
|
183
|
+
console.log(`✅ Added to ${termuxProps}`);
|
|
184
|
+
console.log(' Run: termux-reload-settings');
|
|
185
|
+
} else {
|
|
186
|
+
console.log('⏭️ Skipped termux.properties');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('\n✅ Installation complete!');
|
|
192
|
+
console.log('\nNext steps:');
|
|
193
|
+
console.log(' 1. Reload tmux: tmux source ~/.tmux.conf');
|
|
194
|
+
console.log(' 2. Restart your shell or run: source ~/.bashrc');
|
|
195
|
+
if (isTermux) {
|
|
196
|
+
console.log(' 3. Reload Termux: termux-reload-settings');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if shortcuts are installed in a file
|
|
201
|
+
export function isShortcutsInstalled(filePath: string): boolean {
|
|
202
|
+
const marker = 'generated by genie-cli';
|
|
203
|
+
return contentExists(filePath, marker);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Remove content between markers from a file
|
|
207
|
+
function removeMarkedContent(filePath: string, marker: string): boolean {
|
|
208
|
+
if (!existsSync(filePath)) return false;
|
|
209
|
+
|
|
210
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
211
|
+
if (!content.includes(marker)) return false;
|
|
212
|
+
|
|
213
|
+
// Split into lines and find the block to remove
|
|
214
|
+
const lines = content.split('\n');
|
|
215
|
+
const filteredLines: string[] = [];
|
|
216
|
+
let inBlock = false;
|
|
217
|
+
let blockStartIndex = -1;
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
const line = lines[i];
|
|
221
|
+
if (line.includes(marker) && !inBlock) {
|
|
222
|
+
// Start of a marked block - find the comment start (could be previous line)
|
|
223
|
+
inBlock = true;
|
|
224
|
+
// Check if previous line is a blank line we should also remove
|
|
225
|
+
if (blockStartIndex === -1 && filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === '') {
|
|
226
|
+
filteredLines.pop();
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (inBlock) {
|
|
231
|
+
// Check if this is the end of the block (empty line followed by non-genie content)
|
|
232
|
+
if (line.trim() === '' && i + 1 < lines.length && !lines[i + 1].includes('genie')) {
|
|
233
|
+
inBlock = false;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Check if this line is part of our genie config (has specific markers)
|
|
237
|
+
const isGenieContent =
|
|
238
|
+
line.includes('genie') ||
|
|
239
|
+
line.includes('Ctrl+') ||
|
|
240
|
+
line.includes('split-window') ||
|
|
241
|
+
line.includes('new-window') ||
|
|
242
|
+
line.includes('stty -ixon') ||
|
|
243
|
+
line.includes('Warp-like') ||
|
|
244
|
+
line.includes('bind-key -n') ||
|
|
245
|
+
line.includes('extra-keys') ||
|
|
246
|
+
line.includes('F1=') ||
|
|
247
|
+
line.includes('bind -x') ||
|
|
248
|
+
line.startsWith('#') && lines[i - 1]?.includes('genie');
|
|
249
|
+
|
|
250
|
+
if (!isGenieContent && line.trim() !== '') {
|
|
251
|
+
inBlock = false;
|
|
252
|
+
filteredLines.push(line);
|
|
253
|
+
}
|
|
254
|
+
// Skip genie content lines
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
filteredLines.push(line);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Clean up trailing newlines
|
|
261
|
+
while (filteredLines.length > 0 && filteredLines[filteredLines.length - 1].trim() === '') {
|
|
262
|
+
filteredLines.pop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Add back one trailing newline if file had content
|
|
266
|
+
const newContent = filteredLines.length > 0 ? filteredLines.join('\n') + '\n' : '';
|
|
267
|
+
writeFileSync(filePath, newContent);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Uninstall shortcuts from config files
|
|
272
|
+
export async function uninstallShortcuts(): Promise<void> {
|
|
273
|
+
const home = homedir();
|
|
274
|
+
const marker = 'generated by genie-cli';
|
|
275
|
+
|
|
276
|
+
console.log('Uninstalling Warp-like shortcuts...\n');
|
|
277
|
+
|
|
278
|
+
// 1. Remove from tmux.conf
|
|
279
|
+
const tmuxConf = join(home, '.tmux.conf');
|
|
280
|
+
if (!contentExists(tmuxConf, marker)) {
|
|
281
|
+
console.log('✓ tmux.conf has no genie shortcuts');
|
|
282
|
+
} else {
|
|
283
|
+
const answer = await prompt(`Remove shortcuts from ${tmuxConf}? [Y/n] `);
|
|
284
|
+
if (answer !== 'n') {
|
|
285
|
+
if (removeMarkedContent(tmuxConf, marker)) {
|
|
286
|
+
console.log(`✅ Removed from ${tmuxConf}`);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
console.log('⏭️ Skipped tmux.conf');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 2. Remove from bashrc/zshrc
|
|
294
|
+
const zshrc = join(home, '.zshrc');
|
|
295
|
+
const bashrc = join(home, '.bashrc');
|
|
296
|
+
|
|
297
|
+
// Check both files
|
|
298
|
+
for (const shellRc of [zshrc, bashrc]) {
|
|
299
|
+
if (!existsSync(shellRc)) continue;
|
|
300
|
+
if (!contentExists(shellRc, marker)) {
|
|
301
|
+
console.log(`✓ ${shellRc} has no genie shortcuts`);
|
|
302
|
+
} else {
|
|
303
|
+
const answer = await prompt(`Remove shell functions from ${shellRc}? [Y/n] `);
|
|
304
|
+
if (answer !== 'n') {
|
|
305
|
+
if (removeMarkedContent(shellRc, marker)) {
|
|
306
|
+
console.log(`✅ Removed from ${shellRc}`);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
console.log(`⏭️ Skipped ${shellRc}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 3. Remove from termux.properties (if Termux detected)
|
|
315
|
+
const termuxDir = join(home, '.termux');
|
|
316
|
+
const termuxProps = join(termuxDir, 'termux.properties');
|
|
317
|
+
const isTermux = existsSync(termuxDir) || process.env.TERMUX_VERSION;
|
|
318
|
+
|
|
319
|
+
if (isTermux) {
|
|
320
|
+
if (!contentExists(termuxProps, marker)) {
|
|
321
|
+
console.log('✓ termux.properties has no genie shortcuts');
|
|
322
|
+
} else {
|
|
323
|
+
const answer = await prompt(`Remove extra keys from ${termuxProps}? [Y/n] `);
|
|
324
|
+
if (answer !== 'n') {
|
|
325
|
+
if (removeMarkedContent(termuxProps, marker)) {
|
|
326
|
+
console.log(`✅ Removed from ${termuxProps}`);
|
|
327
|
+
console.log(' Run: termux-reload-settings');
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
console.log('⏭️ Skipped termux.properties');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log('\n✅ Uninstallation complete!');
|
|
336
|
+
console.log('\nNext steps:');
|
|
337
|
+
console.log(' 1. Reload tmux: tmux source ~/.tmux.conf');
|
|
338
|
+
console.log(' 2. Restart your shell or run: source ~/.bashrc');
|
|
339
|
+
if (isTermux) {
|
|
340
|
+
console.log(' 3. Reload Termux: termux-reload-settings');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Main handler
|
|
345
|
+
export async function handleShortcuts(options: ShortcutsOptions): Promise<void> {
|
|
346
|
+
if (options.tmux) {
|
|
347
|
+
console.log(generateTmuxConfig());
|
|
348
|
+
} else if (options.termux) {
|
|
349
|
+
console.log(generateTermuxConfig());
|
|
350
|
+
} else if (options.install) {
|
|
351
|
+
await installShortcuts();
|
|
352
|
+
} else {
|
|
353
|
+
displayShortcuts();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import * as tmux from '../lib/tmux.js';
|
|
3
|
+
import { createWorktreeManager } from '../lib/worktree.js';
|
|
4
|
+
|
|
5
|
+
export interface SplitOptions {
|
|
6
|
+
workspace?: string;
|
|
7
|
+
worktree?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function splitSessionPane(
|
|
11
|
+
sessionName: string,
|
|
12
|
+
direction?: string,
|
|
13
|
+
options: SplitOptions = {}
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
// Find session
|
|
17
|
+
const session = await tmux.findSessionByName(sessionName);
|
|
18
|
+
if (!session) {
|
|
19
|
+
console.error(`❌ Session "${sessionName}" not found`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get first window and pane
|
|
24
|
+
const windows = await tmux.listWindows(session.id);
|
|
25
|
+
if (!windows || windows.length === 0) {
|
|
26
|
+
console.error(`❌ No windows found in session "${sessionName}"`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
31
|
+
if (!panes || panes.length === 0) {
|
|
32
|
+
console.error(`❌ No panes found in session "${sessionName}"`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const paneId = panes[0].id;
|
|
37
|
+
|
|
38
|
+
// Determine direction
|
|
39
|
+
const splitDirection = direction === 'h' ? 'horizontal' : 'vertical';
|
|
40
|
+
|
|
41
|
+
// Handle workspace and worktree options
|
|
42
|
+
let workingDir: string | undefined;
|
|
43
|
+
|
|
44
|
+
if (options.worktree) {
|
|
45
|
+
if (!options.workspace) {
|
|
46
|
+
console.error('❌ --worktree requires --workspace');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const manager = createWorktreeManager({
|
|
51
|
+
baseDir: join(options.workspace, '.worktrees'),
|
|
52
|
+
repoPath: options.workspace
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Check if worktree exists, create if not
|
|
56
|
+
const exists = await manager.worktreeExists(options.worktree);
|
|
57
|
+
if (!exists) {
|
|
58
|
+
const wt = await manager.createWorktree(options.worktree, true);
|
|
59
|
+
console.log(`✅ Worktree created at ${wt.path}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
workingDir = manager.getWorktreePath(options.worktree);
|
|
63
|
+
} else if (options.workspace) {
|
|
64
|
+
workingDir = options.workspace;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Split pane
|
|
68
|
+
const newPane = await tmux.splitPane(paneId, splitDirection);
|
|
69
|
+
if (!newPane) {
|
|
70
|
+
console.error('❌ Failed to split pane');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Change to working directory if specified
|
|
75
|
+
if (workingDir && newPane) {
|
|
76
|
+
await tmux.executeTmux(`send-keys -t '${newPane.id}' 'cd ${workingDir}' Enter`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`✅ Pane split ${splitDirection}ly in session "${sessionName}"`);
|
|
80
|
+
if (workingDir) {
|
|
81
|
+
console.log(` Working directory: ${workingDir}`);
|
|
82
|
+
}
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
console.error(`❌ Error splitting pane: ${error.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|