yggtree 1.4.3 → 1.4.5
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/README.md +105 -28
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +5 -3
- package/dist/commands/wt/create-branch.js +41 -28
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +48 -8
- package/dist/commands/wt/delete.js +8 -6
- package/dist/commands/wt/enter.js +5 -3
- package/dist/commands/wt/exec.js +4 -2
- package/dist/commands/wt/handoff.js +7 -0
- package/dist/commands/wt/list.js +3 -1
- package/dist/commands/wt/open.js +152 -79
- package/dist/commands/wt/path.js +4 -2
- package/dist/index.js +113 -46
- package/dist/lib/global-config.js +89 -0
- package/dist/lib/help.js +93 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +71 -6
- package/dist/lib/worktree.js +14 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { welcome, log } from './lib/ui.js';
|
|
4
|
+
import { formatMainMenuChoice, renderMainMenuIntro, welcome, log, } from './lib/ui.js';
|
|
5
|
+
import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
|
|
5
6
|
import { listCommand } from './commands/wt/list.js';
|
|
6
7
|
import { createCommand } from './commands/wt/create.js';
|
|
7
8
|
import { createCommandNew } from './commands/wt/create-branch.js';
|
|
@@ -15,13 +16,24 @@ import { pathCommand } from './commands/wt/path.js';
|
|
|
15
16
|
import { openCommand } from './commands/wt/open.js';
|
|
16
17
|
import { applyCommand } from './commands/wt/apply.js';
|
|
17
18
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
19
|
+
import { handoffCommand } from './commands/wt/handoff.js';
|
|
20
|
+
import { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
|
|
18
21
|
import { getVersion } from './lib/version.js';
|
|
19
|
-
import {
|
|
22
|
+
import { checkForUpdate } from './lib/update-check.js';
|
|
20
23
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
21
24
|
import { bifrostCommand } from './commands/bifrost.js';
|
|
22
25
|
import { thorCommand } from './commands/thor.js';
|
|
23
26
|
const program = new Command();
|
|
24
27
|
const argv = process.argv.map((arg) => arg === '-v' || arg === '—version' ? '--version' : arg);
|
|
28
|
+
function mainMenuChoice(value, glyph, label, detail, tone) {
|
|
29
|
+
return {
|
|
30
|
+
name: formatMainMenuChoice({ glyph, label, detail, tone }),
|
|
31
|
+
value,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mainMenuSeparator(label) {
|
|
35
|
+
return new inquirer.Separator(chalk.dim(` · ${label}`));
|
|
36
|
+
}
|
|
25
37
|
function registerWorktreeCommands(parent) {
|
|
26
38
|
parent.command('list')
|
|
27
39
|
.description('List all repo-linked worktrees')
|
|
@@ -34,16 +46,17 @@ function registerWorktreeCommands(parent) {
|
|
|
34
46
|
await listCommand();
|
|
35
47
|
});
|
|
36
48
|
parent.command('create [branch]')
|
|
37
|
-
.description('Create
|
|
49
|
+
.description('Create an official branch-backed task worktree')
|
|
38
50
|
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
39
51
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
40
52
|
.option('--source <type>', 'Base source (local or remote)')
|
|
41
53
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
42
54
|
.option('--open', 'Open an editor after creation')
|
|
43
55
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
44
|
-
.
|
|
45
|
-
.
|
|
56
|
+
.option('--enter', 'Enter the worktree sub-shell after creation')
|
|
57
|
+
.option('--no-enter', 'Do not enter the worktree sub-shell after creation')
|
|
46
58
|
.option('--exec <command>', 'Command to execute after creation')
|
|
59
|
+
.addHelpText('after', createHelp)
|
|
47
60
|
.action(async (branch, options) => {
|
|
48
61
|
await createCommandNew({
|
|
49
62
|
...options,
|
|
@@ -51,10 +64,11 @@ function registerWorktreeCommands(parent) {
|
|
|
51
64
|
});
|
|
52
65
|
});
|
|
53
66
|
parent.command('create-multi')
|
|
54
|
-
.description('
|
|
67
|
+
.description('Bulk-create official branch-backed worktrees')
|
|
55
68
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
56
69
|
.option('--source <type>', 'Base source (local or remote)')
|
|
57
70
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
71
|
+
.addHelpText('after', createMultiHelp)
|
|
58
72
|
.action(async (options) => {
|
|
59
73
|
await createCommandMulti(options);
|
|
60
74
|
});
|
|
@@ -66,10 +80,11 @@ function registerWorktreeCommands(parent) {
|
|
|
66
80
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
67
81
|
.option('--open', 'Open editors or run a startup command before entering')
|
|
68
82
|
.option('--no-open', 'Skip opening editors or startup commands before entering')
|
|
69
|
-
.option('--tool <command>', 'Editor or
|
|
83
|
+
.option('--tool <command>', 'Editor, app, or terminal command to open after checkout (skips open prompt)')
|
|
70
84
|
.addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
|
|
71
85
|
.option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
|
|
72
86
|
.option('--exec <command>', 'Command to execute after creation')
|
|
87
|
+
.addHelpText('after', checkoutHelp)
|
|
73
88
|
.action(async (name, ref, options) => {
|
|
74
89
|
await createCommand({
|
|
75
90
|
...options,
|
|
@@ -82,15 +97,16 @@ function registerWorktreeCommands(parent) {
|
|
|
82
97
|
registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
|
|
83
98
|
parent.command('delete')
|
|
84
99
|
.description('Delete managed worktrees')
|
|
85
|
-
.option('-a, --all', 'Include repo-linked worktrees outside
|
|
100
|
+
.option('-a, --all', 'Include repo-linked worktrees outside the managed root (except main/current)')
|
|
86
101
|
.action(async (options) => {
|
|
87
102
|
await deleteCommand(options);
|
|
88
103
|
});
|
|
89
104
|
parent.command('open [worktree]')
|
|
90
|
-
.description('Open a worktree in an editor or
|
|
91
|
-
.option('--tool <command>', 'Editor or
|
|
105
|
+
.description('Open a worktree in an editor, supported app, or terminal target')
|
|
106
|
+
.option('--tool <command>', 'Editor, app, or terminal command to use (e.g. cursor, code, codex-app, tmux)')
|
|
92
107
|
.option('--enter', 'Enter the worktree sub-shell after opening')
|
|
93
108
|
.addOption(new Option('--no-enter', 'Do not enter the worktree sub-shell after opening').hideHelp())
|
|
109
|
+
.addHelpText('after', openHelp)
|
|
94
110
|
.action(async (worktree, options) => {
|
|
95
111
|
await openCommand(worktree, options);
|
|
96
112
|
});
|
|
@@ -113,24 +129,40 @@ function registerWorktreeCommands(parent) {
|
|
|
113
129
|
await pathCommand(worktree);
|
|
114
130
|
});
|
|
115
131
|
parent.command('create-sandbox')
|
|
116
|
-
.description('Create a
|
|
132
|
+
.description('Create a local-only disposable experiment sandbox')
|
|
117
133
|
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
118
|
-
.option('--carry', '
|
|
119
|
-
.option('--no-carry', 'Do not
|
|
134
|
+
.option('--carry', 'Copy uncommitted changes to sandbox')
|
|
135
|
+
.option('--no-carry', 'Do not copy uncommitted changes')
|
|
120
136
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
121
137
|
.option('--open', 'Open an editor after creation')
|
|
122
138
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
123
139
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
124
140
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
125
141
|
.option('--exec <command>', 'Command to execute after creation')
|
|
142
|
+
.addHelpText('after', createSandboxHelp)
|
|
126
143
|
.action(async (options) => {
|
|
127
144
|
await createSandboxCommand(options);
|
|
128
145
|
});
|
|
146
|
+
parent.command('handoff')
|
|
147
|
+
.description('Carry dirty current work into a named sandbox')
|
|
148
|
+
.option('-n, --name <name>', 'Optional handoff name (prompted when omitted)')
|
|
149
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
150
|
+
.option('--open', 'Open an editor after creation')
|
|
151
|
+
.option('--no-open', 'Skip opening an editor after creation')
|
|
152
|
+
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
153
|
+
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
154
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
155
|
+
.addHelpText('after', handoffHelp)
|
|
156
|
+
.action(async (options) => {
|
|
157
|
+
await handoffCommand(options);
|
|
158
|
+
});
|
|
129
159
|
parent.command('apply')
|
|
130
|
-
.description('
|
|
160
|
+
.description('Copy sandbox file changes to the origin checkout')
|
|
161
|
+
.addHelpText('after', applyHelp)
|
|
131
162
|
.action(applyCommand);
|
|
132
163
|
parent.command('unapply')
|
|
133
|
-
.description('
|
|
164
|
+
.description('Restore origin files from sandbox apply backups')
|
|
165
|
+
.addHelpText('after', unapplyHelp)
|
|
134
166
|
.action(unapplyCommand);
|
|
135
167
|
}
|
|
136
168
|
function rejectUnknownTopLevelCommand(args) {
|
|
@@ -149,54 +181,69 @@ program
|
|
|
149
181
|
.description('Interactive CLI for managing git worktrees and configs')
|
|
150
182
|
.version(getVersion())
|
|
151
183
|
.allowExcessArguments(false)
|
|
184
|
+
.addHelpText('after', intentRouterHelp)
|
|
152
185
|
.action(async () => {
|
|
153
|
-
|
|
154
|
-
await welcome();
|
|
155
|
-
await notifyIfUpdateAvailable();
|
|
186
|
+
const update = await checkForUpdate();
|
|
187
|
+
await welcome({ update });
|
|
156
188
|
const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
const dailyChoices = [
|
|
190
|
+
mainMenuChoice('create-smart', '◆', 'Grow new realm', 'create a branch-backed worktree', 'growth'),
|
|
191
|
+
mainMenuChoice('worktree-checkout', '⇄', 'Check out branch', 'open an existing ref in a new realm', 'travel'),
|
|
192
|
+
mainMenuChoice('handoff', '✦', 'Hand off current work', 'carry dirty work into a named sandbox', 'sandbox'),
|
|
193
|
+
mainMenuChoice('open', '◇', 'Open realm', 'jump into an existing worktree', 'travel'),
|
|
194
|
+
mainMenuChoice('list', '○', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
|
|
195
|
+
];
|
|
196
|
+
const growthChoices = [
|
|
197
|
+
mainMenuChoice('create-multi', '✧', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
|
|
198
|
+
mainMenuChoice('create-sandbox', '△', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
|
|
199
|
+
];
|
|
200
|
+
const tendingChoices = [
|
|
201
|
+
mainMenuChoice('bootstrap', '↳', 'Bootstrap realm', 'install dependencies and submodules', 'tending'),
|
|
202
|
+
mainMenuChoice('exec', '⌁', 'Cast command', 'run a command inside a worktree', 'tending'),
|
|
203
|
+
mainMenuChoice('path', '⌖', 'Reveal path', 'print a worktree cd command', 'tending'),
|
|
204
|
+
mainMenuChoice('delete', '×', 'Fell realm', 'delete managed worktrees', 'danger'),
|
|
205
|
+
mainMenuChoice('prune', '⌫', 'Prune stale realms', 'remove stale worktree metadata', 'danger'),
|
|
169
206
|
];
|
|
170
207
|
const sandboxChoices = [
|
|
171
|
-
|
|
172
|
-
|
|
208
|
+
mainMenuChoice('apply', '◆', 'Graft sandbox changes', 'apply sandbox changes to origin', 'sandbox'),
|
|
209
|
+
mainMenuChoice('unapply', '↶', 'Undo sandbox graft', 'restore origin from sandbox backups', 'sandbox'),
|
|
210
|
+
];
|
|
211
|
+
const loreChoices = [
|
|
212
|
+
mainMenuChoice('bifrost', '✺', 'Summon the Bifrost', 'open the long way around', 'lore'),
|
|
213
|
+
mainMenuChoice('thor', 'ϟ', 'Consult Thor', 'ask the thunder route', 'lore'),
|
|
214
|
+
mainMenuChoice('exit', '·', 'Leave Yggdrasil', 'exit without changing anything', 'exit'),
|
|
173
215
|
];
|
|
174
216
|
const choices = isInSandbox
|
|
175
217
|
? [
|
|
176
218
|
...sandboxChoices,
|
|
177
|
-
|
|
178
|
-
...
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
219
|
+
mainMenuSeparator('Daily routes'),
|
|
220
|
+
...dailyChoices,
|
|
221
|
+
mainMenuSeparator('Growth and experiments'),
|
|
222
|
+
...growthChoices,
|
|
223
|
+
mainMenuSeparator('Tend realms'),
|
|
224
|
+
...tendingChoices,
|
|
225
|
+
mainMenuSeparator('Lore'),
|
|
226
|
+
...loreChoices,
|
|
183
227
|
]
|
|
184
228
|
: [
|
|
185
|
-
...
|
|
186
|
-
|
|
229
|
+
...dailyChoices,
|
|
230
|
+
mainMenuSeparator('Growth and experiments'),
|
|
231
|
+
...growthChoices,
|
|
232
|
+
mainMenuSeparator('Tend realms'),
|
|
233
|
+
...tendingChoices,
|
|
234
|
+
mainMenuSeparator('Sandbox grafts'),
|
|
187
235
|
...sandboxChoices,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
{ name: `⚡ Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
|
|
191
|
-
{ name: `🚪 Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
|
|
236
|
+
mainMenuSeparator('Lore'),
|
|
237
|
+
...loreChoices,
|
|
192
238
|
];
|
|
239
|
+
console.log(renderMainMenuIntro({ isInSandbox }));
|
|
193
240
|
const { action } = await inquirer.prompt([
|
|
194
241
|
{
|
|
195
242
|
type: 'list',
|
|
196
243
|
name: 'action',
|
|
197
|
-
message: '
|
|
244
|
+
message: 'Which route should Yggtree follow?',
|
|
198
245
|
loop: false,
|
|
199
|
-
pageSize:
|
|
246
|
+
pageSize: 14,
|
|
200
247
|
choices,
|
|
201
248
|
},
|
|
202
249
|
]);
|
|
@@ -240,6 +287,9 @@ program
|
|
|
240
287
|
case 'create-sandbox':
|
|
241
288
|
await createSandboxCommand({ bootstrap: true });
|
|
242
289
|
break;
|
|
290
|
+
case 'handoff':
|
|
291
|
+
await handoffCommand({ bootstrap: true });
|
|
292
|
+
break;
|
|
243
293
|
case 'bifrost':
|
|
244
294
|
await bifrostCommand();
|
|
245
295
|
break;
|
|
@@ -253,6 +303,7 @@ program
|
|
|
253
303
|
});
|
|
254
304
|
// --- Worktree Commands ---
|
|
255
305
|
const wt = program.command('wt').description('Manage git worktrees');
|
|
306
|
+
wt.addHelpText('after', intentRouterHelp);
|
|
256
307
|
registerWorktreeCommands(program);
|
|
257
308
|
registerWorktreeCommands(wt);
|
|
258
309
|
program.addHelpCommand(true);
|
|
@@ -262,5 +313,21 @@ program.command('bifrost')
|
|
|
262
313
|
program.command('thor')
|
|
263
314
|
.description('Consult the God of Thunder (Easter Egg)')
|
|
264
315
|
.action(thorCommand);
|
|
316
|
+
const config = program.command('config').description('Inspect or update global Yggtree settings');
|
|
317
|
+
config.command('get')
|
|
318
|
+
.description('Show resolved global settings')
|
|
319
|
+
.action(configGetCommand);
|
|
320
|
+
config.command('use <preset>')
|
|
321
|
+
.description('Use a global worktree path preset (default, yggtree, codex)')
|
|
322
|
+
.action(configUseCommand);
|
|
323
|
+
config.command('set-worktrees-root <path>')
|
|
324
|
+
.description('Set the global managed worktrees root')
|
|
325
|
+
.action(configSetWorktreesRootCommand);
|
|
326
|
+
config.command('set-worktree-layout <layout>')
|
|
327
|
+
.description('Set the managed worktree path layout (yggtree or codex)')
|
|
328
|
+
.action(configSetWorktreeLayoutCommand);
|
|
329
|
+
config.command('reset')
|
|
330
|
+
.description('Reset global settings to defaults')
|
|
331
|
+
.action(configResetCommand);
|
|
265
332
|
rejectUnknownTopLevelCommand(argv);
|
|
266
333
|
program.parse(argv);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { YGG_ROOT } from './paths.js';
|
|
5
|
+
const CONFIG_PATH = path.join(YGG_ROOT, 'config.json');
|
|
6
|
+
const DEFAULT_WORKTREE_LAYOUT = 'yggtree';
|
|
7
|
+
export function expandHome(value) {
|
|
8
|
+
if (value === '~')
|
|
9
|
+
return os.homedir();
|
|
10
|
+
if (value.startsWith('~/'))
|
|
11
|
+
return path.join(os.homedir(), value.slice(2));
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function formatHome(value) {
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
if (value === home)
|
|
17
|
+
return '~';
|
|
18
|
+
if (value.startsWith(`${home}${path.sep}`))
|
|
19
|
+
return `~/${path.relative(home, value)}`;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function normalizeRoot(value) {
|
|
23
|
+
return path.resolve(expandHome(value || YGG_ROOT));
|
|
24
|
+
}
|
|
25
|
+
export function normalizeWorktreesRootInput(value) {
|
|
26
|
+
return path.resolve(expandHome(value));
|
|
27
|
+
}
|
|
28
|
+
function normalizeLayout(value) {
|
|
29
|
+
return value || DEFAULT_WORKTREE_LAYOUT;
|
|
30
|
+
}
|
|
31
|
+
export async function readGlobalConfig() {
|
|
32
|
+
try {
|
|
33
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
34
|
+
const config = await fs.readJSON(CONFIG_PATH);
|
|
35
|
+
return {
|
|
36
|
+
worktreesRoot: typeof config.worktreesRoot === 'string' ? config.worktreesRoot : undefined,
|
|
37
|
+
worktreeLayout: config.worktreeLayout === 'codex' ? 'codex' : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
export async function writeGlobalConfig(config) {
|
|
47
|
+
await fs.ensureDir(YGG_ROOT);
|
|
48
|
+
await fs.writeJSON(CONFIG_PATH, config, { spaces: 2 });
|
|
49
|
+
}
|
|
50
|
+
export async function getWorktreePathConfig() {
|
|
51
|
+
const config = await readGlobalConfig();
|
|
52
|
+
return {
|
|
53
|
+
root: normalizeRoot(config.worktreesRoot),
|
|
54
|
+
layout: normalizeLayout(config.worktreeLayout),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function getManagedWorktreesRoot() {
|
|
58
|
+
const config = await getWorktreePathConfig();
|
|
59
|
+
return config.root;
|
|
60
|
+
}
|
|
61
|
+
export function buildManagedWorktreePath(repoName, worktreeSlug, config) {
|
|
62
|
+
if (config.layout === 'codex') {
|
|
63
|
+
return path.join(config.root, worktreeSlug, repoName);
|
|
64
|
+
}
|
|
65
|
+
return path.join(config.root, repoName, worktreeSlug);
|
|
66
|
+
}
|
|
67
|
+
export function getPresetConfig(preset) {
|
|
68
|
+
const normalized = preset.trim().toLowerCase();
|
|
69
|
+
if (normalized === 'default' || normalized === 'yggtree') {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
if (normalized === 'codex') {
|
|
73
|
+
return {
|
|
74
|
+
worktreesRoot: '~/.codex/worktrees',
|
|
75
|
+
worktreeLayout: 'codex',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
export function formatGlobalConfig(config, resolved) {
|
|
81
|
+
return [
|
|
82
|
+
`worktreesRoot: ${formatHome(resolved.root)}`,
|
|
83
|
+
`worktreeLayout: ${resolved.layout}`,
|
|
84
|
+
`configFile: ${formatHome(CONFIG_PATH)}`,
|
|
85
|
+
config.worktreesRoot || config.worktreeLayout
|
|
86
|
+
? 'preset: custom'
|
|
87
|
+
: 'preset: default',
|
|
88
|
+
];
|
|
89
|
+
}
|
package/dist/lib/help.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const intentRouterHelp = `
|
|
2
|
+
Choose by intent:
|
|
3
|
+
New official task branch yggtree create feat/name --base main --source remote
|
|
4
|
+
Existing branch/interruption yggtree wc --ref branch-name
|
|
5
|
+
Continue dirty current work yggtree handoff --name task-name
|
|
6
|
+
Disposable local experiment yggtree create-sandbox
|
|
7
|
+
Copy sandbox files to origin yggtree apply # from inside sandbox, not a Git merge
|
|
8
|
+
Bulk official worktrees yggtree create-multi
|
|
9
|
+
|
|
10
|
+
More detail: run yggtree help <command>, for example yggtree help handoff.
|
|
11
|
+
`;
|
|
12
|
+
export const createHelp = `
|
|
13
|
+
Behavior:
|
|
14
|
+
Creates an official branch-backed worktree, publishes the branch to origin, and enters the shell by default.
|
|
15
|
+
Use for real task branches. For disposable experiments, use create-sandbox.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
yggtree create feat/new-flow --base main --source remote
|
|
19
|
+
yggtree create feat/background-task --base main --source remote --no-open --no-enter
|
|
20
|
+
`;
|
|
21
|
+
export const createMultiHelp = `
|
|
22
|
+
Behavior:
|
|
23
|
+
Bulk-creates official branch-backed worktrees.
|
|
24
|
+
This does not share create's open/enter/exec lifecycle. Use create for one task branch.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
yggtree create-multi --base main --source remote
|
|
28
|
+
`;
|
|
29
|
+
export const checkoutHelp = `
|
|
30
|
+
Behavior:
|
|
31
|
+
Creates or reuses a worktree for an existing branch or ref without disturbing current work.
|
|
32
|
+
Use for interruptions and branch review. Use create for a new official task branch.
|
|
33
|
+
Enters the worktree shell by default; add --no-enter when automation should return.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
yggtree wc --ref hotfix/payment-timeout
|
|
37
|
+
yggtree wc --ref main --name fresh-main --no-open --no-enter
|
|
38
|
+
`;
|
|
39
|
+
export const openHelp = `
|
|
40
|
+
Behavior:
|
|
41
|
+
Opens an existing worktree in an editor, desktop app, or terminal target.
|
|
42
|
+
Returns by default after opening. Add --enter only when the shell should continue inside the worktree.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
yggtree open my-feature --tool cursor
|
|
46
|
+
yggtree open my-feature --tool codex-app
|
|
47
|
+
yggtree list --open
|
|
48
|
+
`;
|
|
49
|
+
export const createSandboxHelp = `
|
|
50
|
+
Behavior:
|
|
51
|
+
Creates a local-only disposable experiment from the current branch.
|
|
52
|
+
For continuing current dirty work, prefer handoff instead of teaching create-sandbox --carry.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
yggtree create-sandbox
|
|
56
|
+
yggtree create-sandbox --name ui-option-a --no-open
|
|
57
|
+
|
|
58
|
+
Related:
|
|
59
|
+
handoff carries current dirty work into a named sandbox.
|
|
60
|
+
apply copies changed sandbox files back to origin.
|
|
61
|
+
`;
|
|
62
|
+
export const handoffHelp = `
|
|
63
|
+
Behavior:
|
|
64
|
+
Carry staged, unstaged, and untracked work into a named sandbox so you can continue there.
|
|
65
|
+
The origin checkout is not cleaned; review or reset it separately if needed.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
yggtree handoff --name continue-auth-refactor
|
|
69
|
+
|
|
70
|
+
Related:
|
|
71
|
+
create-sandbox starts a disposable local experiment.
|
|
72
|
+
apply copies changed sandbox files back to origin.
|
|
73
|
+
`;
|
|
74
|
+
export const applyHelp = `
|
|
75
|
+
Behavior:
|
|
76
|
+
Copies changed files from the current sandbox back to the origin checkout.
|
|
77
|
+
Stores backups in sandbox metadata before overwriting origin files.
|
|
78
|
+
Not a Git merge, rebase, patch, or cherry-pick. Review the origin diff afterward.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
yggtree apply
|
|
82
|
+
|
|
83
|
+
Related:
|
|
84
|
+
unapply restores from the sandbox backup metadata while the sandbox still exists.
|
|
85
|
+
`;
|
|
86
|
+
export const unapplyHelp = `
|
|
87
|
+
Behavior:
|
|
88
|
+
Restores origin files from sandbox metadata created by apply.
|
|
89
|
+
Only works while the sandbox still exists.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
yggtree unapply
|
|
93
|
+
`;
|
package/dist/lib/paths.js
CHANGED
package/dist/lib/repo-context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { getRepoRoot } from './git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from './global-config.js';
|
|
4
5
|
import { getValidRegisteredRepos } from './registry.js';
|
|
5
6
|
import { log } from './ui.js';
|
|
6
7
|
import { formatWorktreeDisplayPath } from './worktree.js';
|
|
@@ -11,6 +12,7 @@ export async function ensureRepoContext() {
|
|
|
11
12
|
catch {
|
|
12
13
|
const validRepos = await getValidRegisteredRepos();
|
|
13
14
|
const repoEntries = Object.entries(validRepos);
|
|
15
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
14
16
|
if (repoEntries.length === 0) {
|
|
15
17
|
log.error('Not inside a git repository and no registered realms found.');
|
|
16
18
|
log.dim('Run `yggtree` inside an existing git project first to register it.');
|
|
@@ -34,7 +36,7 @@ export async function ensureRepoContext() {
|
|
|
34
36
|
name: 'selectedRepoPath',
|
|
35
37
|
message: 'Select a realm:',
|
|
36
38
|
choices: repoEntries.map(([name, repoPath]) => ({
|
|
37
|
-
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(repoPath))}`,
|
|
39
|
+
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(repoPath, managedRoot))}`,
|
|
38
40
|
value: repoPath,
|
|
39
41
|
})),
|
|
40
42
|
pageSize: 10,
|
package/dist/lib/ui.js
CHANGED
|
@@ -6,15 +6,80 @@ import path from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { getVersion } from './version.js';
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const yggAccent = chalk.hex('#DEADED');
|
|
10
|
+
const yggLeaf = chalk.hex('#9AD68B');
|
|
11
|
+
const yggSky = chalk.hex('#7AC7D8');
|
|
12
|
+
const yggBark = chalk.hex('#C99A5B');
|
|
13
|
+
const yggRoot = chalk.hex('#B48AE8');
|
|
14
|
+
const yggWarn = chalk.hex('#E4B85E');
|
|
15
|
+
const yggDanger = chalk.hex('#E06C75');
|
|
16
|
+
const yggMuted = chalk.hex('#8D8D8D');
|
|
17
|
+
export function renderWelcome(options = {}) {
|
|
18
|
+
const rule = yggAccent('─'.repeat(54));
|
|
12
19
|
const title = figlet.textSync('Yggdrasil', { font: 'Standard' });
|
|
13
|
-
console.log(gradient.mind.multiline(title));
|
|
14
20
|
const version = getVersion();
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const updateLines = renderUpdateNotice(options.update);
|
|
22
|
+
return [
|
|
23
|
+
'',
|
|
24
|
+
rule,
|
|
25
|
+
gradient.mind.multiline(title),
|
|
26
|
+
chalk.bold(yggAccent(' Yggdrasil')),
|
|
27
|
+
chalk.dim(` v${version} • The World Tree Worktree Assistant`),
|
|
28
|
+
...updateLines,
|
|
29
|
+
rule,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
export const welcome = async (options = {}) => {
|
|
34
|
+
console.log(renderWelcome(options));
|
|
17
35
|
};
|
|
36
|
+
export function renderMainMenuIntro(options) {
|
|
37
|
+
const detail = options.isInSandbox
|
|
38
|
+
? 'Sandbox tools appear first because this realm can apply or undo grafted changes.'
|
|
39
|
+
: 'Create, enter, inspect, or tend your worktrees.';
|
|
40
|
+
return [
|
|
41
|
+
'Choose the next realm action.',
|
|
42
|
+
yggMuted('Daily routes first. Maintenance and lore wait below.'),
|
|
43
|
+
'',
|
|
44
|
+
`${yggBark('┌')} ${badge('route 01')} ${chalk.bold('Realm')}`,
|
|
45
|
+
`${yggBark('│')} ${yggMuted(detail)}`,
|
|
46
|
+
].join('\n');
|
|
47
|
+
}
|
|
48
|
+
export function formatMainMenuChoice(options) {
|
|
49
|
+
const tone = menuTone(options.tone);
|
|
50
|
+
const label = options.label.padEnd(24);
|
|
51
|
+
return `${tone(options.glyph)} ${chalk.bold(label)} ${yggMuted(options.detail)}`;
|
|
52
|
+
}
|
|
53
|
+
function renderUpdateNotice(update) {
|
|
54
|
+
if (!update?.updateAvailable)
|
|
55
|
+
return [];
|
|
56
|
+
return [
|
|
57
|
+
'',
|
|
58
|
+
`${yggWarn('Update available')} ${yggMuted(`yggtree ${update.currentVersion} -> ${update.latestVersion}`)}`,
|
|
59
|
+
yggMuted('Run: npm install -g yggtree'),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function badge(value) {
|
|
63
|
+
return yggRoot(` ${value} `);
|
|
64
|
+
}
|
|
65
|
+
function menuTone(tone) {
|
|
66
|
+
switch (tone) {
|
|
67
|
+
case 'growth':
|
|
68
|
+
return yggLeaf;
|
|
69
|
+
case 'travel':
|
|
70
|
+
return yggSky;
|
|
71
|
+
case 'sandbox':
|
|
72
|
+
return yggRoot;
|
|
73
|
+
case 'tending':
|
|
74
|
+
return yggBark;
|
|
75
|
+
case 'danger':
|
|
76
|
+
return yggDanger;
|
|
77
|
+
case 'lore':
|
|
78
|
+
return yggAccent;
|
|
79
|
+
case 'exit':
|
|
80
|
+
return yggMuted;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
18
83
|
// --- Logger ---
|
|
19
84
|
export const log = {
|
|
20
85
|
info: (msg) => console.log(chalk.blue('ℹ'), msg),
|
package/dist/lib/worktree.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { YGG_ROOT } from './paths.js';
|
|
5
5
|
import { getSandboxMetaPath } from './sandbox.js';
|
|
6
6
|
export const WORKTREE_TYPE_ORDER = {
|
|
7
7
|
MAIN: 0,
|
|
@@ -12,19 +12,23 @@ export const WORKTREE_TYPE_ORDER = {
|
|
|
12
12
|
export function getWorktreeBranchName(worktree) {
|
|
13
13
|
return worktree.branch || worktree.HEAD || 'detached';
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function isPathInsideRoot(worktreePath, managedRoot) {
|
|
16
|
+
const relative = path.relative(managedRoot, worktreePath);
|
|
17
|
+
return relative === '' || Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
17
18
|
}
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export function isManagedWorktreePath(worktreePath, managedRoot = YGG_ROOT) {
|
|
20
|
+
return isPathInsideRoot(path.resolve(worktreePath), path.resolve(managedRoot));
|
|
21
|
+
}
|
|
22
|
+
export function formatWorktreeDisplayPath(worktreePath, managedRoot = YGG_ROOT) {
|
|
23
|
+
if (isManagedWorktreePath(worktreePath, managedRoot)) {
|
|
24
|
+
return path.relative(managedRoot, worktreePath);
|
|
21
25
|
}
|
|
22
26
|
return worktreePath.replace(process.env.HOME || '', '~');
|
|
23
27
|
}
|
|
24
|
-
export function findWorktreeByName(worktrees, worktreeName) {
|
|
28
|
+
export function findWorktreeByName(worktrees, worktreeName, managedRoot = YGG_ROOT) {
|
|
25
29
|
return worktrees.find(worktree => {
|
|
26
30
|
const branchName = getWorktreeBranchName(worktree);
|
|
27
|
-
const relativePath = path.relative(
|
|
31
|
+
const relativePath = path.relative(managedRoot, worktree.path);
|
|
28
32
|
const basename = path.basename(worktree.path);
|
|
29
33
|
return branchName === worktreeName ||
|
|
30
34
|
relativePath === worktreeName ||
|
|
@@ -32,8 +36,8 @@ export function findWorktreeByName(worktrees, worktreeName) {
|
|
|
32
36
|
basename === worktreeName;
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
|
-
export async function detectWorktreeType(worktree, mainWorktreePath) {
|
|
36
|
-
const isManaged = isManagedWorktreePath(worktree.path);
|
|
39
|
+
export async function detectWorktreeType(worktree, mainWorktreePath, managedRoot = YGG_ROOT) {
|
|
40
|
+
const isManaged = isManagedWorktreePath(worktree.path, managedRoot);
|
|
37
41
|
if (!isManaged) {
|
|
38
42
|
return worktree.path === mainWorktreePath ? 'MAIN' : 'LINKED';
|
|
39
43
|
}
|