@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/README.md CHANGED
@@ -223,11 +223,12 @@ Both `pnpm dev project list` and `pnpm dev -- project list` are supported.
223
223
  Updating `package.json` to a new version and merging that change to `main`
224
224
  automatically runs the release workflow.
225
225
 
226
- - CI runs lint, tests, and build
226
+ - CI runs first on the `main` push
227
+ - Release only runs after that CI run succeeds
227
228
  - The workflow creates and pushes the matching `vX.Y.Z` tag
228
229
  - `pnpm pack` creates an installable tarball
229
230
  - GitHub Releases gets the tarball and `SHA256SUMS.txt` attached automatically
230
- - The same workflow publishes `@mbluemer_2/gittyup` to npm
231
+ - The same workflow publishes `@mbluemer_2/gittyup` to npm using the `NPM_TOKEN` repo secret
231
232
 
232
233
  You can still create a release manually by pushing a tag like `v0.1.0` yourself.
233
234
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbluemer_2/gittyup",
3
- "version": "0.1.2",
3
+ "version": "0.1.8",
4
4
  "description": "Filesystem-driven CLI for managing bare git repositories and worktrees",
5
5
  "main": "dist/cli.js",
6
6
  "files": [
package/dist/aliases.js DELETED
@@ -1,32 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeArgv = normalizeArgv;
4
- const COMMAND_ALIASES = {
5
- p: ['project'],
6
- pl: ['project', 'list'],
7
- pd: ['project', 'doctor'],
8
- pg: ['project', 'init', 'get'],
9
- ps: ['project', 'init', 'set'],
10
- px: ['project', 'init', 'clear'],
11
- pc: ['project', 'create'],
12
- pcl: ['project', 'clone'],
13
- pi: ['project', 'import'],
14
- pr: ['project', 'rename'],
15
- w: ['worktree'],
16
- wl: ['worktree', 'list'],
17
- wa: ['worktree', 'add'],
18
- wr: ['worktree', 'remove'],
19
- s: ['status'],
20
- st: ['status'],
21
- t: ['tmux'],
22
- tl: ['tmux', 'launch'],
23
- ts: ['tmux', 'switch'],
24
- };
25
- function normalizeArgv(argv) {
26
- const normalized = argv[2] === '--' ? [argv[0], argv[1], ...argv.slice(3)] : [...argv];
27
- const alias = normalized[2] ? COMMAND_ALIASES[normalized[2]] : undefined;
28
- if (!alias) {
29
- return normalized;
30
- }
31
- return [normalized[0], normalized[1], ...alias, ...normalized.slice(3)];
32
- }
package/dist/cli.js DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const program_1 = require("./program");
5
- void (0, program_1.runCli)();
package/dist/config.js DELETED
@@ -1,127 +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.PROJECT_METADATA_FILENAME = exports.MAIN_WORKTREE_ALIAS = exports.BARE_REPO_DIRNAME = exports.DEFAULT_ROOT_DIR = void 0;
7
- exports.expandHome = expandHome;
8
- exports.resolveRootDir = resolveRootDir;
9
- exports.validateProjectName = validateProjectName;
10
- exports.validateAlias = validateAlias;
11
- exports.deriveAlias = deriveAlias;
12
- exports.inferProjectName = inferProjectName;
13
- exports.validateBranchName = validateBranchName;
14
- exports.validateRemoteUrl = validateRemoteUrl;
15
- exports.buildProjectPaths = buildProjectPaths;
16
- const node_os_1 = __importDefault(require("node:os"));
17
- const node_path_1 = __importDefault(require("node:path"));
18
- const errors_1 = require("./errors");
19
- exports.DEFAULT_ROOT_DIR = node_path_1.default.join(node_os_1.default.homedir(), 'src');
20
- exports.BARE_REPO_DIRNAME = 'repo.git';
21
- exports.MAIN_WORKTREE_ALIAS = 'main';
22
- exports.PROJECT_METADATA_FILENAME = '.gittyup.json';
23
- function expandHome(value) {
24
- if (value === '~') {
25
- return node_os_1.default.homedir();
26
- }
27
- if (value.startsWith('~/')) {
28
- return node_path_1.default.join(node_os_1.default.homedir(), value.slice(2));
29
- }
30
- if (value.startsWith('$HOME/')) {
31
- return node_path_1.default.join(node_os_1.default.homedir(), value.slice(6));
32
- }
33
- return value;
34
- }
35
- function resolveRootDir(input) {
36
- return node_path_1.default.resolve(expandHome(input ?? process.env.GITTYUP_ROOT ?? exports.DEFAULT_ROOT_DIR));
37
- }
38
- function validateProjectName(name) {
39
- if (name.trim().length === 0) {
40
- throw new errors_1.CliError('Project name cannot be empty.');
41
- }
42
- if (name !== name.trim()) {
43
- throw new errors_1.CliError('Project name cannot start or end with whitespace.');
44
- }
45
- if (/[/\\]/.test(name)) {
46
- throw new errors_1.CliError('Project name must be a single directory name.');
47
- }
48
- if (name === '.' || name === '..') {
49
- throw new errors_1.CliError("Project name must not be '.' or '..'.");
50
- }
51
- return name;
52
- }
53
- function validateAlias(alias) {
54
- if (alias.trim().length === 0) {
55
- throw new errors_1.CliError('Worktree alias cannot be empty.');
56
- }
57
- if (alias !== alias.trim()) {
58
- throw new errors_1.CliError('Worktree alias cannot start or end with whitespace.');
59
- }
60
- if (/[/\\]/.test(alias)) {
61
- throw new errors_1.CliError('Worktree alias must be a single directory name.');
62
- }
63
- if (alias === '.' || alias === '..') {
64
- throw new errors_1.CliError("Worktree alias must not be '.' or '..'.");
65
- }
66
- return alias;
67
- }
68
- function deriveAlias(branch) {
69
- const alias = branch
70
- .replace(/[/\\]+/g, '-')
71
- .replace(/[^A-Za-z0-9._-]+/g, '-')
72
- .replace(/-+/g, '-')
73
- .replace(/^-|-$/g, '');
74
- return validateAlias(alias || 'worktree');
75
- }
76
- function inferProjectName(remote) {
77
- const trimmed = remote.trim().replace(/\/$/, '');
78
- const scpTail = trimmed.includes(':') && !trimmed.includes('://')
79
- ? trimmed.slice(trimmed.lastIndexOf(':') + 1)
80
- : trimmed;
81
- const tail = scpTail.split('/').pop() ?? scpTail;
82
- const name = tail.endsWith('.git') ? tail.slice(0, -4) : tail;
83
- return validateProjectName(name);
84
- }
85
- // eslint-disable-next-line no-control-regex
86
- const INVALID_BRANCH_CHARS = /[\x00-\x1f\x7f~^:?*[\\]/;
87
- const INVALID_BRANCH_START = /^[./]/;
88
- const INVALID_BRANCH_END = /[./]$/;
89
- const INVALID_BRANCH_SEQUENCE = /(\.\.)|(\/\/)|(@\{)/;
90
- function validateBranchName(branch) {
91
- const trimmed = branch.trim();
92
- if (trimmed.length === 0) {
93
- throw new errors_1.CliError('Branch name cannot be empty.');
94
- }
95
- if (INVALID_BRANCH_CHARS.test(trimmed)) {
96
- throw new errors_1.CliError('Branch name contains invalid characters (control chars, ~, ^, :, ?, *, [, \\).');
97
- }
98
- if (INVALID_BRANCH_START.test(trimmed)) {
99
- throw new errors_1.CliError('Branch name cannot start with . or /.');
100
- }
101
- if (INVALID_BRANCH_END.test(trimmed)) {
102
- throw new errors_1.CliError('Branch name cannot end with . or /.');
103
- }
104
- if (INVALID_BRANCH_SEQUENCE.test(trimmed)) {
105
- throw new errors_1.CliError('Branch name cannot contain "..", "//", or "@{".');
106
- }
107
- return trimmed;
108
- }
109
- function validateRemoteUrl(remote) {
110
- const trimmed = remote.trim();
111
- if (trimmed.length === 0) {
112
- throw new errors_1.CliError('Remote URL cannot be empty.');
113
- }
114
- return trimmed;
115
- }
116
- function buildProjectPaths(rootDir, projectName) {
117
- const name = validateProjectName(projectName);
118
- const rootPath = node_path_1.default.join(rootDir, name);
119
- const gitDir = node_path_1.default.join(rootPath, exports.BARE_REPO_DIRNAME);
120
- const metadataPath = node_path_1.default.join(rootPath, exports.PROJECT_METADATA_FILENAME);
121
- return {
122
- name,
123
- rootPath,
124
- gitDir,
125
- metadataPath,
126
- };
127
- }
package/dist/errors.js DELETED
@@ -1,19 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CliError = void 0;
4
- exports.toErrorMessage = toErrorMessage;
5
- class CliError extends Error {
6
- exitCode;
7
- constructor(message, exitCode = 1) {
8
- super(message);
9
- this.name = 'CliError';
10
- this.exitCode = exitCode;
11
- }
12
- }
13
- exports.CliError = CliError;
14
- function toErrorMessage(error) {
15
- if (error instanceof Error) {
16
- return error.message;
17
- }
18
- return String(error);
19
- }
package/dist/git.js DELETED
@@ -1,293 +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.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
- }