@mbluemer_2/gittyup 0.1.2 → 0.1.8

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/program.js DELETED
@@ -1,293 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createProgram = createProgram;
4
- exports.runCli = runCli;
5
- const commander_1 = require("commander");
6
- const aliases_1 = require("./aliases");
7
- const config_1 = require("./config");
8
- const errors_1 = require("./errors");
9
- const git_1 = require("./git");
10
- const interactive_1 = require("./interactive");
11
- const projects_1 = require("./projects");
12
- const output_1 = require("./output");
13
- const tmux_1 = require("./tmux");
14
- const defaultServices = {
15
- resolveRootDir: config_1.resolveRootDir,
16
- checkGitAvailable: git_1.checkGitAvailable,
17
- discoverProjects: projects_1.discoverProjects,
18
- diagnoseProjects: projects_1.diagnoseProjects,
19
- createProject: projects_1.createProject,
20
- cloneProject: projects_1.cloneProject,
21
- importProject: projects_1.importProject,
22
- renameProject: projects_1.renameProject,
23
- getProjectInit: projects_1.getProjectInit,
24
- configureProjectInit: projects_1.configureProjectInit,
25
- clearConfiguredProjectInit: projects_1.clearConfiguredProjectInit,
26
- listProjectWorktrees: projects_1.listProjectWorktrees,
27
- addProjectWorktree: projects_1.addProjectWorktree,
28
- removeProjectWorktree: projects_1.removeProjectWorktree,
29
- getProjectStatuses: projects_1.getProjectStatuses,
30
- getProjectWorktree: projects_1.getProjectWorktree,
31
- resolveWorktreeAddInput: interactive_1.resolveWorktreeAddInput,
32
- resolveWorktreeRemoveInput: interactive_1.resolveWorktreeRemoveInput,
33
- resolveTmuxLaunchInput: interactive_1.resolveTmuxLaunchInput,
34
- printProjects: output_1.printProjects,
35
- printProjectDiagnostics: output_1.printProjectDiagnostics,
36
- printProjectInit: output_1.printProjectInit,
37
- printWorktrees: output_1.printWorktrees,
38
- printStatuses: output_1.printStatuses,
39
- buildTmuxSessionName: tmux_1.buildTmuxSessionName,
40
- checkTmuxAvailable: tmux_1.checkTmuxAvailable,
41
- checkFzfAvailable: tmux_1.checkFzfAvailable,
42
- launchTmuxSession: tmux_1.launchTmuxSession,
43
- pickTmuxSession: tmux_1.pickTmuxSession,
44
- stdout: console,
45
- stderr: console,
46
- cwd: () => process.cwd(),
47
- };
48
- function createServices(overrides = {}) {
49
- return { ...defaultServices, ...overrides };
50
- }
51
- function createProgram(overrides = {}) {
52
- const services = createServices(overrides);
53
- const program = new commander_1.Command();
54
- function requireGit(command) {
55
- return command.hook('preAction', async () => {
56
- await services.checkGitAvailable();
57
- });
58
- }
59
- function getContext(command) {
60
- const options = command.optsWithGlobals();
61
- return {
62
- rootDir: services.resolveRootDir(options.root),
63
- json: Boolean(options.json),
64
- };
65
- }
66
- async function runAction(action) {
67
- try {
68
- await action();
69
- }
70
- catch (error) {
71
- services.stderr.error((0, errors_1.toErrorMessage)(error));
72
- process.exitCode = error instanceof errors_1.CliError ? error.exitCode : 1;
73
- }
74
- }
75
- program
76
- .name('gittyup')
77
- .description('Manage bare git projects and linked worktrees')
78
- .version('0.0.1')
79
- .showHelpAfterError()
80
- .showSuggestionAfterError()
81
- .option('--root <path>', 'root directory containing project folders', '~/src')
82
- .option('--json', 'emit machine-readable JSON');
83
- const projectCommand = program
84
- .command('project')
85
- .alias('p')
86
- .description('Manage project roots');
87
- const projectListCommand = requireGit(projectCommand
88
- .command('list')
89
- .description('List discovered projects under the root directory'));
90
- projectListCommand.action(() => runAction(async () => {
91
- const context = getContext(projectListCommand);
92
- const projects = await services.discoverProjects(context.rootDir);
93
- services.printProjects(projects, context.json);
94
- }));
95
- const projectDoctorCommand = requireGit(projectCommand
96
- .command('doctor')
97
- .description('Inspect managed project folders and surface broken entries'));
98
- projectDoctorCommand.action(() => runAction(async () => {
99
- const context = getContext(projectDoctorCommand);
100
- const diagnostics = await services.diagnoseProjects(context.rootDir);
101
- services.printProjectDiagnostics(diagnostics, context.json);
102
- }));
103
- const projectCreateCommand = requireGit(projectCommand
104
- .command('create')
105
- .description('Create a new local bare repository project')
106
- .argument('<name>', 'project directory name'));
107
- projectCreateCommand.action((name) => runAction(async () => {
108
- const context = getContext(projectCreateCommand);
109
- const project = await services.createProject(context.rootDir, name);
110
- services.printProjects([project], context.json);
111
- }));
112
- const projectInitCommand = requireGit(projectCommand
113
- .command('init')
114
- .description('Manage the command run after new worktree creation'));
115
- const projectInitGetCommand = projectInitCommand
116
- .command('get')
117
- .description('Show the configured init command for a project')
118
- .argument('<project>', 'managed project name');
119
- projectInitGetCommand.action((projectName) => runAction(async () => {
120
- const context = getContext(projectInitGetCommand);
121
- const init = await services.getProjectInit(context.rootDir, projectName);
122
- services.printProjectInit(init, context.json);
123
- }));
124
- const projectInitSetCommand = projectInitCommand
125
- .command('set')
126
- .description('Set the init command run after new worktree creation')
127
- .argument('<project>', 'managed project name')
128
- .argument('<command>', 'shell command to run inside new worktrees');
129
- projectInitSetCommand.action((projectName, command) => runAction(async () => {
130
- const context = getContext(projectInitSetCommand);
131
- const init = await services.configureProjectInit(context.rootDir, projectName, command);
132
- services.printProjectInit(init, context.json);
133
- }));
134
- const projectInitClearCommand = projectInitCommand
135
- .command('clear')
136
- .description('Clear the configured init command for a project')
137
- .argument('<project>', 'managed project name');
138
- projectInitClearCommand.action((projectName) => runAction(async () => {
139
- const context = getContext(projectInitClearCommand);
140
- const init = await services.clearConfiguredProjectInit(context.rootDir, projectName);
141
- services.printProjectInit(init, context.json);
142
- }));
143
- const projectCloneCommand = requireGit(projectCommand
144
- .command('clone')
145
- .description('Clone a remote into the bare-repo layout and create a main worktree')
146
- .argument('<remote>', 'remote URL or path')
147
- .argument('[name]', 'override the project directory name'));
148
- projectCloneCommand.action((remote, name) => runAction(async () => {
149
- const context = getContext(projectCloneCommand);
150
- const project = await services.cloneProject(context.rootDir, remote, name);
151
- services.printProjects([project], context.json);
152
- }));
153
- const projectImportCommand = requireGit(projectCommand
154
- .command('import')
155
- .description('Import an existing local git repo into the bare-repo layout in a new project directory')
156
- .argument('<source>', 'path to an existing local git repository root')
157
- .argument('[name]', 'override the managed project directory name'));
158
- projectImportCommand.action((source, name) => runAction(async () => {
159
- const context = getContext(projectImportCommand);
160
- const project = await services.importProject(context.rootDir, source, name);
161
- services.printProjects([project], context.json);
162
- }));
163
- const projectRenameCommand = requireGit(projectCommand
164
- .command('rename')
165
- .description('Rename a managed project directory and repair linked worktrees')
166
- .argument('<project>', 'current managed project name')
167
- .argument('<name>', 'new managed project name'));
168
- projectRenameCommand.action((projectName, nextName) => runAction(async () => {
169
- const context = getContext(projectRenameCommand);
170
- const project = await services.renameProject(context.rootDir, projectName, nextName);
171
- services.printProjects([project], context.json);
172
- }));
173
- const worktreeCommand = program
174
- .command('worktree')
175
- .alias('w')
176
- .description('Manage linked worktrees');
177
- const worktreeListCommand = requireGit(worktreeCommand
178
- .command('list')
179
- .description('List linked worktrees')
180
- .argument('[project]', 'limit output to a single project'));
181
- worktreeListCommand.action((projectName) => runAction(async () => {
182
- const context = getContext(worktreeListCommand);
183
- const worktrees = await services.listProjectWorktrees(context.rootDir, projectName);
184
- services.printWorktrees(worktrees, context.json);
185
- }));
186
- const worktreeAddCommand = requireGit(worktreeCommand
187
- .command('add')
188
- .description('Create a linked worktree for a branch')
189
- .argument('[project]', 'project name')
190
- .argument('[branch]', 'branch name to check out or create')
191
- .option('-a, --alias <alias>', 'friendly directory name for the worktree')
192
- .option('--from <ref>', 'create a new branch from a specific start point')
193
- .option('--tmux', 'launch or attach a tmux session for the new worktree'));
194
- worktreeAddCommand.action((projectName, branch, options) => runAction(async () => {
195
- const context = getContext(worktreeAddCommand);
196
- const input = await services.resolveWorktreeAddInput({
197
- rootDir: context.rootDir,
198
- projectName,
199
- branch,
200
- });
201
- const worktree = await services.addProjectWorktree(context.rootDir, input.projectName, input.branch, options.alias, options.from);
202
- if (options.tmux) {
203
- await services.checkTmuxAvailable();
204
- if (context.json) {
205
- services.printWorktrees([worktree], true);
206
- }
207
- await services.launchTmuxSession(services.buildTmuxSessionName(worktree.project, worktree.alias), worktree.path);
208
- return;
209
- }
210
- services.printWorktrees([worktree], context.json);
211
- }));
212
- const worktreeRemoveCommand = requireGit(worktreeCommand
213
- .command('remove')
214
- .description('Remove a linked worktree by alias')
215
- .argument('[project]', 'project name')
216
- .argument('[alias]', 'worktree alias directory name')
217
- .option('-f, --force', 'remove even if the worktree has local changes'));
218
- worktreeRemoveCommand.action((projectName, alias, options) => runAction(async () => {
219
- const context = getContext(worktreeRemoveCommand);
220
- const input = await services.resolveWorktreeRemoveInput({
221
- rootDir: context.rootDir,
222
- projectName,
223
- alias,
224
- });
225
- const removed = await services.removeProjectWorktree(context.rootDir, input.projectName, input.alias, Boolean(options.force));
226
- if (context.json) {
227
- services.printWorktrees([removed], true);
228
- return;
229
- }
230
- services.stdout.log(`Removed worktree '${removed.alias}' from project '${removed.project}'.`);
231
- }));
232
- const statusCommand = requireGit(program
233
- .command('status')
234
- .alias('s')
235
- .description('Show local status for linked worktrees')
236
- .argument('[project]', 'limit output to a single project'));
237
- statusCommand.action((projectName) => runAction(async () => {
238
- const context = getContext(statusCommand);
239
- const statuses = await services.getProjectStatuses(context.rootDir, projectName);
240
- services.printStatuses(statuses, context.json);
241
- }));
242
- const tmuxCommand = program
243
- .command('tmux')
244
- .alias('t')
245
- .description('Manage tmux sessions');
246
- const tmuxLaunchCommand = requireGit(tmuxCommand
247
- .command('launch')
248
- .description('Launch or attach a tmux session for a worktree')
249
- .argument('[project]', 'project name')
250
- .argument('[worktree]', 'worktree alias directory name'));
251
- tmuxLaunchCommand.action((projectName, worktreeAlias) => runAction(async () => {
252
- const context = getContext(tmuxLaunchCommand);
253
- await services.checkTmuxAvailable();
254
- const input = await services.resolveTmuxLaunchInput({
255
- rootDir: context.rootDir,
256
- projectName,
257
- alias: worktreeAlias,
258
- });
259
- const worktree = await services.getProjectWorktree(context.rootDir, input.projectName, input.alias);
260
- const sessionName = services.buildTmuxSessionName(worktree.project, worktree.alias);
261
- if (context.json) {
262
- services.stdout.log(JSON.stringify({
263
- session: sessionName,
264
- project: worktree.project,
265
- worktree: worktree.alias,
266
- path: worktree.path,
267
- }, null, 2));
268
- }
269
- await services.launchTmuxSession(sessionName, worktree.path);
270
- }));
271
- const tmuxSwitchCommand = tmuxCommand
272
- .command('switch')
273
- .description('Pick and switch to an open tmux session with fzf');
274
- tmuxSwitchCommand.action(() => runAction(async () => {
275
- const context = getContext(tmuxSwitchCommand);
276
- await services.checkTmuxAvailable();
277
- await services.checkFzfAvailable();
278
- const sessionName = await services.pickTmuxSession();
279
- if (context.json) {
280
- services.stdout.log(JSON.stringify({ session: sessionName }, null, 2));
281
- }
282
- await services.launchTmuxSession(sessionName, services.cwd());
283
- }));
284
- return program;
285
- }
286
- async function runCli(argv = process.argv, overrides = {}) {
287
- const services = createServices(overrides);
288
- const program = createProgram(services);
289
- await program.parseAsync((0, aliases_1.normalizeArgv)(argv)).catch((error) => {
290
- services.stderr.error((0, errors_1.toErrorMessage)(error));
291
- process.exitCode = error instanceof errors_1.CliError ? error.exitCode : 1;
292
- });
293
- }
package/dist/projects.js DELETED
@@ -1,308 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.BARE_REPO_DIRNAME = void 0;
7
- exports.discoverProjects = discoverProjects;
8
- exports.diagnoseProjects = diagnoseProjects;
9
- exports.createProject = createProject;
10
- exports.cloneProject = cloneProject;
11
- exports.importProject = importProject;
12
- exports.renameProject = renameProject;
13
- exports.listProjectWorktrees = listProjectWorktrees;
14
- exports.addProjectWorktree = addProjectWorktree;
15
- exports.getProjectInit = getProjectInit;
16
- exports.configureProjectInit = configureProjectInit;
17
- exports.clearConfiguredProjectInit = clearConfiguredProjectInit;
18
- exports.getProjectWorktree = getProjectWorktree;
19
- exports.removeProjectWorktree = removeProjectWorktree;
20
- exports.getProjectStatuses = getProjectStatuses;
21
- const promises_1 = require("node:fs/promises");
22
- const node_path_1 = __importDefault(require("node:path"));
23
- const config_1 = require("./config");
24
- Object.defineProperty(exports, "BARE_REPO_DIRNAME", { enumerable: true, get: function () { return config_1.BARE_REPO_DIRNAME; } });
25
- const errors_1 = require("./errors");
26
- const git_1 = require("./git");
27
- const metadata_1 = require("./metadata");
28
- const process_1 = require("./process");
29
- const tmux_1 = require("./tmux");
30
- async function pathExists(targetPath) {
31
- try {
32
- await (0, promises_1.access)(targetPath);
33
- return true;
34
- }
35
- catch {
36
- return false;
37
- }
38
- }
39
- async function isDirectory(targetPath) {
40
- try {
41
- const entries = await (0, promises_1.readdir)(targetPath);
42
- return Array.isArray(entries);
43
- }
44
- catch {
45
- return false;
46
- }
47
- }
48
- async function loadProjectRecord(rootDir, projectName) {
49
- const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
50
- if (!(await isDirectory(project.gitDir))) {
51
- throw new errors_1.CliError(`Managed project '${project.name}' is missing ${config_1.BARE_REPO_DIRNAME} under ${project.rootPath}.`);
52
- }
53
- const [worktrees, hasCommits, defaultBranch, init] = await Promise.all([
54
- (0, git_1.listLinkedWorktrees)(project.gitDir),
55
- (0, git_1.repoHasCommits)(project.gitDir),
56
- (0, git_1.getDefaultBranch)(project.gitDir),
57
- (0, metadata_1.getProjectInitRecord)(rootDir, project.name),
58
- ]);
59
- void init;
60
- return {
61
- ...project,
62
- defaultBranch,
63
- hasCommits,
64
- worktreeCount: worktrees.length,
65
- };
66
- }
67
- async function inspectProject(rootDir, projectName) {
68
- try {
69
- return await loadProjectRecord(rootDir, projectName);
70
- }
71
- catch (error) {
72
- const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
73
- return {
74
- name: project.name,
75
- rootPath: project.rootPath,
76
- ok: false,
77
- error: error instanceof Error ? error.message : String(error),
78
- };
79
- }
80
- }
81
- async function runProjectInitCommand(rootDir, project, worktree) {
82
- const init = await (0, metadata_1.getProjectInitRecord)(rootDir, project.name);
83
- if (!init.initCommand) {
84
- return;
85
- }
86
- try {
87
- await (0, process_1.runShellCommand)(init.initCommand, { cwd: worktree.path });
88
- }
89
- catch (error) {
90
- throw new errors_1.CliError(`Worktree '${worktree.alias}' was created, but project init command failed: ${init.initCommand}\n${error instanceof Error ? error.message : String(error)}`);
91
- }
92
- }
93
- function sortProjects(projects) {
94
- return projects.sort((left, right) => left.name.localeCompare(right.name));
95
- }
96
- function sortWorktrees(worktrees) {
97
- return worktrees.sort((left, right) => {
98
- const projectCompare = left.project.localeCompare(right.project);
99
- if (projectCompare !== 0) {
100
- return projectCompare;
101
- }
102
- return left.alias.localeCompare(right.alias);
103
- });
104
- }
105
- async function discoverProjects(rootDir) {
106
- const diagnostics = await diagnoseProjects(rootDir);
107
- return sortProjects(diagnostics.filter((entry) => 'gitDir' in entry));
108
- }
109
- async function diagnoseProjects(rootDir) {
110
- if (!(await pathExists(rootDir))) {
111
- return [];
112
- }
113
- const entries = await (0, promises_1.readdir)(rootDir, { withFileTypes: true });
114
- const projectNames = entries
115
- .filter((entry) => entry.isDirectory())
116
- .map((entry) => entry.name);
117
- const projects = await Promise.all(projectNames.map((projectName) => inspectProject(rootDir, projectName)));
118
- return sortProjects(projects);
119
- }
120
- async function createProject(rootDir, projectName) {
121
- const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
122
- await (0, promises_1.mkdir)(rootDir, { recursive: true });
123
- if (await pathExists(project.rootPath)) {
124
- throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
125
- }
126
- let createdRootPath = false;
127
- try {
128
- await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
129
- createdRootPath = true;
130
- await (0, git_1.initBareRepo)(project.gitDir);
131
- await (0, git_1.bootstrapBareRepo)(project.gitDir, config_1.MAIN_WORKTREE_ALIAS);
132
- await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), config_1.MAIN_WORKTREE_ALIAS);
133
- return loadProjectRecord(rootDir, project.name);
134
- }
135
- catch (error) {
136
- if (createdRootPath) {
137
- await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
138
- }
139
- throw error;
140
- }
141
- }
142
- async function cloneProject(rootDir, remote, projectName) {
143
- const validatedRemote = (0, config_1.validateRemoteUrl)(remote);
144
- const project = (0, config_1.buildProjectPaths)(rootDir, projectName ?? (0, config_1.inferProjectName)(validatedRemote));
145
- await (0, promises_1.mkdir)(rootDir, { recursive: true });
146
- if (await pathExists(project.rootPath)) {
147
- throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
148
- }
149
- let createdRootPath = false;
150
- try {
151
- await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
152
- createdRootPath = true;
153
- await (0, git_1.cloneBareRepo)(validatedRemote, project.gitDir);
154
- const defaultBranch = await (0, git_1.getDefaultBranch)(project.gitDir);
155
- if ((await (0, git_1.repoHasCommits)(project.gitDir)) && defaultBranch) {
156
- await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), defaultBranch);
157
- }
158
- return loadProjectRecord(rootDir, project.name);
159
- }
160
- catch (error) {
161
- if (createdRootPath) {
162
- await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
163
- }
164
- throw error;
165
- }
166
- }
167
- async function importProject(rootDir, sourcePath, projectName) {
168
- const resolvedSourcePath = await (0, promises_1.realpath)(node_path_1.default.resolve(sourcePath));
169
- const sourceTopLevel = await (0, promises_1.realpath)(node_path_1.default.resolve(await (0, git_1.getRepoTopLevel)(resolvedSourcePath)));
170
- if (resolvedSourcePath !== sourceTopLevel) {
171
- throw new errors_1.CliError(`Source path must be the repository root: ${sourceTopLevel}`);
172
- }
173
- if (await (0, git_1.isBareRepository)(resolvedSourcePath)) {
174
- throw new errors_1.CliError('Source repository must be a non-bare working repository.');
175
- }
176
- const project = (0, config_1.buildProjectPaths)(rootDir, projectName ?? node_path_1.default.basename(sourceTopLevel));
177
- await (0, promises_1.mkdir)(rootDir, { recursive: true });
178
- if (await pathExists(project.rootPath)) {
179
- throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
180
- }
181
- let createdRootPath = false;
182
- try {
183
- await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
184
- createdRootPath = true;
185
- await (0, git_1.cloneBareRepo)(sourceTopLevel, project.gitDir);
186
- const defaultBranch = await (0, git_1.getDefaultBranch)(project.gitDir);
187
- if ((await (0, git_1.repoHasCommits)(project.gitDir)) && defaultBranch) {
188
- await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), defaultBranch);
189
- }
190
- return loadProjectRecord(rootDir, project.name);
191
- }
192
- catch (error) {
193
- if (createdRootPath) {
194
- await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
195
- }
196
- throw error;
197
- }
198
- }
199
- async function renameProject(rootDir, projectName, nextProjectName) {
200
- const currentProject = await loadProjectRecord(rootDir, projectName);
201
- const nextProject = (0, config_1.buildProjectPaths)(rootDir, nextProjectName);
202
- if (currentProject.name === nextProject.name) {
203
- throw new errors_1.CliError('New project name must be different from the current name.');
204
- }
205
- if (await pathExists(nextProject.rootPath)) {
206
- throw new errors_1.CliError(`Project path already exists: ${nextProject.rootPath}`);
207
- }
208
- const currentRootPath = await (0, promises_1.realpath)(currentProject.rootPath);
209
- const worktrees = await (0, git_1.listLinkedWorktrees)(currentProject.gitDir);
210
- const currentWorktreePaths = worktrees.map((worktree) => worktree.path);
211
- await (0, promises_1.rename)(currentProject.rootPath, nextProject.rootPath);
212
- try {
213
- const nextRootPath = await (0, promises_1.realpath)(nextProject.rootPath);
214
- const repairedPaths = worktrees.map((worktree) => node_path_1.default.join(nextRootPath, node_path_1.default.relative(currentRootPath, worktree.path)));
215
- if (repairedPaths.length > 0) {
216
- await (0, git_1.repairWorktrees)(nextProject.gitDir, repairedPaths);
217
- }
218
- }
219
- catch (error) {
220
- await (0, promises_1.rename)(nextProject.rootPath, currentProject.rootPath);
221
- if (currentWorktreePaths.length > 0) {
222
- await (0, git_1.repairWorktrees)(currentProject.gitDir, currentWorktreePaths);
223
- }
224
- throw new errors_1.CliError(`Failed to rename project '${currentProject.name}' to '${nextProject.name}'. Changes were rolled back: ${error instanceof Error ? error.message : String(error)}`);
225
- }
226
- return loadProjectRecord(rootDir, nextProject.name);
227
- }
228
- async function listProjectWorktrees(rootDir, projectName) {
229
- const projects = projectName
230
- ? [await loadProjectRecord(rootDir, projectName)]
231
- : await discoverProjects(rootDir);
232
- const worktrees = await Promise.all(projects.map(async (project) => {
233
- const entries = await (0, git_1.listLinkedWorktrees)(project.gitDir);
234
- return entries.map((entry) => ({
235
- ...entry,
236
- project: project.name,
237
- rootPath: project.rootPath,
238
- gitDir: project.gitDir,
239
- }));
240
- }));
241
- return sortWorktrees(worktrees.flat());
242
- }
243
- async function addProjectWorktree(rootDir, projectName, branch, alias, from) {
244
- const validatedBranch = (0, config_1.validateBranchName)(branch);
245
- const project = await loadProjectRecord(rootDir, projectName);
246
- const worktreeAlias = alias
247
- ? (0, config_1.validateAlias)(alias)
248
- : (0, config_1.deriveAlias)(validatedBranch);
249
- const worktreePath = node_path_1.default.join(project.rootPath, worktreeAlias);
250
- const existingWorktrees = await (0, git_1.listLinkedWorktrees)(project.gitDir);
251
- if (!project.hasCommits) {
252
- throw new errors_1.CliError(`Project '${project.name}' has no commits yet. Create the first commit before adding linked worktrees.`);
253
- }
254
- if (await pathExists(worktreePath)) {
255
- throw new errors_1.CliError(`Worktree path already exists: ${worktreePath}`);
256
- }
257
- if (existingWorktrees.some((entry) => entry.alias === worktreeAlias)) {
258
- throw new errors_1.CliError(`Worktree alias '${worktreeAlias}' already exists in project '${project.name}'.`);
259
- }
260
- await (0, git_1.addWorktree)(project.gitDir, worktreePath, validatedBranch, from);
261
- const created = (await listProjectWorktrees(rootDir, project.name)).find((entry) => entry.alias === worktreeAlias);
262
- if (!created) {
263
- throw new errors_1.CliError(`Worktree '${worktreeAlias}' was created but could not be rediscovered.`);
264
- }
265
- await runProjectInitCommand(rootDir, project, created);
266
- return created;
267
- }
268
- async function getProjectInit(rootDir, projectName) {
269
- const project = await loadProjectRecord(rootDir, projectName);
270
- return (0, metadata_1.getProjectInitRecord)(rootDir, project.name);
271
- }
272
- async function configureProjectInit(rootDir, projectName, command) {
273
- const project = await loadProjectRecord(rootDir, projectName);
274
- return (0, metadata_1.setProjectInitCommand)(rootDir, project.name, command);
275
- }
276
- async function clearConfiguredProjectInit(rootDir, projectName) {
277
- const project = await loadProjectRecord(rootDir, projectName);
278
- return (0, metadata_1.clearProjectInitCommand)(rootDir, project.name);
279
- }
280
- async function getProjectWorktree(rootDir, projectName, alias) {
281
- const worktreeAlias = (0, config_1.validateAlias)(alias);
282
- const worktrees = await listProjectWorktrees(rootDir, projectName);
283
- const target = worktrees.find((entry) => entry.alias === worktreeAlias);
284
- if (!target) {
285
- throw new errors_1.CliError(`Worktree '${worktreeAlias}' was not found in project '${projectName}'.`);
286
- }
287
- return target;
288
- }
289
- async function removeProjectWorktree(rootDir, projectName, alias, force) {
290
- if (alias === config_1.MAIN_WORKTREE_ALIAS) {
291
- throw new errors_1.CliError("Cannot remove the 'main' worktree. It is the canonical linked worktree for managed projects.");
292
- }
293
- const project = await loadProjectRecord(rootDir, projectName);
294
- const target = await getProjectWorktree(rootDir, project.name, alias);
295
- await (0, git_1.removeWorktree)(project.gitDir, target.path, force);
296
- await (0, tmux_1.killTmuxSessionIfExists)((0, tmux_1.buildTmuxSessionName)(target.project, target.alias));
297
- return target;
298
- }
299
- async function getProjectStatuses(rootDir, projectName) {
300
- const worktrees = await listProjectWorktrees(rootDir, projectName);
301
- const statuses = await Promise.all(worktrees.map(async (worktree) => ({
302
- ...(await (0, git_1.getWorktreeStatus)(worktree)),
303
- project: worktree.project,
304
- rootPath: worktree.rootPath,
305
- gitDir: worktree.gitDir,
306
- })));
307
- return sortWorktrees(statuses);
308
- }