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.
Files changed (91) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +203 -300
  3. package/dist/commands/completions.d.ts +16 -0
  4. package/dist/commands/completions.d.ts.map +1 -0
  5. package/dist/commands/completions.js +46 -0
  6. package/dist/commands/completions.js.map +1 -0
  7. package/dist/commands/explain-copy.d.ts +11 -0
  8. package/dist/commands/explain-copy.d.ts.map +1 -0
  9. package/dist/commands/explain-copy.js +34 -0
  10. package/dist/commands/explain-copy.js.map +1 -0
  11. package/dist/commands/finish.d.ts +20 -0
  12. package/dist/commands/finish.d.ts.map +1 -0
  13. package/dist/commands/finish.js +817 -0
  14. package/dist/commands/finish.js.map +1 -0
  15. package/dist/commands/gc.d.ts +22 -0
  16. package/dist/commands/gc.d.ts.map +1 -0
  17. package/dist/commands/gc.js +163 -0
  18. package/dist/commands/gc.js.map +1 -0
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +1 -0
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/open.d.ts +15 -1
  23. package/dist/commands/open.d.ts.map +1 -1
  24. package/dist/commands/open.js +96 -17
  25. package/dist/commands/open.js.map +1 -1
  26. package/dist/commands/resume.d.ts +21 -0
  27. package/dist/commands/resume.d.ts.map +1 -0
  28. package/dist/commands/resume.js +106 -0
  29. package/dist/commands/resume.js.map +1 -0
  30. package/dist/commands/rm.d.ts.map +1 -1
  31. package/dist/commands/rm.js +6 -14
  32. package/dist/commands/rm.js.map +1 -1
  33. package/dist/commands/spawn.d.ts +3 -0
  34. package/dist/commands/spawn.d.ts.map +1 -1
  35. package/dist/commands/spawn.js +182 -19
  36. package/dist/commands/spawn.js.map +1 -1
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +198 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/lib/completions.d.ts +35 -0
  42. package/dist/lib/completions.d.ts.map +1 -0
  43. package/dist/lib/completions.js +368 -0
  44. package/dist/lib/completions.js.map +1 -0
  45. package/dist/lib/config.d.ts.map +1 -1
  46. package/dist/lib/config.js +2 -0
  47. package/dist/lib/config.js.map +1 -1
  48. package/dist/lib/errors.d.ts +43 -0
  49. package/dist/lib/errors.d.ts.map +1 -0
  50. package/dist/lib/errors.js +251 -0
  51. package/dist/lib/errors.js.map +1 -0
  52. package/dist/lib/git.d.ts +17 -0
  53. package/dist/lib/git.d.ts.map +1 -1
  54. package/dist/lib/git.js +78 -10
  55. package/dist/lib/git.js.map +1 -1
  56. package/dist/lib/safety.d.ts +79 -0
  57. package/dist/lib/safety.d.ts.map +1 -0
  58. package/dist/lib/safety.js +133 -0
  59. package/dist/lib/safety.js.map +1 -0
  60. package/dist/lib/secrets.d.ts +29 -0
  61. package/dist/lib/secrets.d.ts.map +1 -1
  62. package/dist/lib/secrets.js +115 -0
  63. package/dist/lib/secrets.js.map +1 -1
  64. package/dist/lib/session.d.ts +93 -0
  65. package/dist/lib/session.d.ts.map +1 -0
  66. package/dist/lib/session.js +254 -0
  67. package/dist/lib/session.js.map +1 -0
  68. package/dist/lib/types.d.ts +6 -1
  69. package/dist/lib/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/.claude/settings.local.json +0 -7
  72. package/src/commands/doctor.ts +0 -222
  73. package/src/commands/init.ts +0 -120
  74. package/src/commands/list.ts +0 -133
  75. package/src/commands/open.ts +0 -78
  76. package/src/commands/prune.ts +0 -36
  77. package/src/commands/rm.ts +0 -125
  78. package/src/commands/shell.ts +0 -99
  79. package/src/commands/spawn.ts +0 -169
  80. package/src/index.ts +0 -123
  81. package/src/lib/config.ts +0 -120
  82. package/src/lib/git.ts +0 -243
  83. package/src/lib/output.ts +0 -102
  84. package/src/lib/paths.ts +0 -108
  85. package/src/lib/secrets.ts +0 -193
  86. package/src/lib/types.ts +0 -69
  87. package/tests/config.test.ts +0 -102
  88. package/tests/paths.test.ts +0 -155
  89. package/tests/secrets.test.ts +0 -150
  90. package/tsconfig.json +0 -20
  91. package/vitest.config.ts +0 -15
@@ -1,99 +0,0 @@
1
- /**
2
- * cry shell command
3
- *
4
- * Output shell integration code for easy directory navigation.
5
- */
6
-
7
- import * as out from '../lib/output.js';
8
-
9
- type ShellType = 'bash' | 'zsh' | 'fish';
10
-
11
- const SHELL_FUNCTIONS: Record<ShellType, string> = {
12
- bash: `# cry shell integration (add to ~/.bashrc)
13
- crycd() {
14
- local target
15
- target="$(cry open "$1" --path-only 2>/dev/null)"
16
- if [ -n "$target" ]; then
17
- cd "$target"
18
- else
19
- echo "Worktree not found: $1" >&2
20
- return 1
21
- fi
22
- }
23
-
24
- # Auto-completion for crycd
25
- _crycd_completions() {
26
- local branches
27
- branches=$(cry list --json 2>/dev/null | grep -o '"branch":"[^"]*"' | cut -d'"' -f4)
28
- COMPREPLY=($(compgen -W "$branches" -- "\${COMP_WORDS[COMP_CWORD]}"))
29
- }
30
- complete -F _crycd_completions crycd
31
- `,
32
-
33
- zsh: `# cry shell integration (add to ~/.zshrc)
34
- crycd() {
35
- local target
36
- target="$(cry open "$1" --path-only 2>/dev/null)"
37
- if [[ -n "$target" ]]; then
38
- cd "$target"
39
- else
40
- echo "Worktree not found: $1" >&2
41
- return 1
42
- fi
43
- }
44
-
45
- # Auto-completion for crycd
46
- _crycd() {
47
- local branches
48
- branches=(\${(f)"$(cry list --json 2>/dev/null | grep -o '"branch":"[^"]*"' | cut -d'"' -f4)"})
49
- _describe 'branch' branches
50
- }
51
- compdef _crycd crycd
52
- `,
53
-
54
- fish: `# cry shell integration (add to ~/.config/fish/config.fish)
55
- function crycd
56
- set -l target (cry open $argv[1] --path-only 2>/dev/null)
57
- if test -n "$target"
58
- cd $target
59
- else
60
- echo "Worktree not found: $argv[1]" >&2
61
- return 1
62
- end
63
- end
64
-
65
- # Auto-completion for crycd
66
- complete -c crycd -f -a "(cry list --json 2>/dev/null | grep -o '\"branch\":\"[^\"]*\"' | cut -d'\"' -f4)"
67
- `,
68
- };
69
-
70
- interface ShellOptions {
71
- shell?: string;
72
- }
73
-
74
- export async function shell(options: ShellOptions): Promise<void> {
75
- const shellEnv = process.env.SHELL ?? '';
76
- let detectedShell: ShellType = 'bash';
77
-
78
- if (options.shell) {
79
- if (options.shell === 'fish' || options.shell === 'zsh' || options.shell === 'bash') {
80
- detectedShell = options.shell;
81
- } else {
82
- out.error(`Unsupported shell: ${options.shell}`);
83
- out.info('Supported shells: bash, zsh, fish');
84
- process.exit(1);
85
- }
86
- } else if (shellEnv.includes('zsh')) {
87
- detectedShell = 'zsh';
88
- } else if (shellEnv.includes('fish')) {
89
- detectedShell = 'fish';
90
- }
91
-
92
- const script = SHELL_FUNCTIONS[detectedShell];
93
-
94
- out.log(out.fmt.dim(`# Shell integration for ${detectedShell}`));
95
- out.log(out.fmt.dim('# Copy and paste into your shell config, or run:'));
96
- out.log(out.fmt.dim(`# cry shell >> ~/.${detectedShell === 'fish' ? 'config/fish/config.fish' : detectedShell + 'rc'}`));
97
- out.newline();
98
- console.log(script);
99
- }
@@ -1,169 +0,0 @@
1
- /**
2
- * cry spawn command
3
- *
4
- * Create a worktree for a branch with optional secrets handling and hooks.
5
- */
6
-
7
- import { existsSync } from 'node:fs';
8
- import path from 'node:path';
9
- import {
10
- isGitRepo,
11
- getRepoRoot,
12
- getRepoName,
13
- branchExists,
14
- addWorktree,
15
- listWorktrees,
16
- runCommand,
17
- commandExists,
18
- } from '../lib/git.js';
19
- import { getMergedConfig, configExists } from '../lib/config.js';
20
- import { getDefaultWorktreePath } from '../lib/paths.js';
21
- import { processSecrets } from '../lib/secrets.js';
22
- import * as out from '../lib/output.js';
23
- import type { SecretMode } from '../lib/types.js';
24
-
25
- interface SpawnOptions {
26
- new?: boolean;
27
- path?: string;
28
- base?: string;
29
- mode?: SecretMode;
30
- run?: string;
31
- agent?: string;
32
- }
33
-
34
- export async function spawn(branch: string, options: SpawnOptions): Promise<void> {
35
- // Check if we're in a git repo
36
- if (!isGitRepo()) {
37
- out.error('Not a git repository. Run this command from within a git repo.');
38
- process.exit(1);
39
- }
40
-
41
- const repoRoot = getRepoRoot();
42
- const repoName = getRepoName();
43
-
44
- // Load config
45
- const config = configExists(repoRoot) ? getMergedConfig(repoRoot) : {
46
- worktreeBaseDir: undefined,
47
- defaultMode: 'none' as SecretMode,
48
- include: [],
49
- hooks: { postCreate: [] },
50
- agentCommand: 'claude',
51
- };
52
-
53
- // Determine mode
54
- const mode: SecretMode = options.mode ?? config.defaultMode;
55
-
56
- // Calculate worktree path
57
- const worktreePath = getDefaultWorktreePath(repoRoot, branch, {
58
- explicitPath: options.path,
59
- baseDir: options.base ?? config.worktreeBaseDir,
60
- repoName,
61
- });
62
-
63
- // Check if destination already exists
64
- if (existsSync(worktreePath)) {
65
- out.error(`Destination already exists: ${worktreePath}`);
66
- out.info('Remove it first or choose a different path with --path');
67
- process.exit(1);
68
- }
69
-
70
- // Check if worktree already exists for this branch
71
- const existingWorktrees = listWorktrees(repoRoot);
72
- const existingForBranch = existingWorktrees.find((w) => w.branch === branch);
73
- if (existingForBranch) {
74
- out.error(`A worktree already exists for branch '${branch}'`);
75
- out.info(`Path: ${existingForBranch.worktree}`);
76
- out.info('Remove it first with: cry rm ' + branch);
77
- process.exit(1);
78
- }
79
-
80
- // Determine if we need to create the branch
81
- const needsNewBranch = options.new || !branchExists(branch, repoRoot);
82
-
83
- out.header('Creating worktree');
84
- out.log(` Branch: ${out.fmt.branch(branch)}${needsNewBranch ? out.fmt.gray(' (new)') : ''}`);
85
- out.log(` Path: ${out.fmt.path(worktreePath)}`);
86
- out.log(` Mode: ${out.fmt.cyan(mode)}`);
87
- out.newline();
88
-
89
- // Create the worktree
90
- try {
91
- addWorktree(worktreePath, branch, needsNewBranch, repoRoot);
92
- out.success('Worktree created');
93
- } catch (error) {
94
- out.error(`Failed to create worktree: ${(error as Error).message}`);
95
- process.exit(1);
96
- }
97
-
98
- // Handle secrets
99
- if (mode !== 'none' && config.include.length > 0) {
100
- out.newline();
101
- out.log(`Processing secrets (${mode} mode)...`);
102
-
103
- const { processed, skipped } = await processSecrets(
104
- mode,
105
- config.include,
106
- repoRoot,
107
- worktreePath
108
- );
109
-
110
- if (processed.length > 0) {
111
- out.success(`${mode === 'copy' ? 'Copied' : 'Symlinked'} ${processed.length} file(s):`);
112
- for (const file of processed) {
113
- out.log(` ${out.fmt.dim('•')} ${file}`);
114
- }
115
- }
116
-
117
- if (skipped.length > 0) {
118
- out.warn(`Skipped ${skipped.length} file(s) for safety:`);
119
- for (const file of skipped) {
120
- out.log(` ${out.fmt.dim('•')} ${file.path}: ${file.reason}`);
121
- }
122
- }
123
- }
124
-
125
- // Run post-create hooks from config
126
- const hooks = config.hooks.postCreate;
127
- if (hooks.length > 0) {
128
- out.newline();
129
- out.log('Running post-create hooks...');
130
- for (const hook of hooks) {
131
- out.log(` ${out.fmt.dim('$')} ${hook}`);
132
- const code = await runCommand(hook, worktreePath);
133
- if (code !== 0) {
134
- out.warn(`Hook exited with code ${code}`);
135
- }
136
- }
137
- }
138
-
139
- // Run --run command if provided
140
- if (options.run) {
141
- out.newline();
142
- out.log('Running custom command...');
143
- out.log(` ${out.fmt.dim('$')} ${options.run}`);
144
- const code = await runCommand(options.run, worktreePath);
145
- if (code !== 0) {
146
- out.warn(`Command exited with code ${code}`);
147
- }
148
- }
149
-
150
- // Handle agent launch
151
- const agentChoice = options.agent ?? 'none';
152
- if (agentChoice === 'claude') {
153
- const agentCmd = config.agentCommand;
154
- out.newline();
155
-
156
- if (commandExists(agentCmd)) {
157
- out.log(`Launching ${agentCmd}...`);
158
- await runCommand(agentCmd, worktreePath);
159
- } else {
160
- out.warn(`Agent command '${agentCmd}' not found.`);
161
- out.info('Install Claude Code: https://docs.anthropic.com/claude-code');
162
- }
163
- }
164
-
165
- // Final summary
166
- out.newline();
167
- out.header('Worktree ready');
168
- out.log(` ${out.fmt.dim('cd')} ${worktreePath}`);
169
- }
package/src/index.ts DELETED
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * cry - Git worktrees made painless for vibecoders
4
- *
5
- * A CLI tool for managing git worktrees with parallel AI-agent sessions in mind.
6
- */
7
-
8
- import { Command } from 'commander';
9
- import { init } from './commands/init.js';
10
- import { spawn } from './commands/spawn.js';
11
- import { list } from './commands/list.js';
12
- import { open } from './commands/open.js';
13
- import { rm } from './commands/rm.js';
14
- import { prune } from './commands/prune.js';
15
- import { doctor } from './commands/doctor.js';
16
- import { shell } from './commands/shell.js';
17
- import type { SecretMode } from './lib/types.js';
18
-
19
- const program = new Command();
20
-
21
- program
22
- .name('cry')
23
- .description('Git worktrees made painless for vibecoders running parallel AI-agent sessions')
24
- .version('1.0.0');
25
-
26
- // cry init
27
- program
28
- .command('init')
29
- .description('Initialize cry configuration in the current repository')
30
- .option('-f, --force', 'Overwrite existing configuration')
31
- .action(async (options) => {
32
- await init({ force: options.force });
33
- });
34
-
35
- // cry spawn <branch>
36
- program
37
- .command('spawn <branch>')
38
- .description('Create a worktree for a branch')
39
- .option('-n, --new', 'Create a new branch (equivalent to git worktree add -b)')
40
- .option('-p, --path <dir>', 'Explicit path for the worktree')
41
- .option('-b, --base <dir>', 'Base directory for worktrees')
42
- .option('-m, --mode <mode>', 'Secret handling mode: copy, symlink, or none', 'copy')
43
- .option('-r, --run <cmd>', 'Command to run after creating worktree')
44
- .option('-a, --agent <agent>', 'Launch agent after setup: claude or none', 'none')
45
- .action(async (branch: string, options) => {
46
- const mode = options.mode as SecretMode;
47
- if (!['copy', 'symlink', 'none'].includes(mode)) {
48
- console.error(`Invalid mode: ${mode}. Must be 'copy', 'symlink', or 'none'.`);
49
- process.exit(1);
50
- }
51
- await spawn(branch, {
52
- new: options.new,
53
- path: options.path,
54
- base: options.base,
55
- mode,
56
- run: options.run,
57
- agent: options.agent,
58
- });
59
- });
60
-
61
- // cry list
62
- program
63
- .command('list')
64
- .alias('ls')
65
- .description('List all worktrees with their status')
66
- .option('-j, --json', 'Output as JSON')
67
- .action(async (options) => {
68
- await list({ json: options.json });
69
- });
70
-
71
- // cry open <branch-or-path>
72
- program
73
- .command('open <branch-or-path>')
74
- .description('Open or navigate to a worktree by branch name or path')
75
- .option('-c, --cmd <cmd>', 'Command to execute in the worktree directory')
76
- .option('-p, --path-only', 'Only print the path (for scripting)')
77
- .action(async (branchOrPath: string, options) => {
78
- await open(branchOrPath, { cmd: options.cmd, pathOnly: options.pathOnly });
79
- });
80
-
81
- // cry rm <branch-or-path>
82
- program
83
- .command('rm <branch-or-path>')
84
- .alias('remove')
85
- .description('Remove a worktree safely')
86
- .option('-b, --with-branch', 'Also delete the branch')
87
- .option('-f, --force', 'Force removal even if dirty')
88
- .option('-y, --yes', 'Skip confirmation prompts')
89
- .action(async (branchOrPath: string, options) => {
90
- await rm(branchOrPath, {
91
- withBranch: options.withBranch,
92
- force: options.force,
93
- yes: options.yes,
94
- });
95
- });
96
-
97
- // cry prune
98
- program
99
- .command('prune')
100
- .description('Clean up stale worktree references')
101
- .action(async () => {
102
- await prune();
103
- });
104
-
105
- // cry doctor
106
- program
107
- .command('doctor')
108
- .description('Check and diagnose cry configuration and setup')
109
- .action(async () => {
110
- await doctor();
111
- });
112
-
113
- // cry shell
114
- program
115
- .command('shell')
116
- .description('Output shell integration code for crycd navigation function')
117
- .option('-s, --shell <shell>', 'Shell type: bash, zsh, or fish (auto-detected)')
118
- .action(async (options) => {
119
- await shell({ shell: options.shell });
120
- });
121
-
122
- // Parse and execute
123
- program.parse();
package/src/lib/config.ts DELETED
@@ -1,120 +0,0 @@
1
- /**
2
- * Configuration management for cry
3
- */
4
-
5
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
- import path from 'node:path';
7
- import type { MergedConfig, CryConfig, CryLocalConfig } from './types.js';
8
-
9
- export const CONFIG_FILE = '.cry.json';
10
- export const LOCAL_CONFIG_FILE = '.cry.local.json';
11
- export const WORKTREE_INCLUDE_FILE = '.worktreeinclude';
12
-
13
- const DEFAULT_CONFIG: CryConfig = {
14
- defaultMode: 'copy',
15
- include: ['.env', '.env.*', '.env.local'],
16
- hooks: {
17
- postCreate: [],
18
- },
19
- agentCommand: 'claude',
20
- };
21
-
22
- /**
23
- * Load the main config file
24
- */
25
- export function loadConfig(repoRoot: string): CryConfig | null {
26
- const configPath = path.join(repoRoot, CONFIG_FILE);
27
- if (!existsSync(configPath)) {
28
- return null;
29
- }
30
- try {
31
- const content = readFileSync(configPath, 'utf-8');
32
- return JSON.parse(content) as CryConfig;
33
- } catch (error) {
34
- throw new Error(`Failed to parse ${CONFIG_FILE}: ${(error as Error).message}`);
35
- }
36
- }
37
-
38
- /**
39
- * Load the local config file
40
- */
41
- export function loadLocalConfig(repoRoot: string): CryLocalConfig | null {
42
- const configPath = path.join(repoRoot, LOCAL_CONFIG_FILE);
43
- if (!existsSync(configPath)) {
44
- return null;
45
- }
46
- try {
47
- const content = readFileSync(configPath, 'utf-8');
48
- return JSON.parse(content) as CryLocalConfig;
49
- } catch (error) {
50
- throw new Error(`Failed to parse ${LOCAL_CONFIG_FILE}: ${(error as Error).message}`);
51
- }
52
- }
53
-
54
- /**
55
- * Merge main config with local overrides
56
- */
57
- export function mergeConfig(config: CryConfig | null, localConfig: CryLocalConfig | null): MergedConfig {
58
- const base = config ?? DEFAULT_CONFIG;
59
-
60
- return {
61
- worktreeBaseDir: localConfig?.worktreeBaseDir ?? base.worktreeBaseDir,
62
- defaultMode: base.defaultMode,
63
- include: [...(base.include ?? []), ...(localConfig?.include ?? [])],
64
- hooks: {
65
- postCreate: [
66
- ...(base.hooks?.postCreate ?? []),
67
- ...(localConfig?.hooks?.postCreate ?? []),
68
- ],
69
- },
70
- agentCommand: localConfig?.agentCommand ?? base.agentCommand ?? 'claude',
71
- };
72
- }
73
-
74
- /**
75
- * Get merged configuration
76
- */
77
- export function getMergedConfig(repoRoot: string): MergedConfig {
78
- const config = loadConfig(repoRoot);
79
- const localConfig = loadLocalConfig(repoRoot);
80
- return mergeConfig(config, localConfig);
81
- }
82
-
83
- /**
84
- * Save the main config file
85
- */
86
- export function saveConfig(repoRoot: string, config: CryConfig): void {
87
- const configPath = path.join(repoRoot, CONFIG_FILE);
88
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
89
- }
90
-
91
- /**
92
- * Save the local config file
93
- */
94
- export function saveLocalConfig(repoRoot: string, config: CryLocalConfig): void {
95
- const configPath = path.join(repoRoot, LOCAL_CONFIG_FILE);
96
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
97
- }
98
-
99
- /**
100
- * Check if config exists
101
- */
102
- export function configExists(repoRoot: string): boolean {
103
- return existsSync(path.join(repoRoot, CONFIG_FILE));
104
- }
105
-
106
- /**
107
- * Get default config
108
- */
109
- export function getDefaultConfig(): CryConfig {
110
- return { ...DEFAULT_CONFIG };
111
- }
112
-
113
- /**
114
- * Create default local config
115
- */
116
- export function getDefaultLocalConfig(): CryLocalConfig {
117
- return {
118
- include: [],
119
- };
120
- }