yggtree 1.4.4 → 1.4.6
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 +103 -22
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +6 -5
- package/dist/commands/wt/copy-env.js +31 -0
- package/dist/commands/wt/create-branch.js +3 -2
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +3 -2
- package/dist/commands/wt/delete.js +102 -44
- package/dist/commands/wt/enter.js +6 -4
- package/dist/commands/wt/exec.js +5 -3
- package/dist/commands/wt/list.js +4 -2
- package/dist/commands/wt/open.js +30 -12
- package/dist/commands/wt/path.js +5 -3
- package/dist/index.js +141 -57
- package/dist/lib/global-config.js +118 -0
- package/dist/lib/help.js +127 -0
- package/dist/lib/main-menu.js +71 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +77 -6
- package/dist/lib/worktree.js +14 -10
- package/package.json +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
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, layout, repoRoot) {
|
|
23
|
+
if (value)
|
|
24
|
+
return path.resolve(expandHome(value));
|
|
25
|
+
if (layout === 'claude') {
|
|
26
|
+
return path.join(path.resolve(repoRoot || process.cwd()), '.claude', 'worktrees');
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(YGG_ROOT);
|
|
29
|
+
}
|
|
30
|
+
export function normalizeWorktreesRootInput(value) {
|
|
31
|
+
return path.resolve(expandHome(value));
|
|
32
|
+
}
|
|
33
|
+
function normalizeLayout(value) {
|
|
34
|
+
return value || DEFAULT_WORKTREE_LAYOUT;
|
|
35
|
+
}
|
|
36
|
+
function isWorktreeLayout(value) {
|
|
37
|
+
return value === 'yggtree' || value === 'codex' || value === 'claude';
|
|
38
|
+
}
|
|
39
|
+
export async function readGlobalConfig() {
|
|
40
|
+
try {
|
|
41
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
42
|
+
const config = await fs.readJSON(CONFIG_PATH);
|
|
43
|
+
return {
|
|
44
|
+
worktreesRoot: typeof config.worktreesRoot === 'string' ? config.worktreesRoot : undefined,
|
|
45
|
+
worktreeLayout: isWorktreeLayout(config.worktreeLayout) ? config.worktreeLayout : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
export async function writeGlobalConfig(config) {
|
|
55
|
+
await fs.ensureDir(YGG_ROOT);
|
|
56
|
+
await fs.writeJSON(CONFIG_PATH, config, { spaces: 2 });
|
|
57
|
+
}
|
|
58
|
+
export async function getWorktreePathConfig(repoRoot, presetOverride) {
|
|
59
|
+
const config = presetOverride === undefined
|
|
60
|
+
? await readGlobalConfig()
|
|
61
|
+
: getPresetConfig(presetOverride);
|
|
62
|
+
if (!config) {
|
|
63
|
+
throw new Error(`Unknown config preset "${presetOverride}". Available presets: default, yggtree, codex, claude.`);
|
|
64
|
+
}
|
|
65
|
+
const layout = normalizeLayout(config.worktreeLayout);
|
|
66
|
+
return {
|
|
67
|
+
root: normalizeRoot(config.worktreesRoot, layout, repoRoot),
|
|
68
|
+
layout,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function getManagedWorktreesRoot(repoRoot) {
|
|
72
|
+
const config = await getWorktreePathConfig(repoRoot);
|
|
73
|
+
return config.root;
|
|
74
|
+
}
|
|
75
|
+
export function buildManagedWorktreePath(repoName, worktreeSlug, config) {
|
|
76
|
+
if (config.layout === 'codex') {
|
|
77
|
+
return path.join(config.root, worktreeSlug, repoName);
|
|
78
|
+
}
|
|
79
|
+
if (config.layout === 'claude') {
|
|
80
|
+
return path.join(config.root, worktreeSlug);
|
|
81
|
+
}
|
|
82
|
+
return path.join(config.root, repoName, worktreeSlug);
|
|
83
|
+
}
|
|
84
|
+
export function getPresetConfig(preset) {
|
|
85
|
+
const normalized = preset.trim().toLowerCase();
|
|
86
|
+
if (normalized === 'default' || normalized === 'yggtree') {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
if (normalized === 'codex') {
|
|
90
|
+
return {
|
|
91
|
+
worktreesRoot: '~/.codex/worktrees',
|
|
92
|
+
worktreeLayout: 'codex',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (normalized === 'claude') {
|
|
96
|
+
return {
|
|
97
|
+
worktreeLayout: 'claude',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function inferPreset(config) {
|
|
103
|
+
if (!config.worktreesRoot && !config.worktreeLayout)
|
|
104
|
+
return 'default';
|
|
105
|
+
if (!config.worktreesRoot && config.worktreeLayout === 'claude')
|
|
106
|
+
return 'claude';
|
|
107
|
+
if (config.worktreesRoot === '~/.codex/worktrees' && config.worktreeLayout === 'codex')
|
|
108
|
+
return 'codex';
|
|
109
|
+
return 'custom';
|
|
110
|
+
}
|
|
111
|
+
export function formatGlobalConfig(config, resolved) {
|
|
112
|
+
return [
|
|
113
|
+
`worktreesRoot: ${formatHome(resolved.root)}`,
|
|
114
|
+
`worktreeLayout: ${resolved.layout}`,
|
|
115
|
+
`configFile: ${formatHome(CONFIG_PATH)}`,
|
|
116
|
+
`preset: ${inferPreset(config)}`,
|
|
117
|
+
];
|
|
118
|
+
}
|
package/dist/lib/help.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
Agent-native path layout yggtree config use claude # or codex
|
|
10
|
+
Delete without prompts yggtree delete branch-name --yes
|
|
11
|
+
|
|
12
|
+
More detail: run yggtree help <command>, for example yggtree help handoff.
|
|
13
|
+
`;
|
|
14
|
+
const agentPathTip = `
|
|
15
|
+
Agent-native paths:
|
|
16
|
+
Add --config claude for Claude Code's repo-local .claude/worktrees layout.
|
|
17
|
+
Add --config codex for Codex's ~/.codex/worktrees layout.
|
|
18
|
+
`;
|
|
19
|
+
export const createHelp = `
|
|
20
|
+
Behavior:
|
|
21
|
+
Creates an official branch-backed worktree, publishes the branch to origin, and enters the shell by default.
|
|
22
|
+
Use for real task branches. For disposable experiments, use create-sandbox.
|
|
23
|
+
|
|
24
|
+
${agentPathTip}
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
yggtree create feat/new-flow --base main --source remote
|
|
28
|
+
yggtree create feat/background-task --base main --source remote --no-open --no-enter
|
|
29
|
+
yggtree create feat/claude-layout --base main --source remote --config claude
|
|
30
|
+
`;
|
|
31
|
+
export const createMultiHelp = `
|
|
32
|
+
Behavior:
|
|
33
|
+
Bulk-creates official branch-backed worktrees.
|
|
34
|
+
This does not share create's open/enter/exec lifecycle. Use create for one task branch.
|
|
35
|
+
|
|
36
|
+
${agentPathTip}
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
yggtree create-multi --base main --source remote
|
|
40
|
+
yggtree create-multi --base main --source remote --config codex
|
|
41
|
+
`;
|
|
42
|
+
export const checkoutHelp = `
|
|
43
|
+
Behavior:
|
|
44
|
+
Creates or reuses a worktree for an existing branch or ref without disturbing current work.
|
|
45
|
+
Use for interruptions and branch review. Use create for a new official task branch.
|
|
46
|
+
Enters the worktree shell by default; add --no-enter when automation should return.
|
|
47
|
+
|
|
48
|
+
${agentPathTip}
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
yggtree wc --ref hotfix/payment-timeout
|
|
52
|
+
yggtree wc --ref main --name fresh-main --no-open --no-enter
|
|
53
|
+
yggtree wc --ref main --name fresh-main --config yggtree
|
|
54
|
+
`;
|
|
55
|
+
export const openHelp = `
|
|
56
|
+
Behavior:
|
|
57
|
+
Opens an existing worktree in an editor, desktop app, or terminal target.
|
|
58
|
+
Returns by default after opening. Add --enter only when the shell should continue inside the worktree.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
yggtree open my-feature --tool cursor
|
|
62
|
+
yggtree open my-feature --tool codex-app
|
|
63
|
+
yggtree list --open
|
|
64
|
+
`;
|
|
65
|
+
export const deleteHelp = `
|
|
66
|
+
Behavior:
|
|
67
|
+
Deletes selected worktrees. Main and current worktrees are always protected.
|
|
68
|
+
Without arguments, delete stays interactive. For automation, pass explicit targets and --yes.
|
|
69
|
+
Add --all only when the target is outside Yggtree's managed root and appears as LINKED in list.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
yggtree delete my-feature --yes
|
|
73
|
+
yggtree delete my-feature other-feature --yes
|
|
74
|
+
yggtree delete --all --yes
|
|
75
|
+
yggtree delete external-feature --all --yes
|
|
76
|
+
`;
|
|
77
|
+
export const createSandboxHelp = `
|
|
78
|
+
Behavior:
|
|
79
|
+
Creates a local-only disposable experiment from the current branch.
|
|
80
|
+
For continuing current dirty work, prefer handoff instead of teaching create-sandbox --carry.
|
|
81
|
+
|
|
82
|
+
${agentPathTip}
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
yggtree create-sandbox
|
|
86
|
+
yggtree create-sandbox --name ui-option-a --no-open
|
|
87
|
+
yggtree create-sandbox --name claude-scratch --config claude
|
|
88
|
+
|
|
89
|
+
Related:
|
|
90
|
+
handoff carries current dirty work into a named sandbox.
|
|
91
|
+
apply copies changed sandbox files back to origin.
|
|
92
|
+
`;
|
|
93
|
+
export const handoffHelp = `
|
|
94
|
+
Behavior:
|
|
95
|
+
Carry staged, unstaged, and untracked work into a named sandbox so you can continue there.
|
|
96
|
+
The origin checkout is not cleaned; review or reset it separately if needed.
|
|
97
|
+
|
|
98
|
+
${agentPathTip}
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
yggtree handoff --name continue-auth-refactor
|
|
102
|
+
yggtree handoff --name continue-auth-refactor --config codex
|
|
103
|
+
|
|
104
|
+
Related:
|
|
105
|
+
create-sandbox starts a disposable local experiment.
|
|
106
|
+
apply copies changed sandbox files back to origin.
|
|
107
|
+
`;
|
|
108
|
+
export const applyHelp = `
|
|
109
|
+
Behavior:
|
|
110
|
+
Copies changed files from the current sandbox back to the origin checkout.
|
|
111
|
+
Stores backups in sandbox metadata before overwriting origin files.
|
|
112
|
+
Not a Git merge, rebase, patch, or cherry-pick. Review the origin diff afterward.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
yggtree apply
|
|
116
|
+
|
|
117
|
+
Related:
|
|
118
|
+
unapply restores from the sandbox backup metadata while the sandbox still exists.
|
|
119
|
+
`;
|
|
120
|
+
export const unapplyHelp = `
|
|
121
|
+
Behavior:
|
|
122
|
+
Restores origin files from sandbox metadata created by apply.
|
|
123
|
+
Only works while the sandbox still exists.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
yggtree unapply
|
|
127
|
+
`;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function buildMainMenuEntries(context) {
|
|
2
|
+
const dailyChoices = actions([
|
|
3
|
+
'create-smart',
|
|
4
|
+
'worktree-checkout',
|
|
5
|
+
'handoff',
|
|
6
|
+
'open',
|
|
7
|
+
'list',
|
|
8
|
+
]);
|
|
9
|
+
const growthChoices = actions([
|
|
10
|
+
'create-multi',
|
|
11
|
+
'create-sandbox',
|
|
12
|
+
]);
|
|
13
|
+
const tendingChoices = actions([
|
|
14
|
+
'bootstrap',
|
|
15
|
+
'exec',
|
|
16
|
+
'path',
|
|
17
|
+
'delete',
|
|
18
|
+
'prune',
|
|
19
|
+
]);
|
|
20
|
+
const sandboxChoices = actions([
|
|
21
|
+
'apply',
|
|
22
|
+
'unapply',
|
|
23
|
+
]);
|
|
24
|
+
const loreChoices = actions([
|
|
25
|
+
'bifrost',
|
|
26
|
+
'thor',
|
|
27
|
+
'docs',
|
|
28
|
+
'exit',
|
|
29
|
+
]);
|
|
30
|
+
if (context === 'sandbox') {
|
|
31
|
+
return [
|
|
32
|
+
...sandboxChoices,
|
|
33
|
+
separator('Daily routes'),
|
|
34
|
+
...dailyChoices,
|
|
35
|
+
separator('Growth and experiments'),
|
|
36
|
+
...growthChoices,
|
|
37
|
+
separator('Tend realms'),
|
|
38
|
+
...tendingChoices,
|
|
39
|
+
separator('Lore'),
|
|
40
|
+
...loreChoices,
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
if (context === 'worktree') {
|
|
44
|
+
return [
|
|
45
|
+
...actions(['copy-env', 'path', 'exec', 'bootstrap']),
|
|
46
|
+
separator('Daily routes'),
|
|
47
|
+
...dailyChoices,
|
|
48
|
+
separator('Growth and experiments'),
|
|
49
|
+
...growthChoices,
|
|
50
|
+
separator('Tend realms'),
|
|
51
|
+
...actions(['delete', 'prune']),
|
|
52
|
+
separator('Lore'),
|
|
53
|
+
...loreChoices,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
...dailyChoices,
|
|
58
|
+
separator('Growth and experiments'),
|
|
59
|
+
...growthChoices,
|
|
60
|
+
separator('Tend realms'),
|
|
61
|
+
...tendingChoices,
|
|
62
|
+
separator('Lore'),
|
|
63
|
+
...loreChoices,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function actions(values) {
|
|
67
|
+
return values.map(value => ({ type: 'action', value }));
|
|
68
|
+
}
|
|
69
|
+
function separator(label) {
|
|
70
|
+
return { type: 'separator', label };
|
|
71
|
+
}
|
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,86 @@ 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 = (() => {
|
|
38
|
+
if (options.context === 'sandbox') {
|
|
39
|
+
return 'Sandbox tools appear first because this realm can apply or undo grafted changes.';
|
|
40
|
+
}
|
|
41
|
+
if (options.context === 'worktree') {
|
|
42
|
+
return 'Current-realm tools appear first for this worktree.';
|
|
43
|
+
}
|
|
44
|
+
return 'Create, enter, inspect, or tend your worktrees.';
|
|
45
|
+
})();
|
|
46
|
+
return [
|
|
47
|
+
'Choose the next realm action.',
|
|
48
|
+
yggMuted('Daily routes first. Maintenance and lore wait below.'),
|
|
49
|
+
'',
|
|
50
|
+
`${yggBark('┌')} ${badge('route 01')} ${chalk.bold('Realm')}`,
|
|
51
|
+
`${yggBark('│')} ${yggMuted(detail)}`,
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
export function formatMainMenuChoice(options) {
|
|
55
|
+
const tone = menuTone(options.tone);
|
|
56
|
+
const label = options.label.padEnd(24);
|
|
57
|
+
return `${tone(options.glyph)} ${chalk.bold(label)} ${yggMuted(options.detail)}`;
|
|
58
|
+
}
|
|
59
|
+
function renderUpdateNotice(update) {
|
|
60
|
+
if (!update?.updateAvailable)
|
|
61
|
+
return [];
|
|
62
|
+
return [
|
|
63
|
+
'',
|
|
64
|
+
`${yggWarn('Update available')} ${yggMuted(`yggtree ${update.currentVersion} -> ${update.latestVersion}`)}`,
|
|
65
|
+
yggMuted('Run: npm install -g yggtree'),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
function badge(value) {
|
|
69
|
+
return yggRoot(` ${value} `);
|
|
70
|
+
}
|
|
71
|
+
function menuTone(tone) {
|
|
72
|
+
switch (tone) {
|
|
73
|
+
case 'growth':
|
|
74
|
+
return yggLeaf;
|
|
75
|
+
case 'travel':
|
|
76
|
+
return yggSky;
|
|
77
|
+
case 'sandbox':
|
|
78
|
+
return yggRoot;
|
|
79
|
+
case 'tending':
|
|
80
|
+
return yggBark;
|
|
81
|
+
case 'danger':
|
|
82
|
+
return yggDanger;
|
|
83
|
+
case 'lore':
|
|
84
|
+
return yggAccent;
|
|
85
|
+
case 'exit':
|
|
86
|
+
return yggMuted;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
18
89
|
// --- Logger ---
|
|
19
90
|
export const log = {
|
|
20
91
|
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
|
}
|