cluttry 1.0.3 → 1.5.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/CHANGELOG.md +71 -0
- package/README.md +203 -300
- package/dist/commands/completions.d.ts +16 -0
- package/dist/commands/completions.d.ts.map +1 -0
- package/dist/commands/completions.js +46 -0
- package/dist/commands/completions.js.map +1 -0
- package/dist/commands/explain-copy.d.ts +11 -0
- package/dist/commands/explain-copy.d.ts.map +1 -0
- package/dist/commands/explain-copy.js +34 -0
- package/dist/commands/explain-copy.js.map +1 -0
- package/dist/commands/finish.d.ts +20 -0
- package/dist/commands/finish.d.ts.map +1 -0
- package/dist/commands/finish.js +817 -0
- package/dist/commands/finish.js.map +1 -0
- package/dist/commands/gc.d.ts +22 -0
- package/dist/commands/gc.d.ts.map +1 -0
- package/dist/commands/gc.js +163 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/open.d.ts +15 -1
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +96 -17
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/resume.d.ts +21 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +106 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +6 -14
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/spawn.d.ts +3 -0
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +182 -19
- package/dist/commands/spawn.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -9
- package/dist/index.js.map +1 -1
- package/dist/lib/completions.d.ts +35 -0
- package/dist/lib/completions.d.ts.map +1 -0
- package/dist/lib/completions.js +368 -0
- package/dist/lib/completions.js.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +43 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +251 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/git.d.ts +17 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +78 -10
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/safety.d.ts +79 -0
- package/dist/lib/safety.d.ts.map +1 -0
- package/dist/lib/safety.js +133 -0
- package/dist/lib/safety.js.map +1 -0
- package/dist/lib/secrets.d.ts +29 -0
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +115 -0
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/session.d.ts +93 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +254 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/types.d.ts +6 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -7
- package/src/commands/doctor.ts +0 -222
- package/src/commands/init.ts +0 -120
- package/src/commands/list.ts +0 -133
- package/src/commands/open.ts +0 -78
- package/src/commands/prune.ts +0 -36
- package/src/commands/rm.ts +0 -125
- package/src/commands/shell.ts +0 -99
- package/src/commands/spawn.ts +0 -169
- package/src/index.ts +0 -123
- package/src/lib/config.ts +0 -120
- package/src/lib/git.ts +0 -243
- package/src/lib/output.ts +0 -102
- package/src/lib/paths.ts +0 -108
- package/src/lib/secrets.ts +0 -193
- package/src/lib/types.ts +0 -69
- package/tests/config.test.ts +0 -102
- package/tests/paths.test.ts +0 -155
- package/tests/secrets.test.ts +0 -150
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -15
package/src/lib/git.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git operations for cry
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { execSync, spawn } from 'node:child_process';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import type { WorktreeInfo } from './types.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Execute a git command and return stdout
|
|
12
|
-
*/
|
|
13
|
-
export function git(args: string[], cwd?: string): string {
|
|
14
|
-
const options = cwd ? { cwd, encoding: 'utf-8' as const } : { encoding: 'utf-8' as const };
|
|
15
|
-
try {
|
|
16
|
-
return execSync(`git ${args.join(' ')}`, { ...options, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
17
|
-
} catch (error: unknown) {
|
|
18
|
-
const execError = error as { stderr?: Buffer; message?: string };
|
|
19
|
-
const stderr = execError.stderr?.toString?.() || execError.message || 'Unknown git error';
|
|
20
|
-
throw new Error(stderr.trim());
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if we're in a git repository
|
|
26
|
-
*/
|
|
27
|
-
export function isGitRepo(cwd?: string): boolean {
|
|
28
|
-
try {
|
|
29
|
-
git(['rev-parse', '--git-dir'], cwd);
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get the root directory of the git repository
|
|
38
|
-
*/
|
|
39
|
-
export function getRepoRoot(cwd?: string): string {
|
|
40
|
-
return git(['rev-parse', '--show-toplevel'], cwd);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get the repository name from the root path
|
|
45
|
-
*/
|
|
46
|
-
export function getRepoName(cwd?: string): string {
|
|
47
|
-
const root = getRepoRoot(cwd);
|
|
48
|
-
return path.basename(root);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Check if a branch exists
|
|
53
|
-
*/
|
|
54
|
-
export function branchExists(branch: string, cwd?: string): boolean {
|
|
55
|
-
try {
|
|
56
|
-
git(['rev-parse', '--verify', `refs/heads/${branch}`], cwd);
|
|
57
|
-
return true;
|
|
58
|
-
} catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get the current branch name
|
|
65
|
-
*/
|
|
66
|
-
export function getCurrentBranch(cwd?: string): string | null {
|
|
67
|
-
try {
|
|
68
|
-
return git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Check if a file is tracked by git
|
|
76
|
-
*/
|
|
77
|
-
export function isTracked(filePath: string, cwd?: string): boolean {
|
|
78
|
-
try {
|
|
79
|
-
git(['ls-files', '--error-unmatch', filePath], cwd);
|
|
80
|
-
return true;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if a file is ignored by git
|
|
88
|
-
*/
|
|
89
|
-
export function isIgnored(filePath: string, cwd?: string): boolean {
|
|
90
|
-
try {
|
|
91
|
-
git(['check-ignore', '-q', filePath], cwd);
|
|
92
|
-
return true;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* List all worktrees using porcelain format
|
|
100
|
-
*/
|
|
101
|
-
export function listWorktrees(cwd?: string): WorktreeInfo[] {
|
|
102
|
-
const output = git(['worktree', 'list', '--porcelain'], cwd);
|
|
103
|
-
const worktrees: WorktreeInfo[] = [];
|
|
104
|
-
let current: Partial<WorktreeInfo> = {};
|
|
105
|
-
|
|
106
|
-
for (const line of output.split('\n')) {
|
|
107
|
-
if (line === '') {
|
|
108
|
-
if (current.worktree) {
|
|
109
|
-
worktrees.push(current as WorktreeInfo);
|
|
110
|
-
}
|
|
111
|
-
current = {};
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (line.startsWith('worktree ')) {
|
|
116
|
-
current.worktree = line.substring(9);
|
|
117
|
-
} else if (line.startsWith('HEAD ')) {
|
|
118
|
-
current.head = line.substring(5);
|
|
119
|
-
} else if (line.startsWith('branch ')) {
|
|
120
|
-
current.branch = line.substring(7).replace('refs/heads/', '');
|
|
121
|
-
} else if (line === 'bare') {
|
|
122
|
-
current.bare = true;
|
|
123
|
-
} else if (line === 'detached') {
|
|
124
|
-
current.detached = true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Don't forget the last entry
|
|
129
|
-
if (current.worktree) {
|
|
130
|
-
worktrees.push(current as WorktreeInfo);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return worktrees;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Add a worktree
|
|
138
|
-
*/
|
|
139
|
-
export function addWorktree(
|
|
140
|
-
targetPath: string,
|
|
141
|
-
branch: string,
|
|
142
|
-
createBranch: boolean,
|
|
143
|
-
cwd?: string
|
|
144
|
-
): void {
|
|
145
|
-
const args = ['worktree', 'add'];
|
|
146
|
-
if (createBranch) {
|
|
147
|
-
args.push('-b', branch);
|
|
148
|
-
}
|
|
149
|
-
args.push(targetPath);
|
|
150
|
-
if (!createBranch) {
|
|
151
|
-
args.push(branch);
|
|
152
|
-
}
|
|
153
|
-
git(args, cwd);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Remove a worktree
|
|
158
|
-
*/
|
|
159
|
-
export function removeWorktree(worktreePath: string, force: boolean, cwd?: string): void {
|
|
160
|
-
const args = ['worktree', 'remove'];
|
|
161
|
-
if (force) {
|
|
162
|
-
args.push('--force');
|
|
163
|
-
}
|
|
164
|
-
args.push(worktreePath);
|
|
165
|
-
git(args, cwd);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Prune worktrees
|
|
170
|
-
*/
|
|
171
|
-
export function pruneWorktrees(cwd?: string): string {
|
|
172
|
-
return git(['worktree', 'prune', '--verbose'], cwd);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Delete a branch
|
|
177
|
-
*/
|
|
178
|
-
export function deleteBranch(branch: string, force: boolean, cwd?: string): void {
|
|
179
|
-
const flag = force ? '-D' : '-d';
|
|
180
|
-
git(['branch', flag, branch], cwd);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Check if a worktree is dirty (has uncommitted changes)
|
|
185
|
-
*/
|
|
186
|
-
export function isWorktreeDirty(worktreePath: string): boolean {
|
|
187
|
-
try {
|
|
188
|
-
const status = git(['status', '--porcelain'], worktreePath);
|
|
189
|
-
return status.length > 0;
|
|
190
|
-
} catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Get short HEAD SHA for a worktree
|
|
197
|
-
*/
|
|
198
|
-
export function getShortHead(worktreePath: string): string {
|
|
199
|
-
try {
|
|
200
|
-
return git(['rev-parse', '--short', 'HEAD'], worktreePath);
|
|
201
|
-
} catch {
|
|
202
|
-
return 'unknown';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Run a command in a directory
|
|
208
|
-
*/
|
|
209
|
-
export function runCommand(command: string, cwd: string): Promise<number> {
|
|
210
|
-
return new Promise((resolve) => {
|
|
211
|
-
const isWindows = process.platform === 'win32';
|
|
212
|
-
const shell = isWindows ? 'cmd.exe' : '/bin/sh';
|
|
213
|
-
const shellArgs = isWindows ? ['/c', command] : ['-c', command];
|
|
214
|
-
|
|
215
|
-
const child = spawn(shell, shellArgs, {
|
|
216
|
-
cwd,
|
|
217
|
-
stdio: 'inherit',
|
|
218
|
-
env: process.env,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
child.on('close', (code) => {
|
|
222
|
-
resolve(code ?? 1);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
child.on('error', () => {
|
|
226
|
-
resolve(1);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Check if a command exists
|
|
233
|
-
*/
|
|
234
|
-
export function commandExists(cmd: string): boolean {
|
|
235
|
-
try {
|
|
236
|
-
const isWindows = process.platform === 'win32';
|
|
237
|
-
const checkCmd = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
238
|
-
execSync(checkCmd, { stdio: 'pipe' });
|
|
239
|
-
return true;
|
|
240
|
-
} catch {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/lib/output.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output utilities for cry
|
|
3
|
-
*
|
|
4
|
-
* Provides consistent, colorful terminal output without external dependencies.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// ANSI color codes
|
|
8
|
-
const colors = {
|
|
9
|
-
reset: '\x1b[0m',
|
|
10
|
-
bold: '\x1b[1m',
|
|
11
|
-
dim: '\x1b[2m',
|
|
12
|
-
|
|
13
|
-
red: '\x1b[31m',
|
|
14
|
-
green: '\x1b[32m',
|
|
15
|
-
yellow: '\x1b[33m',
|
|
16
|
-
blue: '\x1b[34m',
|
|
17
|
-
magenta: '\x1b[35m',
|
|
18
|
-
cyan: '\x1b[36m',
|
|
19
|
-
white: '\x1b[37m',
|
|
20
|
-
gray: '\x1b[90m',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Check if colors should be used
|
|
24
|
-
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
25
|
-
|
|
26
|
-
function colorize(text: string, ...codes: string[]): string {
|
|
27
|
-
if (!useColors) return text;
|
|
28
|
-
return codes.join('') + text + colors.reset;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const fmt = {
|
|
32
|
-
bold: (text: string) => colorize(text, colors.bold),
|
|
33
|
-
dim: (text: string) => colorize(text, colors.dim),
|
|
34
|
-
red: (text: string) => colorize(text, colors.red),
|
|
35
|
-
green: (text: string) => colorize(text, colors.green),
|
|
36
|
-
yellow: (text: string) => colorize(text, colors.yellow),
|
|
37
|
-
blue: (text: string) => colorize(text, colors.blue),
|
|
38
|
-
magenta: (text: string) => colorize(text, colors.magenta),
|
|
39
|
-
cyan: (text: string) => colorize(text, colors.cyan),
|
|
40
|
-
gray: (text: string) => colorize(text, colors.gray),
|
|
41
|
-
|
|
42
|
-
success: (text: string) => colorize(text, colors.green),
|
|
43
|
-
error: (text: string) => colorize(text, colors.red),
|
|
44
|
-
warn: (text: string) => colorize(text, colors.yellow),
|
|
45
|
-
info: (text: string) => colorize(text, colors.cyan),
|
|
46
|
-
path: (text: string) => colorize(text, colors.blue),
|
|
47
|
-
branch: (text: string) => colorize(text, colors.magenta),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export function log(message: string): void {
|
|
51
|
-
console.log(message);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function success(message: string): void {
|
|
55
|
-
console.log(fmt.green('✓') + ' ' + message);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function error(message: string): void {
|
|
59
|
-
console.error(fmt.red('✗') + ' ' + message);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function warn(message: string): void {
|
|
63
|
-
console.log(fmt.yellow('⚠') + ' ' + message);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function info(message: string): void {
|
|
67
|
-
console.log(fmt.cyan('ℹ') + ' ' + message);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function header(message: string): void {
|
|
71
|
-
console.log('\n' + fmt.bold(message));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function list(items: string[], prefix = ' '): void {
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
console.log(prefix + '• ' + item);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function table(rows: string[][], columnWidths?: number[]): void {
|
|
81
|
-
if (rows.length === 0) return;
|
|
82
|
-
|
|
83
|
-
// Calculate column widths if not provided
|
|
84
|
-
const widths = columnWidths ?? rows[0].map((_, i) =>
|
|
85
|
-
Math.max(...rows.map(row => (row[i] ?? '').length))
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
for (const row of rows) {
|
|
89
|
-
const paddedCells = row.map((cell, i) =>
|
|
90
|
-
(cell ?? '').padEnd(widths[i] ?? 0)
|
|
91
|
-
);
|
|
92
|
-
console.log(' ' + paddedCells.join(' '));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function json(data: unknown): void {
|
|
97
|
-
console.log(JSON.stringify(data, null, 2));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function newline(): void {
|
|
101
|
-
console.log();
|
|
102
|
-
}
|
package/src/lib/paths.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path utilities for cry
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Sanitize a branch name for use in filesystem paths
|
|
9
|
-
* - Replace slashes with double dashes
|
|
10
|
-
* - Remove or replace other problematic characters
|
|
11
|
-
*/
|
|
12
|
-
export function sanitizeBranchName(branch: string): string {
|
|
13
|
-
return branch
|
|
14
|
-
.replace(/\//g, '--') // Replace slashes with double dashes
|
|
15
|
-
.replace(/[<>:"|?*\\]/g, '-') // Replace Windows-forbidden chars
|
|
16
|
-
.replace(/\s+/g, '-') // Replace whitespace
|
|
17
|
-
.replace(/^\.+/, '') // Remove leading dots
|
|
18
|
-
.replace(/\.+$/, '') // Remove trailing dots
|
|
19
|
-
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
20
|
-
.replace(/^-+/, '') // Remove leading dashes
|
|
21
|
-
.replace(/-+$/, ''); // Remove trailing dashes
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Calculate the default worktree path
|
|
26
|
-
*/
|
|
27
|
-
export function getDefaultWorktreePath(
|
|
28
|
-
repoRoot: string,
|
|
29
|
-
branch: string,
|
|
30
|
-
options?: {
|
|
31
|
-
explicitPath?: string;
|
|
32
|
-
baseDir?: string;
|
|
33
|
-
repoName?: string;
|
|
34
|
-
}
|
|
35
|
-
): string {
|
|
36
|
-
// Explicit path wins
|
|
37
|
-
if (options?.explicitPath) {
|
|
38
|
-
// If it's relative, resolve against CWD
|
|
39
|
-
if (!path.isAbsolute(options.explicitPath)) {
|
|
40
|
-
return path.resolve(options.explicitPath);
|
|
41
|
-
}
|
|
42
|
-
return options.explicitPath;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const sanitizedBranch = sanitizeBranchName(branch);
|
|
46
|
-
|
|
47
|
-
// Base directory specified
|
|
48
|
-
if (options?.baseDir) {
|
|
49
|
-
const repoName = options.repoName ?? path.basename(repoRoot);
|
|
50
|
-
const baseDir = path.isAbsolute(options.baseDir)
|
|
51
|
-
? options.baseDir
|
|
52
|
-
: path.resolve(repoRoot, options.baseDir);
|
|
53
|
-
return path.join(baseDir, repoName, sanitizedBranch);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Default: .worktrees/<branch> inside repo
|
|
57
|
-
return path.join(repoRoot, '.worktrees', sanitizedBranch);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check if a path is inside the .worktrees directory
|
|
62
|
-
*/
|
|
63
|
-
export function isInsideWorktreesDir(targetPath: string, repoRoot: string): boolean {
|
|
64
|
-
const worktreesDir = path.join(repoRoot, '.worktrees');
|
|
65
|
-
const normalizedTarget = path.normalize(targetPath);
|
|
66
|
-
const normalizedWorktrees = path.normalize(worktreesDir);
|
|
67
|
-
return normalizedTarget.startsWith(normalizedWorktrees);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get relative path from repo root
|
|
72
|
-
*/
|
|
73
|
-
export function getRelativePath(absolutePath: string, repoRoot: string): string {
|
|
74
|
-
return path.relative(repoRoot, absolutePath);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Resolve a branch-or-path argument to a worktree path
|
|
79
|
-
*/
|
|
80
|
-
export function resolveBranchOrPath(
|
|
81
|
-
branchOrPath: string,
|
|
82
|
-
worktrees: Array<{ branch: string | null; path: string }>,
|
|
83
|
-
repoRoot: string
|
|
84
|
-
): { path: string; branch: string | null } | null {
|
|
85
|
-
// First, try to match by branch name
|
|
86
|
-
const byBranch = worktrees.find((w) => w.branch === branchOrPath);
|
|
87
|
-
if (byBranch) {
|
|
88
|
-
return { path: byBranch.path, branch: byBranch.branch };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Try to match by path (absolute or relative)
|
|
92
|
-
const absolutePath = path.isAbsolute(branchOrPath)
|
|
93
|
-
? branchOrPath
|
|
94
|
-
: path.resolve(repoRoot, branchOrPath);
|
|
95
|
-
|
|
96
|
-
const byPath = worktrees.find((w) => path.normalize(w.path) === path.normalize(absolutePath));
|
|
97
|
-
if (byPath) {
|
|
98
|
-
return { path: byPath.path, branch: byPath.branch };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Try partial path match (end of path)
|
|
102
|
-
const byPartialPath = worktrees.find((w) => w.path.endsWith(branchOrPath));
|
|
103
|
-
if (byPartialPath) {
|
|
104
|
-
return { path: byPartialPath.path, branch: byPartialPath.branch };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return null;
|
|
108
|
-
}
|
package/src/lib/secrets.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Secret file handling for cry
|
|
3
|
-
*
|
|
4
|
-
* This module ensures that only git-ignored files are ever copied or symlinked.
|
|
5
|
-
* It provides a safety layer to prevent accidentally exposing tracked files.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, copyFileSync, symlinkSync, mkdirSync, statSync, readdirSync } from 'node:fs';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
import { glob } from 'glob';
|
|
11
|
-
import { isTracked, isIgnored } from './git.js';
|
|
12
|
-
import type { SecretMode } from './types.js';
|
|
13
|
-
|
|
14
|
-
export interface FileCheckResult {
|
|
15
|
-
path: string;
|
|
16
|
-
exists: boolean;
|
|
17
|
-
isTracked: boolean;
|
|
18
|
-
isIgnored: boolean;
|
|
19
|
-
safe: boolean;
|
|
20
|
-
reason?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check if a file is safe to copy/symlink
|
|
25
|
-
* A file is safe if:
|
|
26
|
-
* 1. It exists
|
|
27
|
-
* 2. It is NOT tracked by git
|
|
28
|
-
* 3. It IS ignored by git
|
|
29
|
-
*/
|
|
30
|
-
export function checkFileSafety(filePath: string, repoRoot: string): FileCheckResult {
|
|
31
|
-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
|
|
32
|
-
const relativePath = path.relative(repoRoot, absolutePath);
|
|
33
|
-
|
|
34
|
-
const result: FileCheckResult = {
|
|
35
|
-
path: relativePath,
|
|
36
|
-
exists: existsSync(absolutePath),
|
|
37
|
-
isTracked: false,
|
|
38
|
-
isIgnored: false,
|
|
39
|
-
safe: false,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
if (!result.exists) {
|
|
43
|
-
result.reason = 'File does not exist';
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
result.isTracked = isTracked(relativePath, repoRoot);
|
|
48
|
-
if (result.isTracked) {
|
|
49
|
-
result.reason = 'File is tracked by git (would be committed)';
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
result.isIgnored = isIgnored(relativePath, repoRoot);
|
|
54
|
-
if (!result.isIgnored) {
|
|
55
|
-
result.reason = 'File is not ignored by git (could be accidentally committed)';
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
result.safe = true;
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Expand glob patterns to actual file paths
|
|
65
|
-
*/
|
|
66
|
-
export async function expandIncludePatterns(
|
|
67
|
-
patterns: string[],
|
|
68
|
-
repoRoot: string
|
|
69
|
-
): Promise<string[]> {
|
|
70
|
-
const allFiles: Set<string> = new Set();
|
|
71
|
-
|
|
72
|
-
for (const pattern of patterns) {
|
|
73
|
-
try {
|
|
74
|
-
const matches = await glob(pattern, {
|
|
75
|
-
cwd: repoRoot,
|
|
76
|
-
dot: true,
|
|
77
|
-
nodir: true,
|
|
78
|
-
});
|
|
79
|
-
for (const match of matches) {
|
|
80
|
-
allFiles.add(match);
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// If glob fails, treat as literal path
|
|
84
|
-
if (existsSync(path.join(repoRoot, pattern))) {
|
|
85
|
-
allFiles.add(pattern);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return Array.from(allFiles).sort();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Get all safe files from include patterns
|
|
95
|
-
*/
|
|
96
|
-
export async function getSafeFiles(
|
|
97
|
-
patterns: string[],
|
|
98
|
-
repoRoot: string
|
|
99
|
-
): Promise<{ safe: FileCheckResult[]; unsafe: FileCheckResult[] }> {
|
|
100
|
-
const files = await expandIncludePatterns(patterns, repoRoot);
|
|
101
|
-
const safe: FileCheckResult[] = [];
|
|
102
|
-
const unsafe: FileCheckResult[] = [];
|
|
103
|
-
|
|
104
|
-
for (const file of files) {
|
|
105
|
-
const result = checkFileSafety(file, repoRoot);
|
|
106
|
-
if (result.safe) {
|
|
107
|
-
safe.push(result);
|
|
108
|
-
} else if (result.exists) {
|
|
109
|
-
// Only report unsafe if file actually exists
|
|
110
|
-
unsafe.push(result);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { safe, unsafe };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Copy a file to the target directory, preserving relative path
|
|
119
|
-
*/
|
|
120
|
-
export function copyFile(
|
|
121
|
-
relativePath: string,
|
|
122
|
-
sourceRoot: string,
|
|
123
|
-
targetRoot: string
|
|
124
|
-
): void {
|
|
125
|
-
const sourcePath = path.join(sourceRoot, relativePath);
|
|
126
|
-
const targetPath = path.join(targetRoot, relativePath);
|
|
127
|
-
|
|
128
|
-
// Create parent directories if needed
|
|
129
|
-
const targetDir = path.dirname(targetPath);
|
|
130
|
-
if (!existsSync(targetDir)) {
|
|
131
|
-
mkdirSync(targetDir, { recursive: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
copyFileSync(sourcePath, targetPath);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Create a symlink in the target directory pointing to source
|
|
139
|
-
*/
|
|
140
|
-
export function createSymlink(
|
|
141
|
-
relativePath: string,
|
|
142
|
-
sourceRoot: string,
|
|
143
|
-
targetRoot: string
|
|
144
|
-
): void {
|
|
145
|
-
const sourcePath = path.join(sourceRoot, relativePath);
|
|
146
|
-
const targetPath = path.join(targetRoot, relativePath);
|
|
147
|
-
|
|
148
|
-
// Create parent directories if needed
|
|
149
|
-
const targetDir = path.dirname(targetPath);
|
|
150
|
-
if (!existsSync(targetDir)) {
|
|
151
|
-
mkdirSync(targetDir, { recursive: true });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Use absolute path for symlink target for reliability
|
|
155
|
-
symlinkSync(sourcePath, targetPath);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Process files according to mode (copy or symlink)
|
|
160
|
-
*/
|
|
161
|
-
export async function processSecrets(
|
|
162
|
-
mode: SecretMode,
|
|
163
|
-
patterns: string[],
|
|
164
|
-
sourceRoot: string,
|
|
165
|
-
targetRoot: string
|
|
166
|
-
): Promise<{ processed: string[]; skipped: FileCheckResult[] }> {
|
|
167
|
-
if (mode === 'none') {
|
|
168
|
-
return { processed: [], skipped: [] };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const { safe, unsafe } = await getSafeFiles(patterns, sourceRoot);
|
|
172
|
-
const processed: string[] = [];
|
|
173
|
-
|
|
174
|
-
for (const file of safe) {
|
|
175
|
-
try {
|
|
176
|
-
if (mode === 'copy') {
|
|
177
|
-
copyFile(file.path, sourceRoot, targetRoot);
|
|
178
|
-
} else if (mode === 'symlink') {
|
|
179
|
-
createSymlink(file.path, sourceRoot, targetRoot);
|
|
180
|
-
}
|
|
181
|
-
processed.push(file.path);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
// Add to skipped with error reason
|
|
184
|
-
unsafe.push({
|
|
185
|
-
...file,
|
|
186
|
-
safe: false,
|
|
187
|
-
reason: `Failed to ${mode}: ${(error as Error).message}`,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return { processed, skipped: unsafe };
|
|
193
|
-
}
|
package/src/lib/types.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cry Configuration Types
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface CryConfig {
|
|
6
|
-
/** Base directory for worktrees (optional, defaults to .worktrees/) */
|
|
7
|
-
worktreeBaseDir?: string;
|
|
8
|
-
/** Default mode for secrets handling */
|
|
9
|
-
defaultMode: 'copy' | 'symlink' | 'none';
|
|
10
|
-
/** List of globs/paths to manage (e.g. [".env", ".env.*", "config/oauth*.json"]) */
|
|
11
|
-
include: string[];
|
|
12
|
-
/** Hook commands */
|
|
13
|
-
hooks?: {
|
|
14
|
-
postCreate?: string[];
|
|
15
|
-
};
|
|
16
|
-
/** Default agent command */
|
|
17
|
-
agentCommand?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CryLocalConfig {
|
|
21
|
-
/** Machine-specific base directory override */
|
|
22
|
-
worktreeBaseDir?: string;
|
|
23
|
-
/** Additional include paths for this machine */
|
|
24
|
-
include?: string[];
|
|
25
|
-
/** Additional hooks for this machine */
|
|
26
|
-
hooks?: {
|
|
27
|
-
postCreate?: string[];
|
|
28
|
-
};
|
|
29
|
-
/** Override agent command */
|
|
30
|
-
agentCommand?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface MergedConfig {
|
|
34
|
-
worktreeBaseDir?: string;
|
|
35
|
-
defaultMode: 'copy' | 'symlink' | 'none';
|
|
36
|
-
include: string[];
|
|
37
|
-
hooks: {
|
|
38
|
-
postCreate: string[];
|
|
39
|
-
};
|
|
40
|
-
agentCommand: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface WorktreeInfo {
|
|
44
|
-
worktree: string;
|
|
45
|
-
head: string;
|
|
46
|
-
branch?: string;
|
|
47
|
-
bare?: boolean;
|
|
48
|
-
detached?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface WorktreeListItem {
|
|
52
|
-
branch: string | null;
|
|
53
|
-
path: string;
|
|
54
|
-
headShort: string;
|
|
55
|
-
dirty: boolean;
|
|
56
|
-
lastModified: Date | null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export type SecretMode = 'copy' | 'symlink' | 'none';
|
|
60
|
-
|
|
61
|
-
export interface SpawnOptions {
|
|
62
|
-
branch: string;
|
|
63
|
-
isNew: boolean;
|
|
64
|
-
path?: string;
|
|
65
|
-
base?: string;
|
|
66
|
-
mode: SecretMode;
|
|
67
|
-
run?: string;
|
|
68
|
-
agent?: 'claude' | 'none';
|
|
69
|
-
}
|