git-workspace-bare 0.1.0

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/.lunterc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "rules": {
3
+ "require-await": "off"
4
+ }
5
+ }
package/.prettierrc ADDED
@@ -0,0 +1 @@
1
+ prettier-config-holepunch
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # git-workspace-bare
2
+
3
+ Manage Git worktrees as development workspaces.
4
+
5
+ The CLI can clone GitHub repositories, create branch worktrees, list registered checkouts, and unregister them.
6
+
7
+ ## Local setup
8
+
9
+ The CLI runs on [Bare](https://github.com/holepunchto/bare). Install Bare using npm:
10
+
11
+ ```sh
12
+ npm install -g bare
13
+ ```
14
+
15
+ Then run from the repository directory:
16
+
17
+ ```sh
18
+ npm install
19
+ npm link
20
+ ```
21
+
22
+ This makes the `git-workspace` command available in your shell. Git also discovers it on your `PATH`, so you can run it as `git workspace`. Bare must be on your `PATH`; Node.js and npm are used for package installation and development tooling.
23
+
24
+ ## Usage
25
+
26
+ ```sh
27
+ git workspace -h
28
+ git workspace --version
29
+ git workspace init nicolasfont/workspace
30
+ git workspace add holepunchto/brittle
31
+ git workspace list
32
+ git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
33
+ git workspace remove holepunchto/brittle
34
+ ```
35
+
36
+ - `--help` (or `-h`) shows the available options.
37
+ - `--version` prints the package version.
38
+
39
+ You can also invoke the executable directly, for example `git-workspace list`. Use `git workspace -h` or `git-workspace --help` for CLI help; Git handles `git workspace --help` as a manual-page request.
40
+
41
+ Running `git workspace` without arguments exits successfully without printing anything.
42
+
43
+ ### Initialize a manifest repository
44
+
45
+ Initialize your manifest repository:
46
+
47
+ ```sh
48
+ git workspace init nicolasfont/workspace
49
+ ```
50
+
51
+ This tries to clone the repository over SSH into `~/workspace/nicolasfont/workspace`. If cloning fails and the destination is absent or empty, it initializes a local Git repository there and sets `origin` to the GitHub SSH URL. The clone failure is reported because a missing repository, authentication failure, and network failure can look similar. Existing nonempty destinations are never overwritten.
52
+
53
+ Creating the repository on GitHub, committing, and pushing remain manual. Initialization saves `~/.config/git-workspace/config.json` with a `manifestRepository` field containing the absolute checkout path. The local repository uses `workspace.json` as the manifest, storing portable repository names rather than machine-specific absolute paths:
54
+
55
+ ```json
56
+ ["holepunchto/brittle", "holepunchto/brittle@feature/jobs"]
57
+ ```
58
+
59
+ On every machine, those names resolve under `~/workspace`.
60
+
61
+ If the manifest repository has no `workspace.json`, initialization writes an empty array. Old local registry files are not read or migrated.
62
+
63
+ If `workspace.json` already exists, it is validated and preserved. Initialization does not clone or remove the repositories listed in the manifest. Repeating `init` with the same manifest repository preserves local edits and does not pull. A different manifest origin is refused.
64
+
65
+ Run `init` before using `add`, `remove`, `list`, or `status`. All commands locate `workspace.json` through that config file; there is no legacy registry fallback or separate activation marker. Updates remain uncommitted changes in the manifest checkout: automatic commits, pushes, and a `git workspace pull` command will come in later commits. For now, the manifest checkout can be committed and pushed with ordinary Git commands.
66
+
67
+ ### Add a repository
68
+
69
+ With Git on your `PATH` and SSH access to GitHub configured:
70
+
71
+ ```sh
72
+ git workspace add holepunchto/brittle
73
+ ```
74
+
75
+ The required `owner/repo` identifies a GitHub repository. If `~/workspace/holepunchto/brittle` does not exist, the command clones `git@github.com:holepunchto/brittle.git` there, then registers the checkout. The default workspace directory is `~/workspace`, regardless of your current directory.
76
+
77
+ If the destination already exists, it must be a Git worktree root whose `origin` points to the requested GitHub repository. SSH and HTTPS origins are accepted. Repeating the command does not pull, switch branches, or add duplicate entries. Existing repositories elsewhere are not moved.
78
+
79
+ Registered names are stored in the initialized manifest. Cloning shows Git's progress; registering an existing checkout is silent. A failed clone or destination validation leaves the registry unchanged. If registration fails after cloning, the checkout remains on disk; fix the registry issue and rerun the same command.
80
+
81
+ Path-based `add` is no longer supported. No sessions are started.
82
+
83
+ ### Add a branch worktree
84
+
85
+ ```sh
86
+ git workspace add holepunchto/brittle@jobs
87
+ ```
88
+
89
+ First ensures `~/workspace/holepunchto/brittle` exists and is registered, cloning it if necessary. Then creates and registers `~/workspace/holepunchto/brittle@jobs`:
90
+
91
+ - Uses the local `jobs` branch if it exists.
92
+ - Otherwise creates `jobs` tracking `origin/jobs`, if that remote-tracking branch exists locally.
93
+ - Otherwise creates `jobs` from the root checkout's current `HEAD`, without an upstream.
94
+
95
+ Existing repositories are not fetched automatically. The root checkout's branch and local edits remain unchanged. If the destination already exists, it must belong to the same Git repository and be on the requested branch; it is reused without switching branches. Git refuses creation when the branch is checked out elsewhere.
96
+
97
+ Branch names containing `/` use nested directories. For example, `git workspace add holepunchto/brittle@feature/jobs` creates branch `feature/jobs` at `~/workspace/holepunchto/brittle@feature/jobs`. Use the same name for `remove`; `list` preserves the slashes. Invalid branch names are rejected before cloning or registration.
98
+
99
+ If worktree creation fails, the root repository remains registered. If registration fails after creation, the worktree remains on disk; fix the registry issue and retry the same command.
100
+
101
+ ### Rename a root repository
102
+
103
+ ```sh
104
+ git workspace rename holepunchto/git-workspace holepunchto/git-workspace-bare
105
+ ```
106
+
107
+ Moves the root checkout and its linked `owner/repo@branch` directories to the new repository
108
+ name, repairs Git's worktree links, and updates their manifest entries. Branch names, commits,
109
+ and uncommitted files are preserved. Linked worktrees outside this naming layout stay in place
110
+ and their Git links are repaired too.
111
+
112
+ The local `origin` URL is updated to the new name, preserving SSH or HTTPS format. Explicit
113
+ origin push URLs pointing to the old repository are updated too; URLs pointing elsewhere and
114
+ other remotes are left alone. An origin already pointing to the new name is also accepted.
115
+
116
+ This is entirely local: it does not rename anything on GitHub, fetch, commit, or push. Rename
117
+ the repository separately in GitHub **Settings → General → Repository name**, or run:
118
+
119
+ ```sh
120
+ gh repo rename git-workspace-bare --repo github.com/holepunchto/git-workspace
121
+ ```
122
+
123
+ That separate command changes the GitHub repository's name without pushing commits. It requires
124
+ an authenticated [GitHub CLI](https://cli.github.com/manual/gh_repo_rename) and repository admin
125
+ permissions. You can do the GitHub rename before or after the local rename; the new origin URL
126
+ will only work once the GitHub repository has the new name.
127
+
128
+ Both arguments must use `owner/repo`. The owner can change too: the root and linked worktrees
129
+ move under the new owner's workspace directory, and origin URLs follow the new `owner/repo`.
130
+ Transfer ownership on GitHub separately; the local command does not perform that transfer.
131
+ The source must be a registered standalone root checkout with the expected GitHub origin.
132
+ Existing destination paths or manifest entries, missing or invalid linked worktrees, linked
133
+ worktrees containing submodules, locked
134
+ worktrees that would move, and symbolic-link checkouts are refused. The active manifest
135
+ repository itself cannot be renamed. If a local move, Git repair, or manifest write fails, the
136
+ command attempts to restore the original paths and origin configuration and reports any
137
+ failure to restore them.
138
+
139
+ ### Rename a branch worktree
140
+
141
+ ```sh
142
+ git workspace rename holepunchto/brittle@jobs holepunchto/brittle@fixes
143
+ ```
144
+
145
+ Renames a branch worktree and the branch it holds together, so the directory name and the
146
+ checked-out branch stay in sync. It moves `~/workspace/holepunchto/brittle@jobs` to
147
+ `~/workspace/holepunchto/brittle@fixes`, renames the local branch with `git branch -m`, and
148
+ updates the manifest entry. Uncommitted work moves with the directory; the worktree does not
149
+ need to be clean.
150
+
151
+ Both arguments are full `owner/repo@branch` names, and the `owner/repo` part must match: a
152
+ worktree cannot be renamed into a different repository.
153
+
154
+ The branch actually checked out does not have to match the `@branch` in the source name. If you
155
+ switched branches inside a worktree with ordinary Git, rename it to the branch it now holds to
156
+ line the directory back up:
157
+
158
+ ```sh
159
+ git workspace rename holepunchto/brittle@jobs holepunchto/brittle@wandered
160
+ ```
161
+
162
+ That moves the directory only. Renaming never touches a branch other than the one the worktree
163
+ has checked out, so a branch left behind by `git switch -c` survives.
164
+
165
+ If the branch has an upstream, it is retargeted to the matching branch on the same remote, so
166
+ renaming `jobs` to `fixes` leaves it tracking `origin/fixes` rather than `origin/jobs`. That
167
+ remote branch may not exist yet; push it before pulling. Branches without an upstream stay
168
+ without one, and remote branches are never renamed or deleted.
169
+
170
+ Renaming is refused if the destination directory exists, if the new branch name is already taken
171
+ by another branch, if the source is not registered or not checked out, or if the worktree is on a
172
+ detached HEAD. Nested branch names use nested directories, as with `add`, and empty parent
173
+ directories left behind are removed.
174
+
175
+ ### List repositories
176
+
177
+ ```sh
178
+ git workspace list
179
+ ```
180
+
181
+ Prints registered repositories as `owner/repo` and linked worktrees as `owner/repo@branch`, one per line sorted alphabetically, regardless of the current directory:
182
+
183
+ ```text
184
+ holepunchto/brittle
185
+ holepunchto/brittle@jobs
186
+ ```
187
+
188
+ An empty manifest produces no output. Missing or invalid manifest data reports an error.
189
+
190
+ Repositories remain listed even if their directories no longer exist. Listing does not modify the registry.
191
+
192
+ ### Check workspace status
193
+
194
+ ```sh
195
+ git workspace status
196
+ ```
197
+
198
+ Checks up to four checkouts concurrently and shows results in alphabetical order, followed by `clean`, `modified`, or `missing`. Modified includes staged changes, unstaged changes, and untracked files; ignored files do not count. Missing means the checkout path no longer exists.
199
+
200
+ An empty registry produces no output. Existing paths that are not valid Git worktree roots report an error. Status does not fetch, modify the registry, or refresh Git's index on disk. It does not report whether branches are ahead of or behind their remotes.
201
+
202
+ ### Remove a repository or worktree
203
+
204
+ ```sh
205
+ git workspace remove holepunchto/brittle
206
+ git workspace remove holepunchto/brittle@jobs
207
+ ```
208
+
209
+ Without `@branch`, deletes the root checkout, including its local Git history and branches, and removes its registry entry. The destination must be a standalone root repository with the expected GitHub origin. The active manifest repository cannot be removed. Removal refuses local changes, untracked files, symbolic-link destinations, or any linked worktrees (including stale worktree records). Remove linked worktrees first. Ignored files are deleted with the checkout, and remote repositories are untouched. Local commits are not checked for whether they have been pushed.
210
+
211
+ With `@branch`, verifies the linked worktree belongs to the root repository, removes the worktree directory, deletes the branch it actually had checked out with `git branch -D`, and unregisters it. The checked-out branch does not have to match the `@branch` in the name: a worktree you switched to another branch is still removed, and that branch is the one deleted. A worktree left on a detached HEAD is removed without deleting any branch. Branch deletion skips Git's merged-status check, including for squash-merged PRs or unmerged local commits. Worktree removal still refuses dirty or locked worktrees, and remote branches are untouched. A worktree previously unregistered by an older version can also be removed this way.
212
+
213
+ After successful worktree and branch removal, empty branch-parent directories are removed up to, but not including, the owner directory. Parents containing sibling worktrees or other files remain.
214
+
215
+ If branch deletion fails after the directory is removed, the branch and existing registry entry remain. Resolve the branch issue and retry the same command to finish removal.
216
+
217
+ Supply the same `owner/repo` or `owner/repo@branch` used by `add`. Paths resolve under `~/workspace`, regardless of the current directory. If a registered directory is already missing, branch deletion is still attempted; an already missing branch is accepted. No GitHub connection is needed, and sessions are not managed.
218
+
219
+ Successful removal is silent. Existing checkouts can also be removed if previously unregistered. A missing, unregistered repository reports an error. Path-based `remove` is not supported.
220
+
221
+ ### Errors
222
+
223
+ Failures print an `error:` message to stderr and exit with status 1. The label is red when stderr is a terminal; redirected output stays plain. When available, a separate `hint:` explains the next step:
224
+
225
+ ```text
226
+ error: cannot remove holepunchto/brittle: linked worktrees exist
227
+ hint: remove the linked worktrees first
228
+ ```
229
+
230
+ ## Development
231
+
232
+ Run the Brittle integration tests on Bare (Git must be installed):
233
+
234
+ ```sh
235
+ npm test
236
+ ```
237
+
238
+ Tests use isolated temporary workspaces and local Git repositories, disable network protocols, and clean up afterward. They do not access your real workspace or Git configuration.
239
+
240
+ Format files with Prettier and the Holepunch config:
241
+
242
+ ```sh
243
+ npm run format
244
+ ```
245
+
246
+ Lint files with Lunte:
247
+
248
+ ```sh
249
+ npm run lint
250
+ ```
251
+
252
+ The `require-await` rule is disabled, so async functions may omit `await`.
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bare
2
+
3
+ import { arg, command, flag, summary } from 'paparam'
4
+ import add from '../lib/add.js'
5
+ import init from '../lib/init.js'
6
+ import remove from '../lib/remove.js'
7
+ import rename from '../lib/rename.js'
8
+ import list from '../lib/list.js'
9
+ import status from '../lib/status.js'
10
+ import reportError from '../lib/report-error.js'
11
+ import pkg from '../package.json' with { type: 'json' }
12
+
13
+ const cmd = command(
14
+ 'git-workspace',
15
+ summary('Manage Git worktrees as development workspaces.'),
16
+ flag('--version', 'Show the version'),
17
+ command(
18
+ 'init',
19
+ summary('Initialize a GitHub repository for the workspace manifest'),
20
+ arg('<repository>', 'Manifest repository in owner/repo form'),
21
+ ({ args }) => {
22
+ try {
23
+ init(args.repository)
24
+ } catch (err) {
25
+ reportError(err)
26
+ }
27
+ }
28
+ ),
29
+ command(
30
+ 'add',
31
+ summary('Clone a repository or create a branch worktree'),
32
+ arg('<repository>', 'owner/repo or owner/repo@branch'),
33
+ ({ args }) => {
34
+ try {
35
+ add(args.repository)
36
+ } catch (err) {
37
+ reportError(err)
38
+ }
39
+ }
40
+ ),
41
+ command(
42
+ 'remove',
43
+ summary('Remove a repository or linked worktree'),
44
+ arg('<repository>', 'owner/repo or owner/repo@branch'),
45
+ ({ args }) => {
46
+ try {
47
+ remove(args.repository)
48
+ } catch (err) {
49
+ reportError(err)
50
+ }
51
+ }
52
+ ),
53
+ command(
54
+ 'rename',
55
+ summary('Rename a root repository or a branch worktree'),
56
+ arg('<from>', 'owner/repo or owner/repo@branch to rename'),
57
+ arg('<to>', 'New owner/repo or owner/repo@branch'),
58
+ ({ args }) => {
59
+ try {
60
+ rename(args.from, args.to)
61
+ } catch (err) {
62
+ reportError(err)
63
+ }
64
+ }
65
+ ),
66
+ command('list', summary('List registered repositories and worktrees'), () => {
67
+ try {
68
+ for (const repository of list()) console.log(repository)
69
+ } catch (err) {
70
+ reportError(err)
71
+ }
72
+ }),
73
+ command('status', summary('Show clean, modified, or missing checkouts'), async () => {
74
+ try {
75
+ const entries = await status()
76
+ const width = entries.reduce((max, entry) => Math.max(max, entry.repository.length), 0)
77
+ for (const { repository, state } of entries) {
78
+ console.log(`${repository.padEnd(width)} ${state}`)
79
+ }
80
+ } catch (err) {
81
+ reportError(err)
82
+ }
83
+ }),
84
+ ({ flags }) => {
85
+ if (flags.version) {
86
+ console.log(pkg.version)
87
+ }
88
+ }
89
+ )
90
+
91
+ try {
92
+ const parsed = cmd.parse()
93
+ if (parsed) await parsed.running
94
+ } catch (err) {
95
+ reportError(err)
96
+ }
package/lib/add.js ADDED
@@ -0,0 +1,105 @@
1
+ import fs from 'bare-fs'
2
+ import path from 'bare-path'
3
+ import { spawnSync } from 'bare-subprocess'
4
+ import { readRegistry, updateRegistry } from './registry.js'
5
+ import repositoryPath from './repository-path.js'
6
+
7
+ export default function add(repository) {
8
+ const directory = repositoryPath(repository)
9
+ readRegistry()
10
+ const [base, branch] = repository.split('@')
11
+ if (branch) {
12
+ git(['check-ref-format', '--branch', branch])
13
+ add(base)
14
+ const root = repositoryPath(base)
15
+ if (!exists(directory)) {
16
+ const args = ['-C', root, 'worktree', 'add']
17
+ if (hasRef(root, `refs/heads/${branch}`)) {
18
+ git([...args, directory, branch], { stdio: 'inherit' })
19
+ } else if (hasRef(root, `refs/remotes/origin/${branch}`)) {
20
+ git([...args, '--track', '-b', branch, directory, `refs/remotes/origin/${branch}`], {
21
+ stdio: 'inherit'
22
+ })
23
+ } else {
24
+ git([...args, '--no-track', '-b', branch, directory, 'HEAD'], { stdio: 'inherit' })
25
+ }
26
+ }
27
+
28
+ const worktree = fs.realpathSync(directory)
29
+ if (fs.realpathSync(git(['-C', directory, 'rev-parse', '--show-toplevel'])) !== worktree) {
30
+ throw new Error('Destination is not a Git worktree root')
31
+ }
32
+ const common = (cwd) =>
33
+ fs.realpathSync(git(['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir']))
34
+ if (
35
+ common(directory) !== common(root) ||
36
+ git(['-C', directory, 'symbolic-ref', 'HEAD']) !== `refs/heads/${branch}`
37
+ ) {
38
+ throw new Error('Destination is not a worktree of the requested repository and branch')
39
+ }
40
+ register(repository, directory, worktree)
41
+ return
42
+ }
43
+
44
+ if (!exists(directory)) {
45
+ fs.mkdirSync(path.dirname(directory), { recursive: true })
46
+ git(['clone', `git@github.com:${repository}.git`, directory], { stdio: 'inherit' })
47
+ }
48
+
49
+ const worktree = fs.realpathSync(directory)
50
+ const root = git(['-C', directory, 'rev-parse', '--show-toplevel'])
51
+ if (fs.realpathSync(root) !== worktree) throw new Error('Destination is not a Git worktree root')
52
+
53
+ const origin = git(['-C', directory, 'config', '--get', 'remote.origin.url'])
54
+ const match =
55
+ /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/)([^/]+\/[^/]+?)\/?$/i.exec(
56
+ origin
57
+ )
58
+ if (!match || match[1].replace(/\.git$/i, '').toLowerCase() !== repository.toLowerCase()) {
59
+ throw new Error(`Destination origin does not match ${repository}`)
60
+ }
61
+
62
+ register(repository, directory, worktree)
63
+ }
64
+
65
+ function exists(directory) {
66
+ try {
67
+ fs.lstatSync(directory)
68
+ return true
69
+ } catch (err) {
70
+ if (err.code !== 'ENOENT') throw err
71
+ return false
72
+ }
73
+ }
74
+
75
+ function hasRef(directory, ref) {
76
+ const result = spawnSync('git', ['-C', directory, 'show-ref', '--verify', '--quiet', ref])
77
+ if (result.error) throw result.error
78
+ if (result.status === 1) return false
79
+ if (result.status !== 0) {
80
+ throw new Error(result.stderr?.toString().trim() || 'Could not inspect Git ref')
81
+ }
82
+ return true
83
+ }
84
+
85
+ function register(repository, directory, worktree) {
86
+ try {
87
+ updateRegistry((worktrees) => {
88
+ if (!worktrees.includes(worktree)) worktrees.push(worktree)
89
+ return worktrees
90
+ })
91
+ } catch (err) {
92
+ throw new Error(
93
+ `Could not register ${directory}: ${err.message}. Retry git workspace add ${repository} after fixing the registry.`
94
+ )
95
+ }
96
+ }
97
+
98
+ function git(args, options) {
99
+ const result = spawnSync('git', args, options)
100
+ if (result.error) throw result.error
101
+ if (result.status !== 0) {
102
+ throw new Error(result.stderr?.toString().trim() || 'Git command failed')
103
+ }
104
+ return result.stdout?.toString().replace(/\r?\n$/, '')
105
+ }
@@ -0,0 +1,12 @@
1
+ import { spawnSync } from 'bare-subprocess'
2
+
3
+ export default function checkedOutBranch(directory) {
4
+ const result = spawnSync('git', ['-C', directory, 'symbolic-ref', '--quiet', 'HEAD'])
5
+ if (result.error) throw result.error
6
+ if (result.status === 1) return null
7
+ if (result.status !== 0) {
8
+ throw new Error(result.stderr?.toString().trim() || 'Could not inspect Git HEAD')
9
+ }
10
+ const ref = result.stdout?.toString().replace(/\r?\n$/, '')
11
+ return ref?.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : null
12
+ }