yggtree 1.0.1 โ†’ 1.1.1

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/dist/index.js CHANGED
@@ -8,11 +8,15 @@ import { createCommandMulti } from './commands/wt/create-multi.js';
8
8
  import { deleteCommand } from './commands/wt/delete.js';
9
9
  import { bootstrapCommand } from './commands/wt/bootstrap.js';
10
10
  import { pruneCommand } from './commands/wt/prune.js';
11
+ import { execCommand } from './commands/wt/exec.js';
12
+ import { enterCommand } from './commands/wt/enter.js';
13
+ import { pathCommand } from './commands/wt/path.js';
14
+ import { getVersion } from './lib/version.js';
11
15
  const program = new Command();
12
16
  program
13
17
  .name('yggtree')
14
18
  .description('Interactive CLI for managing git worktrees and configs')
15
- .version('1.0.0')
19
+ .version(getVersion())
16
20
  .action(async () => {
17
21
  // Interactive Menu if no command is provided
18
22
  await welcome();
@@ -27,9 +31,12 @@ program
27
31
  { name: '๐ŸŒณ Create multiple worktrees', value: 'create-multi' },
28
32
  { name: '๐ŸŒฑ Create new worktree (Manual Slug)', value: 'create-slug' },
29
33
  { name: '๐Ÿ“‹ List worktrees', value: 'list' },
30
- { name: '๐Ÿ—‘๏ธ Delete worktree', value: 'delete' },
34
+ { name: '๐Ÿ—‘๏ธ Delete worktree', value: 'delete' },
31
35
  { name: '๐Ÿš€ Bootstrap worktree', value: 'bootstrap' },
32
36
  { name: '๐Ÿงน Prune stale worktrees', value: 'prune' },
37
+ { name: '๐Ÿš Exec command', value: 'exec' },
38
+ { name: '๐Ÿšช Enter worktree', value: 'enter' },
39
+ { name: '๐Ÿ“ Get worktree path', value: 'path' },
33
40
  new inquirer.Separator(),
34
41
  { name: '๐Ÿšช Exit', value: 'exit' },
35
42
  ],
@@ -57,6 +64,15 @@ program
57
64
  case 'prune':
58
65
  await pruneCommand();
59
66
  break;
67
+ case 'exec':
68
+ await execCommand();
69
+ break;
70
+ case 'enter':
71
+ await enterCommand();
72
+ break;
73
+ case 'path':
74
+ await pathCommand();
75
+ break;
60
76
  case 'exit':
61
77
  log.info('Bye! ๐Ÿ‘‹');
62
78
  process.exit(0);
@@ -67,28 +83,43 @@ const wt = program.command('wt').description('Manage git worktrees');
67
83
  wt.command('list')
68
84
  .description('List all worktrees')
69
85
  .action(listCommand);
70
- wt.command('create')
86
+ wt.command('create [branch]')
71
87
  .description('Create a new worktree (Smart branch detection)')
72
88
  .option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
73
89
  .option('--base <ref>', 'Base ref (e.g. main)')
90
+ .option('--source <type>', 'Base source (local or remote)')
74
91
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
75
- .action(async (options) => {
76
- await createCommandNew(options);
92
+ .option('--enter', 'Enter sub-shell after creation')
93
+ .option('--no-enter', 'Skip entering sub-shell')
94
+ .option('--exec <command>', 'Command to execute after creation')
95
+ .action(async (branch, options) => {
96
+ await createCommandNew({
97
+ ...options,
98
+ branch: branch || options.branch
99
+ });
77
100
  });
78
101
  wt.command('create-multi')
79
102
  .description('Create multiple worktrees (Smart branch detection)')
80
103
  .option('--base <ref>', 'Base ref (e.g. main)')
104
+ .option('--source <type>', 'Base source (local or remote)')
81
105
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
82
106
  .action(async (options) => {
83
107
  await createCommandMulti(options);
84
108
  });
85
- wt.command('create-slug')
109
+ wt.command('create-slug [name] [ref]')
86
110
  .description('Create a new worktree (Manual slug/ref)')
87
111
  .option('-n, --name <slug>', 'Worktree name (slug)')
88
112
  .option('-r, --ref <ref>', 'Existing branch or ref')
89
113
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
90
- .action(async (options) => {
91
- await createCommand(options);
114
+ .option('--enter', 'Enter sub-shell after creation')
115
+ .option('--no-enter', 'Skip entering sub-shell')
116
+ .option('--exec <command>', 'Command to execute after creation')
117
+ .action(async (name, ref, options) => {
118
+ await createCommand({
119
+ ...options,
120
+ name: name || options.name,
121
+ ref: ref || options.ref
122
+ });
92
123
  });
93
124
  wt.command('delete')
94
125
  .description('Delete a managed worktree')
@@ -99,4 +130,23 @@ wt.command('bootstrap')
99
130
  wt.command('prune')
100
131
  .description('Prune stale worktree information')
101
132
  .action(pruneCommand);
133
+ wt.command('exec')
134
+ .description('Execute a command in a worktree')
135
+ .argument('[worktree]', 'Worktree name or path')
136
+ .argument('[command...]', 'Command and arguments to execute')
137
+ .action(async (worktree, command) => {
138
+ await execCommand(worktree, command);
139
+ });
140
+ wt.command('enter')
141
+ .description('Enter a worktree sub-shell')
142
+ .argument('[worktree]', 'Worktree name or path')
143
+ .option('--exec <command>', 'Command to execute before entering')
144
+ .action(async (worktree, options) => {
145
+ await enterCommand(worktree, options);
146
+ });
147
+ wt.command('path [worktree]')
148
+ .description('Show the cd command for a specific worktree')
149
+ .action(async (worktree) => {
150
+ await pathCommand(worktree);
151
+ });
102
152
  program.parse(process.argv);
@@ -2,35 +2,41 @@ import path from 'path';
2
2
  import fs from 'fs-extra';
3
3
  import { execa } from 'execa';
4
4
  import { log, createSpinner } from './ui.js';
5
- export async function getBootstrapCommands(repoRoot) {
6
- const configPath = path.join(repoRoot, 'anvil-worktree.json');
7
- const cursorConfigPath = path.join(repoRoot, '.cursor', 'worktrees.json');
8
- if (await fs.pathExists(configPath)) {
9
- try {
10
- const config = await fs.readJSON(configPath);
11
- if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
12
- return config['setup-worktree'];
5
+ export async function getBootstrapCommands(repoRoot, wtPath) {
6
+ const searchPaths = [];
7
+ if (wtPath)
8
+ searchPaths.push(wtPath);
9
+ searchPaths.push(repoRoot);
10
+ for (const searchPath of searchPaths) {
11
+ const configPath = path.join(searchPath, 'yggtree-worktree.json');
12
+ const cursorConfigPath = path.join(searchPath, '.cursor', 'worktrees.json');
13
+ if (await fs.pathExists(configPath)) {
14
+ try {
15
+ const config = await fs.readJSON(configPath);
16
+ if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
17
+ return config['setup-worktree'];
18
+ }
13
19
  }
14
- }
15
- catch (e) {
16
- log.warning(`Failed to parse ${configPath}.`);
17
- }
18
- }
19
- if (await fs.pathExists(cursorConfigPath)) {
20
- try {
21
- const config = await fs.readJSON(cursorConfigPath);
22
- if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
23
- return config['setup-worktree'];
20
+ catch (e) {
21
+ log.warning(`Failed to parse ${configPath}.`);
24
22
  }
25
23
  }
26
- catch (e) {
27
- log.warning(`Failed to parse ${cursorConfigPath}.`);
24
+ if (await fs.pathExists(cursorConfigPath)) {
25
+ try {
26
+ const config = await fs.readJSON(cursorConfigPath);
27
+ if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
28
+ return config['setup-worktree'];
29
+ }
30
+ }
31
+ catch (e) {
32
+ log.warning(`Failed to parse ${cursorConfigPath}.`);
33
+ }
28
34
  }
29
35
  }
30
36
  return null;
31
37
  }
32
38
  export async function runBootstrap(wtPath, repoRoot) {
33
- const customCommands = await getBootstrapCommands(repoRoot);
39
+ const customCommands = await getBootstrapCommands(repoRoot, wtPath);
34
40
  if (customCommands) {
35
41
  log.info('Using custom bootstrap commands from config...');
36
42
  for (const cmd of customCommands) {
@@ -42,7 +48,10 @@ export async function runBootstrap(wtPath, repoRoot) {
42
48
  }
43
49
  catch (e) {
44
50
  spinner.fail(`Failed: ${cmd}`);
45
- log.error(e.message);
51
+ log.actionableError(e.message, cmd, wtPath, [
52
+ `Try running the command manually: cd ${wtPath} && ${cmd}`,
53
+ 'Check your configuration in yggtree-worktree.json'
54
+ ]);
46
55
  }
47
56
  }
48
57
  }
@@ -59,7 +68,10 @@ export async function runBootstrap(wtPath, repoRoot) {
59
68
  }
60
69
  catch (e) {
61
70
  installSpinner.fail('npm install failed.');
62
- log.error(e.message);
71
+ log.actionableError(e.message, 'npm install', wtPath, [
72
+ `cd ${wtPath} && npm install`,
73
+ 'Check your network connection and package-lock.json'
74
+ ]);
63
75
  }
64
76
  }
65
77
  catch {
@@ -74,8 +86,15 @@ export async function runBootstrap(wtPath, repoRoot) {
74
86
  }
75
87
  catch (e) {
76
88
  subSpinner.fail('Submodule sync failed.');
77
- log.error(e.message);
78
- log.warning('Tip: If auth failed, try adding your key to the agent.');
89
+ const isAuthError = /permission denied|authentication|publickey/i.test(e.message);
90
+ const steps = [
91
+ 'git submodule sync --recursive',
92
+ 'git submodule update --init --recursive'
93
+ ];
94
+ if (isAuthError) {
95
+ steps.unshift('ssh -T git@github.com', 'ssh-add --apple-use-keychain ~/.ssh/id_ed25519');
96
+ }
97
+ log.actionableError(e.message, 'git submodule update --init --recursive', wtPath, steps);
79
98
  }
80
99
  }
81
100
  }
package/dist/lib/git.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { execa } from 'execa';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
+ import { log } from './ui.js';
5
+ import chalk from 'chalk';
4
6
  export async function getRepoRoot() {
5
7
  try {
6
8
  const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
@@ -74,3 +76,43 @@ export async function syncSubmodules(cwd) {
74
76
  await execa('git', ['submodule', 'sync', '--recursive'], { cwd });
75
77
  await execa('git', ['submodule', 'update', '--init', '--recursive'], { cwd });
76
78
  }
79
+ export async function isGitClean(cwd) {
80
+ try {
81
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
82
+ return stdout.trim().length === 0;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Ensures the branch in the given worktree path tracks origin/<branchName>.
90
+ * If it tracks a different base branch (e.g. origin/main), it unsets it and publishes to origin.
91
+ * Fails if origin/<branchName> already exists on remote and is not already the upstream.
92
+ */
93
+ export async function ensureCorrectUpstream(wtPath, branchName) {
94
+ const desiredUpstream = `origin/${branchName}`;
95
+ let currentUpstream = '';
96
+ try {
97
+ const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { cwd: wtPath });
98
+ currentUpstream = stdout.trim();
99
+ }
100
+ catch {
101
+ // No upstream set
102
+ }
103
+ if (currentUpstream === desiredUpstream) {
104
+ return; // Already correct
105
+ }
106
+ // If it's set to something else, unset it
107
+ if (currentUpstream) {
108
+ log.info(`Incorrect upstream detected: ${currentUpstream}. Unsetting...`);
109
+ await execa('git', ['branch', '--unset-upstream'], { cwd: wtPath });
110
+ }
111
+ // Check if remote branch already exists
112
+ const remoteExists = await verifyRef(desiredUpstream);
113
+ if (remoteExists) {
114
+ throw new Error(`Remote branch '${desiredUpstream}' already exists. Cannot publish safely without more info. Use 'git push -u origin HEAD' manually if you want to link them.`);
115
+ }
116
+ log.info(`Publishing branch ${chalk.cyan(branchName)} to ${chalk.cyan(desiredUpstream)}...`);
117
+ await execa('git', ['push', '-u', 'origin', 'HEAD'], { cwd: wtPath });
118
+ }
package/dist/lib/ui.js CHANGED
@@ -4,13 +4,15 @@ import gradient from 'gradient-string';
4
4
  import ora from 'ora';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { getVersion } from './version.js';
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  // --- Personality & Branding ---
9
10
  export const welcome = async () => {
10
11
  console.log('');
11
12
  const title = figlet.textSync('Yggdrasil', { font: 'Standard' });
12
13
  console.log(gradient.mind.multiline(title));
13
- console.log(chalk.dim(' v1.0.0 โ€ข The World Tree Worktree Assistant'));
14
+ const version = getVersion();
15
+ console.log(chalk.dim(` v${version} โ€ข The World Tree Worktree Assistant`));
14
16
  console.log('');
15
17
  };
16
18
  // --- Logger ---
@@ -21,6 +23,17 @@ export const log = {
21
23
  error: (msg) => console.log(chalk.red('โœ–'), msg),
22
24
  dim: (msg) => console.log(chalk.dim(msg)),
23
25
  header: (msg) => console.log(chalk.bold.hex('#DEADED')(`\n${msg}\n`)),
26
+ actionableError: (error, command, worktreePath, nextSteps = []) => {
27
+ console.log(chalk.red('\nโœ– Error:'), error);
28
+ console.log(chalk.dim(' Command:'), chalk.white(command));
29
+ if (worktreePath)
30
+ console.log(chalk.dim(' Path: '), chalk.cyan(worktreePath));
31
+ if (nextSteps.length > 0) {
32
+ console.log(chalk.bold('\n Next steps:'));
33
+ nextSteps.forEach(step => console.log(chalk.yellow(` - ${step}`)));
34
+ }
35
+ console.log('');
36
+ },
24
37
  };
25
38
  // --- Initial Spinners ---
26
39
  export const createSpinner = (text) => {
@@ -0,0 +1,17 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ export function getVersion() {
7
+ try {
8
+ // In dist, this file is in dist/lib/version.js
9
+ // package.json is in the root (../../package.json)
10
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
11
+ const pkg = fs.readJsonSync(packageJsonPath);
12
+ return pkg.version || '1.0.0';
13
+ }
14
+ catch (error) {
15
+ return '1.0.0';
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yggtree",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Interactive CLI for managing git worktrees and configs",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",