@mbluemer_2/gittyup 0.1.2
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 +265 -0
- package/dist/aliases.js +32 -0
- package/dist/cli.js +5 -0
- package/dist/config.js +127 -0
- package/dist/errors.js +19 -0
- package/dist/git.js +293 -0
- package/dist/interactive.js +206 -0
- package/dist/metadata.js +83 -0
- package/dist/output.js +211 -0
- package/dist/process.js +46 -0
- package/dist/program.js +293 -0
- package/dist/projects.js +308 -0
- package/dist/tmux.js +146 -0
- package/dist/types.js +2 -0
- package/package.json +67 -0
package/dist/output.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
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.printProjects = printProjects;
|
|
7
|
+
exports.printProjectDiagnostics = printProjectDiagnostics;
|
|
8
|
+
exports.printProjectInit = printProjectInit;
|
|
9
|
+
exports.printWorktrees = printWorktrees;
|
|
10
|
+
exports.printStatuses = printStatuses;
|
|
11
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
12
|
+
function printJson(value) {
|
|
13
|
+
console.log(JSON.stringify(value, null, 2));
|
|
14
|
+
}
|
|
15
|
+
function getTerminalWidth() {
|
|
16
|
+
return process.stdout.columns ?? 100;
|
|
17
|
+
}
|
|
18
|
+
function truncate(value, width) {
|
|
19
|
+
if (value.length <= width) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (width <= 1) {
|
|
23
|
+
return value.slice(0, width);
|
|
24
|
+
}
|
|
25
|
+
return `${value.slice(0, width - 1)}…`;
|
|
26
|
+
}
|
|
27
|
+
function buildColumnWidths(headers, rows, maxWidths) {
|
|
28
|
+
const gapWidth = 2;
|
|
29
|
+
const terminalWidth = getTerminalWidth();
|
|
30
|
+
const widths = headers.map((header, index) => {
|
|
31
|
+
const longestValue = Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0));
|
|
32
|
+
return Math.min(longestValue, maxWidths[index] ?? longestValue);
|
|
33
|
+
});
|
|
34
|
+
const usedWidth = widths.reduce((sum, width) => sum + width, 0) +
|
|
35
|
+
gapWidth * (headers.length - 1);
|
|
36
|
+
if (usedWidth <= terminalWidth) {
|
|
37
|
+
return widths;
|
|
38
|
+
}
|
|
39
|
+
const overflow = usedWidth - terminalWidth;
|
|
40
|
+
const lastColumn = widths.length - 1;
|
|
41
|
+
widths[lastColumn] = Math.max(headers[lastColumn]?.length ?? 4, widths[lastColumn] - overflow);
|
|
42
|
+
return widths;
|
|
43
|
+
}
|
|
44
|
+
function createTable(headers, colWidths) {
|
|
45
|
+
return new cli_table3_1.default({
|
|
46
|
+
head: headers,
|
|
47
|
+
colWidths,
|
|
48
|
+
wordWrap: true,
|
|
49
|
+
style: {
|
|
50
|
+
head: [],
|
|
51
|
+
border: [],
|
|
52
|
+
compact: true,
|
|
53
|
+
'padding-left': 0,
|
|
54
|
+
'padding-right': 0,
|
|
55
|
+
},
|
|
56
|
+
chars: {
|
|
57
|
+
top: '',
|
|
58
|
+
'top-mid': '',
|
|
59
|
+
'top-left': '',
|
|
60
|
+
'top-right': '',
|
|
61
|
+
bottom: '',
|
|
62
|
+
'bottom-mid': '',
|
|
63
|
+
'bottom-left': '',
|
|
64
|
+
'bottom-right': '',
|
|
65
|
+
left: '',
|
|
66
|
+
'left-mid': '',
|
|
67
|
+
mid: '',
|
|
68
|
+
'mid-mid': '',
|
|
69
|
+
right: '',
|
|
70
|
+
'right-mid': '',
|
|
71
|
+
middle: ' ',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function renderTable(headers, rows, maxWidths, detailLabel, formatRow) {
|
|
76
|
+
const mainHeaders = headers.slice(0, -1);
|
|
77
|
+
const mainRows = rows.map((row) => row.slice(0, -1));
|
|
78
|
+
const colWidths = buildColumnWidths(mainHeaders, mainRows, maxWidths);
|
|
79
|
+
const headerTable = createTable(mainHeaders, colWidths);
|
|
80
|
+
const divider = '-'.repeat(Math.min(getTerminalWidth(), colWidths.reduce((sum, width) => sum + width, 0) +
|
|
81
|
+
2 * (colWidths.length - 1)));
|
|
82
|
+
console.log(headerTable.toString());
|
|
83
|
+
console.log(divider);
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const rowTable = createTable(undefined, colWidths);
|
|
86
|
+
const formattedRow = formatRow ? formatRow(row, colWidths) : row;
|
|
87
|
+
const detail = row[row.length - 1] ?? '';
|
|
88
|
+
rowTable.push(formattedRow.slice(0, -1));
|
|
89
|
+
console.log(rowTable.toString());
|
|
90
|
+
console.log(` ${detailLabel}: ${detail}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(divider);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function formatProjectRow(row, widths) {
|
|
96
|
+
return [
|
|
97
|
+
truncate(row[0] ?? '', widths[0] ?? 20),
|
|
98
|
+
truncate(row[1] ?? '', widths[1] ?? 16),
|
|
99
|
+
row[2] ?? 'no',
|
|
100
|
+
row[3] ?? '0',
|
|
101
|
+
row[4] ?? '',
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
function formatWorktreeRow(row, widths) {
|
|
105
|
+
return [
|
|
106
|
+
truncate(row[0] ?? '', widths[0] ?? 18),
|
|
107
|
+
truncate(row[1] ?? '', widths[1] ?? 18),
|
|
108
|
+
truncate(row[2] ?? '', widths[2] ?? 18),
|
|
109
|
+
row[3] ?? 'no',
|
|
110
|
+
row[4] ?? 'no',
|
|
111
|
+
row[5] ?? '',
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function formatStatusRow(row, widths) {
|
|
115
|
+
return [
|
|
116
|
+
truncate(row[0] ?? '', widths[0] ?? 18),
|
|
117
|
+
truncate(row[1] ?? '', widths[1] ?? 18),
|
|
118
|
+
truncate(row[2] ?? '', widths[2] ?? 18),
|
|
119
|
+
row[3] ?? '-',
|
|
120
|
+
row[4] ?? '0',
|
|
121
|
+
row[5] ?? '-',
|
|
122
|
+
row[6] ?? '',
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function printProjects(projects, asJson) {
|
|
126
|
+
if (asJson) {
|
|
127
|
+
printJson(projects);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (projects.length === 0) {
|
|
131
|
+
console.log('No projects found.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
renderTable(['project', 'branch', 'commits', 'worktrees', 'path'], projects.map((project) => [
|
|
135
|
+
project.name,
|
|
136
|
+
project.defaultBranch ?? '-',
|
|
137
|
+
project.hasCommits ? 'yes' : 'no',
|
|
138
|
+
String(project.worktreeCount),
|
|
139
|
+
project.rootPath,
|
|
140
|
+
]), [20, 16, 7, 10], 'path', formatProjectRow);
|
|
141
|
+
}
|
|
142
|
+
function printProjectDiagnostics(diagnostics, asJson) {
|
|
143
|
+
if (asJson) {
|
|
144
|
+
printJson(diagnostics);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (diagnostics.length === 0) {
|
|
148
|
+
console.log('No projects found.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
renderTable(['project', 'status', 'branch', 'worktrees', 'details'], diagnostics.map((entry) => {
|
|
152
|
+
if ('gitDir' in entry) {
|
|
153
|
+
return [
|
|
154
|
+
entry.name,
|
|
155
|
+
'ok',
|
|
156
|
+
entry.defaultBranch ?? '-',
|
|
157
|
+
String(entry.worktreeCount),
|
|
158
|
+
entry.rootPath,
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
return [entry.name, 'broken', '-', '-', entry.error ?? entry.rootPath];
|
|
162
|
+
}), [20, 8, 16, 10], 'details', formatProjectRow);
|
|
163
|
+
}
|
|
164
|
+
function printProjectInit(init, asJson) {
|
|
165
|
+
if (asJson) {
|
|
166
|
+
printJson(init);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (init.initCommand) {
|
|
170
|
+
console.log(init.initCommand);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(`No init command configured for project '${init.project}'.`);
|
|
174
|
+
}
|
|
175
|
+
function printWorktrees(worktrees, asJson) {
|
|
176
|
+
if (asJson) {
|
|
177
|
+
printJson(worktrees);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (worktrees.length === 0) {
|
|
181
|
+
console.log('No linked worktrees found.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
renderTable(['project', 'alias', 'branch', 'locked', 'prunable', 'path'], worktrees.map((worktree) => [
|
|
185
|
+
worktree.project,
|
|
186
|
+
worktree.alias,
|
|
187
|
+
worktree.branch ?? 'detached',
|
|
188
|
+
worktree.locked ? 'yes' : 'no',
|
|
189
|
+
worktree.prunable ? 'yes' : 'no',
|
|
190
|
+
worktree.path,
|
|
191
|
+
]), [18, 18, 18, 6, 8], 'path', formatWorktreeRow);
|
|
192
|
+
}
|
|
193
|
+
function printStatuses(statuses, asJson) {
|
|
194
|
+
if (asJson) {
|
|
195
|
+
printJson(statuses);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (statuses.length === 0) {
|
|
199
|
+
console.log('No linked worktrees found.');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
renderTable(['project', 'alias', 'branch', 'state', 'changes', 'head', 'path'], statuses.map((status) => [
|
|
203
|
+
status.project,
|
|
204
|
+
status.alias,
|
|
205
|
+
status.branch ?? 'detached',
|
|
206
|
+
status.state,
|
|
207
|
+
String(status.changes),
|
|
208
|
+
status.head?.slice(0, 8) ?? '-',
|
|
209
|
+
status.path,
|
|
210
|
+
]), [18, 18, 18, 7, 7, 8], 'path', formatStatusRow);
|
|
211
|
+
}
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runShellCommand = runShellCommand;
|
|
4
|
+
exports.runCommand = runCommand;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
function runShellCommand(command, options = {}) {
|
|
8
|
+
const shell = process.env.SHELL ?? 'sh';
|
|
9
|
+
return runCommand(shell, ['-lc', command], options);
|
|
10
|
+
}
|
|
11
|
+
function runCommand(command, args, options = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
...options.env,
|
|
18
|
+
},
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
});
|
|
21
|
+
let stdout = '';
|
|
22
|
+
let stderr = '';
|
|
23
|
+
child.stdout.on('data', (chunk) => {
|
|
24
|
+
stdout += chunk.toString();
|
|
25
|
+
});
|
|
26
|
+
child.stderr.on('data', (chunk) => {
|
|
27
|
+
stderr += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
child.on('error', (error) => {
|
|
30
|
+
reject(new errors_1.CliError(`Failed to run ${command}: ${error.message}`));
|
|
31
|
+
});
|
|
32
|
+
child.on('close', (code) => {
|
|
33
|
+
if (code === 0) {
|
|
34
|
+
resolve(stdout.trimEnd());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const message = stderr.trim() ||
|
|
38
|
+
`${command} ${args.join(' ')} failed with exit code ${code}`;
|
|
39
|
+
reject(new errors_1.CliError(message));
|
|
40
|
+
});
|
|
41
|
+
if (options.stdin) {
|
|
42
|
+
child.stdin.write(options.stdin);
|
|
43
|
+
}
|
|
44
|
+
child.stdin.end();
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
}
|