@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/git.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
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.checkGitAvailable = checkGitAvailable;
|
|
7
|
+
exports.runGit = runGit;
|
|
8
|
+
exports.initBareRepo = initBareRepo;
|
|
9
|
+
exports.cloneBareRepo = cloneBareRepo;
|
|
10
|
+
exports.getRepoTopLevel = getRepoTopLevel;
|
|
11
|
+
exports.isBareRepository = isBareRepository;
|
|
12
|
+
exports.bootstrapBareRepo = bootstrapBareRepo;
|
|
13
|
+
exports.repoHasCommits = repoHasCommits;
|
|
14
|
+
exports.getDefaultBranch = getDefaultBranch;
|
|
15
|
+
exports.localBranchExists = localBranchExists;
|
|
16
|
+
exports.parseWorktreeList = parseWorktreeList;
|
|
17
|
+
exports.listWorktrees = listWorktrees;
|
|
18
|
+
exports.listLinkedWorktrees = listLinkedWorktrees;
|
|
19
|
+
exports.addWorktree = addWorktree;
|
|
20
|
+
exports.removeWorktree = removeWorktree;
|
|
21
|
+
exports.repairWorktrees = repairWorktrees;
|
|
22
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
23
|
+
exports.getHeadShortSha = getHeadShortSha;
|
|
24
|
+
exports.getWorktreeStatus = getWorktreeStatus;
|
|
25
|
+
const promises_1 = require("node:fs/promises");
|
|
26
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
const errors_1 = require("./errors");
|
|
29
|
+
const process_1 = require("./process");
|
|
30
|
+
const MINIMUM_GIT_VERSION = '2.17.0';
|
|
31
|
+
function compareVersions(a, b) {
|
|
32
|
+
const aParts = a.split('.').map(Number);
|
|
33
|
+
const bParts = b.split('.').map(Number);
|
|
34
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
35
|
+
const aVal = aParts[i] ?? 0;
|
|
36
|
+
const bVal = bParts[i] ?? 0;
|
|
37
|
+
if (aVal > bVal)
|
|
38
|
+
return 1;
|
|
39
|
+
if (aVal < bVal)
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
async function checkGitAvailable() {
|
|
45
|
+
try {
|
|
46
|
+
const versionOutput = await runGit(['--version']);
|
|
47
|
+
const match = versionOutput.match(/git version (\d+\.\d+\.\d+)/);
|
|
48
|
+
if (!match) {
|
|
49
|
+
throw new errors_1.CliError('Unable to determine git version. Please ensure git is installed.');
|
|
50
|
+
}
|
|
51
|
+
const version = match[1];
|
|
52
|
+
if (compareVersions(version, MINIMUM_GIT_VERSION) < 0) {
|
|
53
|
+
throw new errors_1.CliError(`Git version ${MINIMUM_GIT_VERSION} or higher is required. Found version ${version}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof errors_1.CliError) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
throw new errors_1.CliError('Git is not installed or not available in PATH. Please install git to use gittyup.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function runGit(args, options = {}) {
|
|
64
|
+
return (0, process_1.runCommand)('git', args, options);
|
|
65
|
+
}
|
|
66
|
+
async function initBareRepo(gitDir) {
|
|
67
|
+
await runGit(['init', '--bare', '--initial-branch=main', gitDir]);
|
|
68
|
+
}
|
|
69
|
+
async function cloneBareRepo(remote, gitDir) {
|
|
70
|
+
await runGit(['clone', '--bare', remote, gitDir]);
|
|
71
|
+
}
|
|
72
|
+
async function getRepoTopLevel(repoPath) {
|
|
73
|
+
return runGit(['-C', repoPath, 'rev-parse', '--show-toplevel']);
|
|
74
|
+
}
|
|
75
|
+
async function isBareRepository(repoPath) {
|
|
76
|
+
const output = await runGit([
|
|
77
|
+
'-C',
|
|
78
|
+
repoPath,
|
|
79
|
+
'rev-parse',
|
|
80
|
+
'--is-bare-repository',
|
|
81
|
+
]);
|
|
82
|
+
return output === 'true';
|
|
83
|
+
}
|
|
84
|
+
async function bootstrapBareRepo(gitDir, branch) {
|
|
85
|
+
const tempDir = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), 'gittyup-bootstrap-'));
|
|
86
|
+
try {
|
|
87
|
+
await runGit(['init', '--initial-branch', branch, tempDir]);
|
|
88
|
+
await runGit(['-C', tempDir, 'remote', 'add', 'origin', gitDir]);
|
|
89
|
+
await runGit([
|
|
90
|
+
'-C',
|
|
91
|
+
tempDir,
|
|
92
|
+
'-c',
|
|
93
|
+
'user.name=gittyup',
|
|
94
|
+
'-c',
|
|
95
|
+
'user.email=gittyup@local',
|
|
96
|
+
'commit',
|
|
97
|
+
'--allow-empty',
|
|
98
|
+
'-m',
|
|
99
|
+
'Initial commit',
|
|
100
|
+
]);
|
|
101
|
+
await runGit(['-C', tempDir, 'push', 'origin', `${branch}:${branch}`]);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function repoHasCommits(gitDir) {
|
|
108
|
+
try {
|
|
109
|
+
await runGit([
|
|
110
|
+
'--git-dir',
|
|
111
|
+
gitDir,
|
|
112
|
+
'rev-parse',
|
|
113
|
+
'--verify',
|
|
114
|
+
'HEAD^{commit}',
|
|
115
|
+
]);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function getDefaultBranch(gitDir) {
|
|
123
|
+
try {
|
|
124
|
+
return await runGit([
|
|
125
|
+
'--git-dir',
|
|
126
|
+
gitDir,
|
|
127
|
+
'symbolic-ref',
|
|
128
|
+
'--quiet',
|
|
129
|
+
'--short',
|
|
130
|
+
'HEAD',
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function localBranchExists(gitDir, branch) {
|
|
138
|
+
try {
|
|
139
|
+
await runGit([
|
|
140
|
+
'--git-dir',
|
|
141
|
+
gitDir,
|
|
142
|
+
'show-ref',
|
|
143
|
+
'--verify',
|
|
144
|
+
'--quiet',
|
|
145
|
+
`refs/heads/${branch}`,
|
|
146
|
+
]);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseWorktreeList(output) {
|
|
154
|
+
const normalized = output.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
155
|
+
const trimmed = normalized.trim();
|
|
156
|
+
if (!trimmed) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return trimmed.split(/\n\s*\n/).map((block) => {
|
|
160
|
+
let worktreePath = '';
|
|
161
|
+
let head = null;
|
|
162
|
+
let branch = null;
|
|
163
|
+
let bare = false;
|
|
164
|
+
let locked = false;
|
|
165
|
+
let prunable = false;
|
|
166
|
+
let detached = false;
|
|
167
|
+
for (const line of block.split('\n')) {
|
|
168
|
+
if (line.startsWith('worktree ')) {
|
|
169
|
+
worktreePath = line.slice('worktree '.length);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (line.startsWith('HEAD ')) {
|
|
173
|
+
head = line.slice('HEAD '.length);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (line.startsWith('branch ')) {
|
|
177
|
+
branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (line === 'bare') {
|
|
181
|
+
bare = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (line === 'detached') {
|
|
185
|
+
detached = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (line.startsWith('locked')) {
|
|
189
|
+
locked = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (line.startsWith('prunable')) {
|
|
193
|
+
prunable = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
path: worktreePath,
|
|
198
|
+
alias: node_path_1.default.basename(worktreePath),
|
|
199
|
+
branch,
|
|
200
|
+
head,
|
|
201
|
+
bare,
|
|
202
|
+
locked,
|
|
203
|
+
prunable,
|
|
204
|
+
detached,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function listWorktrees(gitDir) {
|
|
209
|
+
const output = await runGit([
|
|
210
|
+
'--git-dir',
|
|
211
|
+
gitDir,
|
|
212
|
+
'worktree',
|
|
213
|
+
'list',
|
|
214
|
+
'--porcelain',
|
|
215
|
+
]);
|
|
216
|
+
return parseWorktreeList(output);
|
|
217
|
+
}
|
|
218
|
+
async function listLinkedWorktrees(gitDir) {
|
|
219
|
+
const worktrees = await listWorktrees(gitDir);
|
|
220
|
+
return worktrees.filter((entry) => !entry.bare);
|
|
221
|
+
}
|
|
222
|
+
async function addWorktree(gitDir, worktreePath, branch, startPoint) {
|
|
223
|
+
if (!(await repoHasCommits(gitDir))) {
|
|
224
|
+
throw new errors_1.CliError('Repository has no commits yet. Git cannot create a linked worktree for an empty bare repository.');
|
|
225
|
+
}
|
|
226
|
+
const args = ['--git-dir', gitDir, 'worktree', 'add'];
|
|
227
|
+
if (await localBranchExists(gitDir, branch)) {
|
|
228
|
+
args.push(worktreePath, branch);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const resolvedStartPoint = startPoint ?? (await getDefaultBranch(gitDir));
|
|
232
|
+
if (!resolvedStartPoint) {
|
|
233
|
+
throw new errors_1.CliError(`Could not determine a start point for branch '${branch}'.`);
|
|
234
|
+
}
|
|
235
|
+
args.push('-b', branch, worktreePath, resolvedStartPoint);
|
|
236
|
+
}
|
|
237
|
+
await runGit(args);
|
|
238
|
+
}
|
|
239
|
+
async function removeWorktree(gitDir, worktreePath, force) {
|
|
240
|
+
const args = ['--git-dir', gitDir, 'worktree', 'remove'];
|
|
241
|
+
if (force) {
|
|
242
|
+
args.push('--force');
|
|
243
|
+
}
|
|
244
|
+
args.push(worktreePath);
|
|
245
|
+
await runGit(args);
|
|
246
|
+
}
|
|
247
|
+
async function repairWorktrees(gitDir, worktreePaths) {
|
|
248
|
+
const args = ['--git-dir', gitDir, 'worktree', 'repair', ...worktreePaths];
|
|
249
|
+
await runGit(args);
|
|
250
|
+
}
|
|
251
|
+
async function getCurrentBranch(worktreePath) {
|
|
252
|
+
try {
|
|
253
|
+
return await runGit([
|
|
254
|
+
'-C',
|
|
255
|
+
worktreePath,
|
|
256
|
+
'symbolic-ref',
|
|
257
|
+
'--quiet',
|
|
258
|
+
'--short',
|
|
259
|
+
'HEAD',
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function getHeadShortSha(worktreePath) {
|
|
267
|
+
return runGit(['-C', worktreePath, 'rev-parse', '--short', 'HEAD']);
|
|
268
|
+
}
|
|
269
|
+
async function getWorktreeStatus(worktree) {
|
|
270
|
+
const statusOutput = await runGit([
|
|
271
|
+
'-C',
|
|
272
|
+
worktree.path,
|
|
273
|
+
'status',
|
|
274
|
+
'--porcelain',
|
|
275
|
+
]);
|
|
276
|
+
const changes = statusOutput
|
|
277
|
+
? statusOutput.split('\n').filter(Boolean).length
|
|
278
|
+
: 0;
|
|
279
|
+
let branch = worktree.branch;
|
|
280
|
+
let detached = worktree.detached;
|
|
281
|
+
if (!branch) {
|
|
282
|
+
branch = await getCurrentBranch(worktree.path);
|
|
283
|
+
detached = branch === null;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...worktree,
|
|
287
|
+
branch,
|
|
288
|
+
detached,
|
|
289
|
+
head: worktree.head ?? (await getHeadShortSha(worktree.path)),
|
|
290
|
+
state: changes === 0 ? 'clean' : 'dirty',
|
|
291
|
+
changes,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveWorktreeAddInput = resolveWorktreeAddInput;
|
|
37
|
+
exports.resolveWorktreeRemoveInput = resolveWorktreeRemoveInput;
|
|
38
|
+
exports.resolveTmuxLaunchInput = resolveTmuxLaunchInput;
|
|
39
|
+
const readline = __importStar(require("node:readline/promises"));
|
|
40
|
+
const config_1 = require("./config");
|
|
41
|
+
const errors_1 = require("./errors");
|
|
42
|
+
const process_1 = require("./process");
|
|
43
|
+
const projects_1 = require("./projects");
|
|
44
|
+
const tmux_1 = require("./tmux");
|
|
45
|
+
function isInteractiveTerminal() {
|
|
46
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
47
|
+
}
|
|
48
|
+
async function promptText(message) {
|
|
49
|
+
const terminal = readline.createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
output: process.stdout,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
return (await terminal.question(message)).trim();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
terminal.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function selectFromList(prompt, values) {
|
|
61
|
+
try {
|
|
62
|
+
const selection = await (0, process_1.runCommand)('fzf', ['--prompt', prompt], {
|
|
63
|
+
stdin: `${values.join('\n')}\n`,
|
|
64
|
+
});
|
|
65
|
+
return selection.trim();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof errors_1.CliError ? error.message : '';
|
|
69
|
+
if (message.includes('exit code 130')) {
|
|
70
|
+
throw new errors_1.CliError('No project selected.');
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const defaultServices = {
|
|
76
|
+
checkFzfAvailable: tmux_1.checkFzfAvailable,
|
|
77
|
+
discoverProjects: projects_1.discoverProjects,
|
|
78
|
+
listProjectWorktrees: projects_1.listProjectWorktrees,
|
|
79
|
+
isInteractive: isInteractiveTerminal,
|
|
80
|
+
promptText,
|
|
81
|
+
selectFromList,
|
|
82
|
+
};
|
|
83
|
+
async function selectProject(rootDir, services) {
|
|
84
|
+
await services.checkFzfAvailable();
|
|
85
|
+
const projects = await services.discoverProjects(rootDir);
|
|
86
|
+
if (projects.length === 0) {
|
|
87
|
+
throw new errors_1.CliError(`No projects found under ${rootDir}.`);
|
|
88
|
+
}
|
|
89
|
+
return services.selectFromList('project> ', projects.map((project) => project.name));
|
|
90
|
+
}
|
|
91
|
+
async function selectWorktree(options, services) {
|
|
92
|
+
await services.checkFzfAvailable();
|
|
93
|
+
const worktrees = options.projectName
|
|
94
|
+
? await services.listProjectWorktrees(options.rootDir, options.projectName)
|
|
95
|
+
: (await Promise.all((await services.discoverProjects(options.rootDir)).map(async (project) => (await services.listProjectWorktrees(options.rootDir, project.name)).map((worktree) => ({
|
|
96
|
+
...worktree,
|
|
97
|
+
project: project.name,
|
|
98
|
+
}))))).flat();
|
|
99
|
+
const filteredWorktrees = options.includeWorktree
|
|
100
|
+
? worktrees.filter(options.includeWorktree)
|
|
101
|
+
: worktrees;
|
|
102
|
+
if (filteredWorktrees.length === 0) {
|
|
103
|
+
throw new errors_1.CliError(options.emptyMessage);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const selection = await services.selectFromList('worktree> ', filteredWorktrees.map((worktree) => buildWorktreeLabel(worktree, options.projectName)));
|
|
107
|
+
const selected = filteredWorktrees.find((worktree) => buildWorktreeLabel(worktree, options.projectName) === selection);
|
|
108
|
+
if (!selected) {
|
|
109
|
+
throw new errors_1.CliError('Selected worktree was not found.');
|
|
110
|
+
}
|
|
111
|
+
const projectName = selected.project ?? options.projectName;
|
|
112
|
+
if (!projectName) {
|
|
113
|
+
throw new errors_1.CliError('Selected worktree did not include a project.');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
projectName,
|
|
117
|
+
alias: selected.alias,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const message = error instanceof errors_1.CliError ? error.message : '';
|
|
122
|
+
if (message === 'No project selected.') {
|
|
123
|
+
throw new errors_1.CliError('No worktree selected.');
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function buildWorktreeLabel(worktree, fallbackProject) {
|
|
129
|
+
const project = worktree.project ?? fallbackProject;
|
|
130
|
+
if (!project) {
|
|
131
|
+
return worktree.alias;
|
|
132
|
+
}
|
|
133
|
+
return `${project}:${worktree.alias}`;
|
|
134
|
+
}
|
|
135
|
+
async function resolveWorktreeAddInput(options, services = defaultServices) {
|
|
136
|
+
let { projectName, branch } = options;
|
|
137
|
+
if (projectName && branch) {
|
|
138
|
+
return {
|
|
139
|
+
projectName,
|
|
140
|
+
branch: (0, config_1.validateBranchName)(branch),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!services.isInteractive()) {
|
|
144
|
+
throw new errors_1.CliError('worktree add requires <project> and <branch>, or an interactive terminal when omitted.');
|
|
145
|
+
}
|
|
146
|
+
if (!projectName) {
|
|
147
|
+
projectName = await selectProject(options.rootDir, services);
|
|
148
|
+
}
|
|
149
|
+
if (!branch) {
|
|
150
|
+
branch = await services.promptText('Branch name: ');
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
projectName,
|
|
154
|
+
branch: (0, config_1.validateBranchName)(branch),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function resolveWorktreeRemoveInput(options, services = defaultServices) {
|
|
158
|
+
let { projectName, alias } = options;
|
|
159
|
+
if (projectName && alias) {
|
|
160
|
+
return { projectName, alias };
|
|
161
|
+
}
|
|
162
|
+
if (!services.isInteractive()) {
|
|
163
|
+
throw new errors_1.CliError('worktree remove requires <project> and <alias>, or an interactive terminal when omitted.');
|
|
164
|
+
}
|
|
165
|
+
if (!alias) {
|
|
166
|
+
const selection = await selectWorktree({
|
|
167
|
+
rootDir: options.rootDir,
|
|
168
|
+
projectName,
|
|
169
|
+
emptyMessage: projectName
|
|
170
|
+
? `No linked worktrees found in project '${projectName}'.`
|
|
171
|
+
: `No linked worktrees found under ${options.rootDir}.`,
|
|
172
|
+
includeWorktree: (worktree) => worktree.alias !== config_1.MAIN_WORKTREE_ALIAS,
|
|
173
|
+
}, services);
|
|
174
|
+
projectName = selection.projectName;
|
|
175
|
+
alias = selection.alias;
|
|
176
|
+
}
|
|
177
|
+
if (!projectName) {
|
|
178
|
+
throw new errors_1.CliError('Selected worktree did not include a project.');
|
|
179
|
+
}
|
|
180
|
+
return { projectName, alias };
|
|
181
|
+
}
|
|
182
|
+
async function resolveTmuxLaunchInput(options, services = defaultServices) {
|
|
183
|
+
const { projectName, alias } = options;
|
|
184
|
+
if (projectName && alias) {
|
|
185
|
+
return { projectName, alias };
|
|
186
|
+
}
|
|
187
|
+
if (!services.isInteractive()) {
|
|
188
|
+
throw new errors_1.CliError('tmux launch requires <project> and [worktree], or an interactive terminal when omitted.');
|
|
189
|
+
}
|
|
190
|
+
if (!projectName) {
|
|
191
|
+
const selection = await selectWorktree({
|
|
192
|
+
rootDir: options.rootDir,
|
|
193
|
+
emptyMessage: `No linked worktrees found under ${options.rootDir}.`,
|
|
194
|
+
}, services);
|
|
195
|
+
return selection;
|
|
196
|
+
}
|
|
197
|
+
if (!alias) {
|
|
198
|
+
const selection = await selectWorktree({
|
|
199
|
+
rootDir: options.rootDir,
|
|
200
|
+
projectName,
|
|
201
|
+
emptyMessage: `No linked worktrees found in project '${projectName}'.`,
|
|
202
|
+
}, services);
|
|
203
|
+
return selection;
|
|
204
|
+
}
|
|
205
|
+
return { projectName, alias };
|
|
206
|
+
}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getProjectInitRecord = getProjectInitRecord;
|
|
4
|
+
exports.setProjectInitCommand = setProjectInitCommand;
|
|
5
|
+
exports.clearProjectInitCommand = clearProjectInitCommand;
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
function normalizeInitCommand(value) {
|
|
10
|
+
if (value === undefined || value === null) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
throw new errors_1.CliError('Project init command must be a string when configured.');
|
|
15
|
+
}
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (trimmed.length === 0) {
|
|
18
|
+
throw new errors_1.CliError('Project init command cannot be empty.');
|
|
19
|
+
}
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
async function pathExists(targetPath) {
|
|
23
|
+
try {
|
|
24
|
+
await (0, promises_1.access)(targetPath);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function readProjectMetadataFile(rootDir, projectName) {
|
|
32
|
+
const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
|
|
33
|
+
if (!(await pathExists(project.metadataPath))) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(await (0, promises_1.readFile)(project.metadataPath, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new errors_1.CliError(`Project metadata at ${project.metadataPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
+
}
|
|
43
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
44
|
+
throw new errors_1.CliError(`Project metadata at ${project.metadataPath} must be a JSON object.`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
async function getProjectInitRecord(rootDir, projectName) {
|
|
49
|
+
const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
|
|
50
|
+
const metadata = await readProjectMetadataFile(rootDir, projectName);
|
|
51
|
+
return {
|
|
52
|
+
project: project.name,
|
|
53
|
+
initCommand: normalizeInitCommand(metadata.initCommand),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function writeProjectMetadataFile(rootDir, projectName, metadata) {
|
|
57
|
+
const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
|
|
58
|
+
const tempPath = `${project.metadataPath}.tmp`;
|
|
59
|
+
if (Object.keys(metadata).length === 0) {
|
|
60
|
+
await (0, promises_1.rm)(project.metadataPath, { force: true });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await (0, promises_1.writeFile)(tempPath, `${JSON.stringify(metadata, null, 2)}
|
|
64
|
+
`, 'utf8');
|
|
65
|
+
await (0, promises_1.rename)(tempPath, project.metadataPath);
|
|
66
|
+
}
|
|
67
|
+
async function setProjectInitCommand(rootDir, projectName, command) {
|
|
68
|
+
const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
|
|
69
|
+
const initCommand = normalizeInitCommand(command);
|
|
70
|
+
await writeProjectMetadataFile(rootDir, project.name, { initCommand });
|
|
71
|
+
return {
|
|
72
|
+
project: project.name,
|
|
73
|
+
initCommand,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function clearProjectInitCommand(rootDir, projectName) {
|
|
77
|
+
const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
|
|
78
|
+
await writeProjectMetadataFile(rootDir, project.name, {});
|
|
79
|
+
return {
|
|
80
|
+
project: project.name,
|
|
81
|
+
initCommand: null,
|
|
82
|
+
};
|
|
83
|
+
}
|