@jasonfutch/worktree-manager 1.0.0
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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/constants.d.ts +152 -0
- package/dist/constants.js +148 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +117 -0
- package/dist/git/worktree.d.ts +57 -0
- package/dist/git/worktree.js +272 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +336 -0
- package/dist/tui/app.d.ts +39 -0
- package/dist/tui/app.js +842 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.js +4 -0
- package/dist/utils/checks.d.ts +12 -0
- package/dist/utils/checks.js +109 -0
- package/dist/utils/helpers.d.ts +11 -0
- package/dist/utils/helpers.js +20 -0
- package/dist/utils/launch.d.ts +17 -0
- package/dist/utils/launch.js +223 -0
- package/dist/utils/shell.d.ts +28 -0
- package/dist/utils/shell.js +94 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +11 -0
- package/package.json +68 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the worktree manager
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Represents a git worktree
|
|
6
|
+
*/
|
|
7
|
+
export interface Worktree {
|
|
8
|
+
/** Absolute filesystem path to the worktree */
|
|
9
|
+
path: string;
|
|
10
|
+
/** Branch name, or "HEAD (detached)" for detached HEAD state */
|
|
11
|
+
branch: string;
|
|
12
|
+
/** Short SHA of the current commit (7 characters) */
|
|
13
|
+
commit: string;
|
|
14
|
+
/** Whether this is the main worktree (same path as repo root) */
|
|
15
|
+
isMain: boolean;
|
|
16
|
+
/** Whether this is a bare repository */
|
|
17
|
+
isBare: boolean;
|
|
18
|
+
/** Whether the worktree is locked */
|
|
19
|
+
isLocked: boolean;
|
|
20
|
+
/** Reason for lock, if locked with a reason */
|
|
21
|
+
lockedReason?: string;
|
|
22
|
+
/** Whether the worktree is prunable (stale) */
|
|
23
|
+
prunable?: boolean;
|
|
24
|
+
/** Friendly name derived from path basename */
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for creating a new worktree
|
|
29
|
+
*/
|
|
30
|
+
export interface CreateWorktreeOptions {
|
|
31
|
+
/** Name of the branch to create or checkout */
|
|
32
|
+
branch: string;
|
|
33
|
+
/** Base branch to create new branch from (default: 'main') */
|
|
34
|
+
baseBranch?: string;
|
|
35
|
+
/** Custom path for the worktree (default: auto-generated) */
|
|
36
|
+
path?: string;
|
|
37
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if Git is installed and accessible
|
|
3
|
+
*/
|
|
4
|
+
export declare function checkGitInstalled(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Check if Node.js version meets minimum requirement
|
|
7
|
+
*/
|
|
8
|
+
export declare function checkNodeVersion(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Run all startup checks and exit with helpful messages if any fail
|
|
11
|
+
*/
|
|
12
|
+
export declare function runStartupChecks(): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
const MIN_NODE_VERSION = 18;
|
|
4
|
+
/**
|
|
5
|
+
* Check if Git is installed and accessible
|
|
6
|
+
*/
|
|
7
|
+
export function checkGitInstalled() {
|
|
8
|
+
try {
|
|
9
|
+
execSync('git --version', { stdio: 'pipe' });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if Node.js version meets minimum requirement
|
|
18
|
+
*/
|
|
19
|
+
export function checkNodeVersion() {
|
|
20
|
+
const currentVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
21
|
+
return currentVersion >= MIN_NODE_VERSION;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get current Node.js version string
|
|
25
|
+
*/
|
|
26
|
+
function getNodeVersion() {
|
|
27
|
+
return process.versions.node;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detect platform for install instructions
|
|
31
|
+
*/
|
|
32
|
+
function getPlatform() {
|
|
33
|
+
if (process.platform === 'darwin')
|
|
34
|
+
return 'mac';
|
|
35
|
+
if (process.platform === 'win32')
|
|
36
|
+
return 'win';
|
|
37
|
+
return 'linux';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run all startup checks and exit with helpful messages if any fail
|
|
41
|
+
*/
|
|
42
|
+
export function runStartupChecks() {
|
|
43
|
+
const errors = [];
|
|
44
|
+
const platform = getPlatform();
|
|
45
|
+
// Check Git
|
|
46
|
+
if (!checkGitInstalled()) {
|
|
47
|
+
let installInstructions = '';
|
|
48
|
+
if (platform === 'mac') {
|
|
49
|
+
installInstructions = `
|
|
50
|
+
${chalk.cyan('macOS:')}
|
|
51
|
+
brew install git
|
|
52
|
+
${chalk.gray('or install Xcode Command Line Tools:')} xcode-select --install`;
|
|
53
|
+
}
|
|
54
|
+
else if (platform === 'win') {
|
|
55
|
+
installInstructions = `
|
|
56
|
+
${chalk.cyan('Windows:')}
|
|
57
|
+
Download from: ${chalk.underline('https://git-scm.com/download/win')}
|
|
58
|
+
${chalk.gray('or use:')} winget install Git.Git`;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
installInstructions = `
|
|
62
|
+
${chalk.cyan('Ubuntu/Debian:')} sudo apt install git
|
|
63
|
+
${chalk.cyan('Fedora:')} sudo dnf install git
|
|
64
|
+
${chalk.cyan('Arch:')} sudo pacman -S git`;
|
|
65
|
+
}
|
|
66
|
+
errors.push(`
|
|
67
|
+
${chalk.red.bold('Git is not installed or not in PATH.')}
|
|
68
|
+
|
|
69
|
+
${chalk.yellow('Install Git:')}${installInstructions}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
// Check Node version
|
|
73
|
+
if (!checkNodeVersion()) {
|
|
74
|
+
let updateInstructions = '';
|
|
75
|
+
if (platform === 'mac') {
|
|
76
|
+
updateInstructions = `
|
|
77
|
+
${chalk.cyan('macOS:')}
|
|
78
|
+
brew upgrade node
|
|
79
|
+
${chalk.gray('or use nvm:')} nvm install 18`;
|
|
80
|
+
}
|
|
81
|
+
else if (platform === 'win') {
|
|
82
|
+
updateInstructions = `
|
|
83
|
+
${chalk.cyan('Windows:')}
|
|
84
|
+
Download from: ${chalk.underline('https://nodejs.org/')}
|
|
85
|
+
${chalk.gray('or use:')} winget upgrade OpenJS.NodeJS.LTS`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
updateInstructions = `
|
|
89
|
+
${chalk.cyan('Linux:')}
|
|
90
|
+
Download from: ${chalk.underline('https://nodejs.org/')}
|
|
91
|
+
${chalk.gray('or use nvm:')} nvm install 18`;
|
|
92
|
+
}
|
|
93
|
+
errors.push(`
|
|
94
|
+
${chalk.red.bold(`Node.js ${MIN_NODE_VERSION}+ is required.`)} ${chalk.gray(`You have: ${getNodeVersion()}`)}
|
|
95
|
+
|
|
96
|
+
${chalk.yellow('Update Node.js:')}${updateInstructions}
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
// If any errors, print them and exit
|
|
100
|
+
if (errors.length > 0) {
|
|
101
|
+
console.error(chalk.red.bold('\n Dependency Check Failed\n'));
|
|
102
|
+
console.error(chalk.gray('─'.repeat(50)));
|
|
103
|
+
for (const error of errors) {
|
|
104
|
+
console.error(error);
|
|
105
|
+
}
|
|
106
|
+
console.error(chalk.gray('─'.repeat(50)));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper utilities for the worktree manager
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get a truncated string with ellipsis
|
|
6
|
+
*/
|
|
7
|
+
export declare function truncate(str: string, maxLength: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format a relative path from a base path
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatPath(fullPath: string, basePath: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper utilities for the worktree manager
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get a truncated string with ellipsis
|
|
6
|
+
*/
|
|
7
|
+
export function truncate(str, maxLength) {
|
|
8
|
+
if (str.length <= maxLength)
|
|
9
|
+
return str;
|
|
10
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Format a relative path from a base path
|
|
14
|
+
*/
|
|
15
|
+
export function formatPath(fullPath, basePath) {
|
|
16
|
+
if (fullPath.startsWith(basePath)) {
|
|
17
|
+
return fullPath.slice(basePath.length + 1) || '.';
|
|
18
|
+
}
|
|
19
|
+
return fullPath;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EDITORS, AI_TOOLS } from '../constants.js';
|
|
2
|
+
export type EditorType = keyof typeof EDITORS;
|
|
3
|
+
export type AIToolType = keyof typeof AI_TOOLS;
|
|
4
|
+
/**
|
|
5
|
+
* Open a path in an editor
|
|
6
|
+
*/
|
|
7
|
+
export declare function openEditor(targetPath: string, editor: EditorType, onError?: (message: string) => void, onSuccess?: (message: string) => void): void;
|
|
8
|
+
/**
|
|
9
|
+
* Open a terminal at a path
|
|
10
|
+
*/
|
|
11
|
+
export declare function openTerminal(targetPath: string, onError?: (message: string) => void, onSuccess?: (message: string) => void): void;
|
|
12
|
+
/**
|
|
13
|
+
* Launch an AI tool at a path
|
|
14
|
+
*/
|
|
15
|
+
export declare function launchAI(targetPath: string, tool: AIToolType, onError?: (message: string) => void, onSuccess?: (message: string) => void): void;
|
|
16
|
+
export declare const validEditors: EditorType[];
|
|
17
|
+
export declare const validAITools: AIToolType[];
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { escapeAppleScript, escapeWindowsArg } from './shell.js';
|
|
4
|
+
import { PLATFORM, getInstallInstruction, EDITORS } from '../constants.js';
|
|
5
|
+
import { ToolNotFoundError } from '../errors.js';
|
|
6
|
+
const editorNames = {
|
|
7
|
+
code: 'VS Code',
|
|
8
|
+
cursor: 'Cursor',
|
|
9
|
+
zed: 'Zed',
|
|
10
|
+
webstorm: 'WebStorm',
|
|
11
|
+
subl: 'Sublime Text',
|
|
12
|
+
nvim: 'Neovim'
|
|
13
|
+
};
|
|
14
|
+
const aiToolNames = {
|
|
15
|
+
claude: 'Claude',
|
|
16
|
+
gemini: 'Gemini',
|
|
17
|
+
codex: 'Codex'
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Open a path in an editor
|
|
21
|
+
*/
|
|
22
|
+
export function openEditor(targetPath, editor, onError, onSuccess) {
|
|
23
|
+
const name = editorNames[editor];
|
|
24
|
+
const editorConfig = EDITORS[editor];
|
|
25
|
+
const isTerminalEditor = editorConfig?.terminal ?? false;
|
|
26
|
+
const handleError = (err) => {
|
|
27
|
+
if (err.code === 'ENOENT') {
|
|
28
|
+
const hint = getInstallInstruction(editor);
|
|
29
|
+
const message = `${name} not found. ${hint}`;
|
|
30
|
+
if (onError)
|
|
31
|
+
onError(message);
|
|
32
|
+
else
|
|
33
|
+
console.error(chalk.red(message));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const message = `Error: ${err.message}`;
|
|
37
|
+
if (onError)
|
|
38
|
+
onError(message);
|
|
39
|
+
else
|
|
40
|
+
console.error(chalk.red(message));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
let proc;
|
|
45
|
+
if (isTerminalEditor) {
|
|
46
|
+
if (PLATFORM.IS_MAC) {
|
|
47
|
+
// Escape the path for AppleScript context
|
|
48
|
+
const escapedPath = escapeAppleScript(targetPath);
|
|
49
|
+
const script = `
|
|
50
|
+
tell application "Terminal"
|
|
51
|
+
do script "cd '${escapedPath}' && ${editor} ."
|
|
52
|
+
activate
|
|
53
|
+
end tell
|
|
54
|
+
`;
|
|
55
|
+
proc = spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' });
|
|
56
|
+
}
|
|
57
|
+
else if (PLATFORM.IS_WIN) {
|
|
58
|
+
// Escape the path for Windows cmd context
|
|
59
|
+
const escapedPath = escapeWindowsArg(targetPath);
|
|
60
|
+
proc = spawn('cmd', ['/c', 'start', 'cmd', '/k', `cd /d ${escapedPath} && ${editor} .`], {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
shell: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Linux - pass path directly to gnome-terminal which handles it safely
|
|
68
|
+
proc = spawn('gnome-terminal', ['--working-directory', targetPath, '--', editor, '.'], {
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: 'ignore'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// GUI editors can open directly - spawn with array args (safe)
|
|
76
|
+
proc = spawn(editor, [targetPath], { detached: true, stdio: 'ignore' });
|
|
77
|
+
}
|
|
78
|
+
proc.on('error', handleError);
|
|
79
|
+
proc.unref();
|
|
80
|
+
const successMsg = `Opened in ${name}`;
|
|
81
|
+
if (onSuccess)
|
|
82
|
+
onSuccess(successMsg);
|
|
83
|
+
else
|
|
84
|
+
console.log(chalk.green(`✓ ${successMsg}`));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = `Error: ${error instanceof Error ? error.message : error}`;
|
|
88
|
+
if (onError)
|
|
89
|
+
onError(message);
|
|
90
|
+
else
|
|
91
|
+
console.error(chalk.red(message));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Open a terminal at a path
|
|
96
|
+
*/
|
|
97
|
+
export function openTerminal(targetPath, onError, onSuccess) {
|
|
98
|
+
try {
|
|
99
|
+
if (PLATFORM.IS_MAC) {
|
|
100
|
+
// macOS Terminal.app - use open command with path as argument (safe)
|
|
101
|
+
spawn('open', ['-a', 'Terminal', targetPath], { detached: true, stdio: 'ignore' });
|
|
102
|
+
}
|
|
103
|
+
else if (PLATFORM.IS_WIN) {
|
|
104
|
+
// Windows - escape path for cmd.exe
|
|
105
|
+
const escapedPath = escapeWindowsArg(targetPath);
|
|
106
|
+
spawn('cmd', ['/c', 'start', 'cmd', '/k', `cd /d ${escapedPath}`], {
|
|
107
|
+
detached: true,
|
|
108
|
+
stdio: 'ignore',
|
|
109
|
+
shell: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (PLATFORM.IS_WSL) {
|
|
113
|
+
// WSL - pass path directly to wt.exe
|
|
114
|
+
spawn('wt.exe', ['-d', targetPath], { detached: true, stdio: 'ignore' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Linux - try common terminal emulators
|
|
118
|
+
const terminals = ['gnome-terminal', 'konsole', 'xterm', 'terminator'];
|
|
119
|
+
let launched = false;
|
|
120
|
+
for (const term of terminals) {
|
|
121
|
+
try {
|
|
122
|
+
const proc = spawn(term, ['--working-directory', targetPath], {
|
|
123
|
+
detached: true,
|
|
124
|
+
stdio: 'ignore'
|
|
125
|
+
});
|
|
126
|
+
proc.on('error', () => {
|
|
127
|
+
// Silently fail and try next terminal
|
|
128
|
+
});
|
|
129
|
+
proc.unref();
|
|
130
|
+
launched = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Try next terminal
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!launched) {
|
|
138
|
+
throw new ToolNotFoundError('Terminal emulator', 'Install gnome-terminal, konsole, xterm, or terminator');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const successMsg = 'Opened terminal';
|
|
142
|
+
if (onSuccess)
|
|
143
|
+
onSuccess(successMsg);
|
|
144
|
+
else
|
|
145
|
+
console.log(chalk.green(`✓ ${successMsg}`));
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof ToolNotFoundError) {
|
|
149
|
+
if (onError)
|
|
150
|
+
onError(error.message);
|
|
151
|
+
else
|
|
152
|
+
console.error(chalk.red(error.message));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const message = `Error: ${error instanceof Error ? error.message : error}`;
|
|
156
|
+
if (onError)
|
|
157
|
+
onError(message);
|
|
158
|
+
else
|
|
159
|
+
console.error(chalk.red(message));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Launch an AI tool at a path
|
|
165
|
+
*/
|
|
166
|
+
export function launchAI(targetPath, tool, onError, onSuccess) {
|
|
167
|
+
const name = aiToolNames[tool];
|
|
168
|
+
try {
|
|
169
|
+
let proc;
|
|
170
|
+
if (PLATFORM.IS_MAC) {
|
|
171
|
+
// Escape the path for AppleScript context
|
|
172
|
+
const escapedPath = escapeAppleScript(targetPath);
|
|
173
|
+
const script = `
|
|
174
|
+
tell application "Terminal"
|
|
175
|
+
do script "cd '${escapedPath}' && ${tool}"
|
|
176
|
+
activate
|
|
177
|
+
end tell
|
|
178
|
+
`;
|
|
179
|
+
proc = spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' });
|
|
180
|
+
}
|
|
181
|
+
else if (PLATFORM.IS_WIN) {
|
|
182
|
+
// Escape the path for Windows cmd context
|
|
183
|
+
const escapedPath = escapeWindowsArg(targetPath);
|
|
184
|
+
proc = spawn('cmd', ['/c', 'start', 'cmd', '/k', `cd /d ${escapedPath} && ${tool}`], {
|
|
185
|
+
detached: true,
|
|
186
|
+
stdio: 'ignore',
|
|
187
|
+
shell: true
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Linux - pass arguments safely via array
|
|
192
|
+
proc = spawn('gnome-terminal', ['--working-directory', targetPath, '--', tool], {
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: 'ignore'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
proc.on('error', (err) => {
|
|
198
|
+
if (err.code === 'ENOENT') {
|
|
199
|
+
const hint = getInstallInstruction(tool);
|
|
200
|
+
const message = `${name} not found. ${hint}`;
|
|
201
|
+
if (onError)
|
|
202
|
+
onError(message);
|
|
203
|
+
else
|
|
204
|
+
console.error(chalk.red(message));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
proc.unref();
|
|
208
|
+
const successMsg = `Launched ${name}`;
|
|
209
|
+
if (onSuccess)
|
|
210
|
+
onSuccess(successMsg);
|
|
211
|
+
else
|
|
212
|
+
console.log(chalk.green(`✓ ${successMsg}`));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const message = `Error: ${error instanceof Error ? error.message : error}`;
|
|
216
|
+
if (onError)
|
|
217
|
+
onError(message);
|
|
218
|
+
else
|
|
219
|
+
console.error(chalk.red(message));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export const validEditors = ['code', 'cursor', 'zed', 'webstorm', 'subl', 'nvim'];
|
|
223
|
+
export const validAITools = ['claude', 'gemini', 'codex'];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell escaping utilities to prevent command injection
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Escape a string for use as a shell argument (POSIX shells)
|
|
6
|
+
* Wraps the argument in single quotes and escapes any existing single quotes
|
|
7
|
+
*/
|
|
8
|
+
export declare function escapeShellArg(arg: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Escape a string for use in AppleScript
|
|
11
|
+
* Handles single quotes, double quotes, and backslashes
|
|
12
|
+
*/
|
|
13
|
+
export declare function escapeAppleScript(str: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Escape a string for use in Windows cmd.exe
|
|
16
|
+
* Handles special characters that need escaping in batch scripts
|
|
17
|
+
*/
|
|
18
|
+
export declare function escapeWindowsArg(arg: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a path doesn't contain path traversal attempts
|
|
21
|
+
* @returns true if the path is safe, false if it contains suspicious patterns
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPathSafe(targetPath: string, basePath?: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Validate a git branch name
|
|
26
|
+
* Based on git-check-ref-format rules
|
|
27
|
+
*/
|
|
28
|
+
export declare function isValidBranchName(branch: string): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell escaping utilities to prevent command injection
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
/**
|
|
6
|
+
* Escape a string for use as a shell argument (POSIX shells)
|
|
7
|
+
* Wraps the argument in single quotes and escapes any existing single quotes
|
|
8
|
+
*/
|
|
9
|
+
export function escapeShellArg(arg) {
|
|
10
|
+
// Replace single quotes with '\'' (end quote, escaped quote, start quote)
|
|
11
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Escape a string for use in AppleScript
|
|
15
|
+
* Handles single quotes, double quotes, and backslashes
|
|
16
|
+
*/
|
|
17
|
+
export function escapeAppleScript(str) {
|
|
18
|
+
// In AppleScript strings, escape backslashes and quotes
|
|
19
|
+
return str
|
|
20
|
+
.replace(/\\/g, '\\\\')
|
|
21
|
+
.replace(/'/g, "\\'")
|
|
22
|
+
.replace(/"/g, '\\"');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Escape a string for use in Windows cmd.exe
|
|
26
|
+
* Handles special characters that need escaping in batch scripts
|
|
27
|
+
*/
|
|
28
|
+
export function escapeWindowsArg(arg) {
|
|
29
|
+
// For cmd.exe, we need to handle special characters
|
|
30
|
+
// Wrap in double quotes and escape internal double quotes
|
|
31
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate that a path doesn't contain path traversal attempts
|
|
35
|
+
* @returns true if the path is safe, false if it contains suspicious patterns
|
|
36
|
+
*/
|
|
37
|
+
export function isPathSafe(targetPath, basePath) {
|
|
38
|
+
// Check for obvious path traversal patterns
|
|
39
|
+
if (targetPath.includes('..')) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Check for null bytes (can be used to bypass security checks)
|
|
43
|
+
if (targetPath.includes('\0')) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// If a base path is provided, ensure target is within it
|
|
47
|
+
if (basePath) {
|
|
48
|
+
const resolved = path.resolve(targetPath);
|
|
49
|
+
const resolvedBase = path.resolve(basePath);
|
|
50
|
+
// Target should start with base path
|
|
51
|
+
if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate a git branch name
|
|
59
|
+
* Based on git-check-ref-format rules
|
|
60
|
+
*/
|
|
61
|
+
export function isValidBranchName(branch) {
|
|
62
|
+
// Can't be empty
|
|
63
|
+
if (!branch || branch.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Can't start or end with /
|
|
67
|
+
if (branch.startsWith('/') || branch.endsWith('/')) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Can't contain consecutive slashes
|
|
71
|
+
if (branch.includes('//')) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// Can't contain special characters
|
|
75
|
+
const forbidden = ['..', ' ', '~', '^', ':', '?', '*', '[', '\\', '\x7f'];
|
|
76
|
+
for (const char of forbidden) {
|
|
77
|
+
if (branch.includes(char)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Can't start with a dash
|
|
82
|
+
if (branch.startsWith('-')) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// Can't end with .lock
|
|
86
|
+
if (branch.endsWith('.lock')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// Can't contain @{
|
|
90
|
+
if (branch.includes('@{')) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
// Read version from package.json
|
|
7
|
+
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
8
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
9
|
+
export const VERSION = packageJson.version;
|
|
10
|
+
export const PACKAGE_NAME = packageJson.name;
|
|
11
|
+
export const pkg = packageJson;
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jasonfutch/worktree-manager",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Terminal app for managing git worktrees with AI assistance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"wtm": "dist/index.js",
|
|
9
|
+
"worktree-manager": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"lint": "eslint src/",
|
|
22
|
+
"lint:fix": "eslint src/ --fix",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"git",
|
|
27
|
+
"worktree",
|
|
28
|
+
"terminal",
|
|
29
|
+
"tui",
|
|
30
|
+
"cli",
|
|
31
|
+
"claude",
|
|
32
|
+
"ai",
|
|
33
|
+
"vscode",
|
|
34
|
+
"cursor"
|
|
35
|
+
],
|
|
36
|
+
"author": "jasonfutch",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/jasonfutch/worktree-manager.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/jasonfutch/worktree-manager/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/jasonfutch/worktree-manager#readme",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"blessed": "^0.1.81",
|
|
54
|
+
"chalk": "^5.6.2",
|
|
55
|
+
"commander": "^14.0.2",
|
|
56
|
+
"update-notifier": "^7.3.1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^9.17.0",
|
|
60
|
+
"@types/blessed": "^0.1.27",
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"@types/update-notifier": "^6.0.8",
|
|
63
|
+
"eslint": "^9.17.0",
|
|
64
|
+
"tsx": "^4.21.0",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"typescript-eslint": "^8.18.2"
|
|
67
|
+
}
|
|
68
|
+
}
|