@precisa-saude/worktree-cli 1.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/LICENSE +201 -0
- package/README.md +140 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +657 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
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,140 @@
|
|
|
1
|
+
# @precisa-saude/worktree-cli
|
|
2
|
+
|
|
3
|
+
Shared `git worktree` lifecycle CLI for the Precisa Saúde ecosystem.
|
|
4
|
+
Drop-in replacement for each repo's hand-rolled `scripts/worktree.sh`.
|
|
5
|
+
|
|
6
|
+
- **One codebase, one behavior** across fhir-brasil, medbench-brasil,
|
|
7
|
+
datasus-brasil, and platform.
|
|
8
|
+
- **Per-repo config** declared in `package.json` — no code fork.
|
|
9
|
+
- **Single-service and multi-service** repos supported uniformly
|
|
10
|
+
(platform runs API + Web + Landing in one worktree; medbench/fhir run
|
|
11
|
+
just the site).
|
|
12
|
+
- **Atomic port allocation** via a JSON registry on `/tmp/`, guarded by a
|
|
13
|
+
`mkdir`-based lock portable to macOS and Linux.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @precisa-saude/worktree-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configure
|
|
22
|
+
|
|
23
|
+
Add a `worktree` field to the repo's root `package.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"worktree": {
|
|
28
|
+
"directoryPrefix": "medbench-brasil",
|
|
29
|
+
"portRegistry": "/tmp/medbench-worktree-ports.json",
|
|
30
|
+
"buildCommand": "pnpm turbo run build --filter=./packages/*",
|
|
31
|
+
"services": [
|
|
32
|
+
{
|
|
33
|
+
"name": "site",
|
|
34
|
+
"mainPort": 4321,
|
|
35
|
+
"featureBase": 4331,
|
|
36
|
+
"increment": 10,
|
|
37
|
+
"pnpmFilter": "@medbench-brasil/site",
|
|
38
|
+
"logPrefix": "medbench-site"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Multi-service example (platform)
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"worktree": {
|
|
50
|
+
"directoryPrefix": "platform",
|
|
51
|
+
"portRegistry": "/tmp/precisa-worktree-ports.json",
|
|
52
|
+
"services": [
|
|
53
|
+
{
|
|
54
|
+
"name": "api",
|
|
55
|
+
"mainPort": 3000,
|
|
56
|
+
"featureBase": 3010,
|
|
57
|
+
"increment": 10,
|
|
58
|
+
"pnpmFilter": "@precisasaude/api",
|
|
59
|
+
"logPrefix": "precisa-api",
|
|
60
|
+
"env": { "PORT": "{port}" }
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "web",
|
|
64
|
+
"mainPort": 5173,
|
|
65
|
+
"featureBase": 5180,
|
|
66
|
+
"increment": 10,
|
|
67
|
+
"pnpmFilter": "@precisasaude/web",
|
|
68
|
+
"logPrefix": "precisa-web",
|
|
69
|
+
"env": { "VITE_API_URL": "http://localhost:{api_port}/api" }
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
precisa-worktree setup feat/my-work # create worktree, install, allocate ports
|
|
80
|
+
cd ../<prefix>-feat-my-work
|
|
81
|
+
precisa-worktree dev # start dev servers (foreground)
|
|
82
|
+
precisa-worktree dev --detach # detached (writes to log files)
|
|
83
|
+
precisa-worktree dev --force # kill processes on conflicting ports
|
|
84
|
+
precisa-worktree stop # kill this worktree's dev servers
|
|
85
|
+
precisa-worktree logs --service=api # tail a specific service log
|
|
86
|
+
precisa-worktree list # show all worktrees + ports + status
|
|
87
|
+
precisa-worktree teardown feat/my-work # stop, remove, delete branch, free ports
|
|
88
|
+
precisa-worktree teardown --keep-branch X # preserve the local branch
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Commands that take `[branch]` auto-detect from the working directory
|
|
92
|
+
when invoked inside a linked worktree. In the main worktree, pass the
|
|
93
|
+
branch explicitly.
|
|
94
|
+
|
|
95
|
+
## Drop-in migration from `scripts/worktree.sh`
|
|
96
|
+
|
|
97
|
+
Old:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
./scripts/worktree.sh setup feat/my-work
|
|
101
|
+
./scripts/worktree.sh dev
|
|
102
|
+
./scripts/worktree.sh teardown feat/my-work
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
New:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pnpm exec precisa-worktree setup feat/my-work
|
|
109
|
+
pnpm exec precisa-worktree dev
|
|
110
|
+
pnpm exec precisa-worktree teardown feat/my-work
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or add a shim at `scripts/worktree.sh`:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
#!/usr/bin/env bash
|
|
117
|
+
exec pnpm exec precisa-worktree "$@"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## How port allocation works
|
|
121
|
+
|
|
122
|
+
- Main worktree always uses `mainPort` (never allocated — reserved by convention).
|
|
123
|
+
- Feature worktrees get a slot starting at 1. Slot N allocates
|
|
124
|
+
`featureBase + (N-1) * increment` for each service.
|
|
125
|
+
- Slots are recycled after teardown, so long-lived repos don't drift
|
|
126
|
+
into high port numbers.
|
|
127
|
+
- The registry is a JSON file on `/tmp/` (survives across sessions,
|
|
128
|
+
wiped by the OS between boots — safe default).
|
|
129
|
+
- A `mkdir`-based lock serializes concurrent setup/teardown across
|
|
130
|
+
parallel agent sessions without requiring `flock` (macOS-compatible).
|
|
131
|
+
|
|
132
|
+
## Design goals
|
|
133
|
+
|
|
134
|
+
- **No surprise behavior change** relative to the hand-rolled scripts:
|
|
135
|
+
same port allocation algorithm, same worktree directory naming, same
|
|
136
|
+
log file paths.
|
|
137
|
+
- **Fail loudly on misconfiguration**: missing config field → immediate
|
|
138
|
+
error with field name.
|
|
139
|
+
- **Cross-platform**: macOS (lsof) and Linux (ss, fuser) port probing;
|
|
140
|
+
POSIX-atomic locking; no GNU-specific flags.
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { rmdirSync as rmdirSync2 } from "fs";
|
|
5
|
+
import chalk6 from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands/dev.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { createWriteStream, existsSync as existsSync2 } from "fs";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
|
|
13
|
+
// src/config.ts
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { resolve } from "path";
|
|
16
|
+
async function loadConfig(repoRoot) {
|
|
17
|
+
const pkgPath = resolve(repoRoot, "package.json");
|
|
18
|
+
let pkg;
|
|
19
|
+
try {
|
|
20
|
+
pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error(`Failed to read ${pkgPath}: ${err.message}. Is this the repo root?`);
|
|
23
|
+
}
|
|
24
|
+
const cfg = pkg.worktree;
|
|
25
|
+
if (!cfg) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`No "worktree" field in ${pkgPath}. Add a worktree config \u2014 see https://github.com/Precisa-Saude/tooling/tree/main/packages/worktree-cli#configuration`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!cfg.directoryPrefix) {
|
|
31
|
+
throw new Error("worktree.directoryPrefix is required");
|
|
32
|
+
}
|
|
33
|
+
if (!cfg.portRegistry) {
|
|
34
|
+
throw new Error("worktree.portRegistry is required");
|
|
35
|
+
}
|
|
36
|
+
if (!cfg.services?.length) {
|
|
37
|
+
throw new Error("worktree.services must contain at least one entry");
|
|
38
|
+
}
|
|
39
|
+
for (const svc of cfg.services) {
|
|
40
|
+
if (!svc.name) throw new Error("service.name is required");
|
|
41
|
+
if (!svc.pnpmFilter) throw new Error(`service.${svc.name}.pnpmFilter is required`);
|
|
42
|
+
if (!svc.logPrefix) throw new Error(`service.${svc.name}.logPrefix is required`);
|
|
43
|
+
if (typeof svc.mainPort !== "number") {
|
|
44
|
+
throw new Error(`service.${svc.name}.mainPort must be a number`);
|
|
45
|
+
}
|
|
46
|
+
if (typeof svc.featureBase !== "number") {
|
|
47
|
+
throw new Error(`service.${svc.name}.featureBase must be a number`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return cfg;
|
|
51
|
+
}
|
|
52
|
+
function lockDir(cfg) {
|
|
53
|
+
return cfg.portLockDir ?? `${cfg.portRegistry}.lock.d`;
|
|
54
|
+
}
|
|
55
|
+
function increment(svc) {
|
|
56
|
+
return svc.increment ?? 10;
|
|
57
|
+
}
|
|
58
|
+
function devArgs(svc) {
|
|
59
|
+
return svc.devArgs ?? ["--port", "{port}"];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/paths.ts
|
|
63
|
+
import { execSync } from "child_process";
|
|
64
|
+
import { basename, dirname, resolve as resolve2 } from "path";
|
|
65
|
+
function branchToSuffix(branch) {
|
|
66
|
+
return branch.replace(/\//g, "-");
|
|
67
|
+
}
|
|
68
|
+
function worktreeDirectoryName(cfg, branch) {
|
|
69
|
+
return `${cfg.directoryPrefix}-${branchToSuffix(branch)}`;
|
|
70
|
+
}
|
|
71
|
+
function worktreePath(cfg, repoRoot, branch) {
|
|
72
|
+
const parentDir = dirname(repoRoot);
|
|
73
|
+
return resolve2(parentDir, worktreeDirectoryName(cfg, branch));
|
|
74
|
+
}
|
|
75
|
+
function logFile(svc, branch) {
|
|
76
|
+
return `/tmp/${svc.logPrefix}-${branchToSuffix(branch)}.log`;
|
|
77
|
+
}
|
|
78
|
+
function detectBranch(cfg) {
|
|
79
|
+
const cwdBase = basename(process.cwd());
|
|
80
|
+
if (!cwdBase.startsWith(`${cfg.directoryPrefix}-`)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const out = execSync("git branch --show-current", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
83
|
+
return out || null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function findRepoRoot() {
|
|
89
|
+
try {
|
|
90
|
+
return execSync("git rev-parse --show-toplevel", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
91
|
+
} catch {
|
|
92
|
+
return process.cwd();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function mainWorktreeRoot() {
|
|
96
|
+
try {
|
|
97
|
+
const out = execSync("git worktree list --porcelain", {
|
|
98
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
99
|
+
}).toString();
|
|
100
|
+
const firstLine = out.split("\n")[0] ?? "";
|
|
101
|
+
const match = firstLine.match(/^worktree (.+)$/);
|
|
102
|
+
if (match) return match[1];
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
return findRepoRoot();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/ports.ts
|
|
109
|
+
import { execSync as execSync2 } from "child_process";
|
|
110
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
111
|
+
function listenersOnPort(port) {
|
|
112
|
+
for (const [cmd, parse] of probers) {
|
|
113
|
+
if (!hasCommand(cmd)) continue;
|
|
114
|
+
try {
|
|
115
|
+
const out = execSync2(probeCommand(cmd, port), { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
116
|
+
return parse(out);
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.error(`Warning: lsof/ss/fuser not available \u2014 can't inspect port ${port}`);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
var probers = [
|
|
125
|
+
[
|
|
126
|
+
"lsof",
|
|
127
|
+
(out) => out.split("\n").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n))
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"ss",
|
|
131
|
+
(out) => {
|
|
132
|
+
const pids = [];
|
|
133
|
+
for (const line of out.split("\n")) {
|
|
134
|
+
const matches = line.match(/pid=(\d+)/g) ?? [];
|
|
135
|
+
for (const m of matches) pids.push(parseInt(m.slice(4), 10));
|
|
136
|
+
}
|
|
137
|
+
return pids;
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"fuser",
|
|
142
|
+
(out) => out.split(/\s+/).map((s) => parseInt(s, 10)).filter((n) => !isNaN(n))
|
|
143
|
+
]
|
|
144
|
+
];
|
|
145
|
+
function probeCommand(cmd, port) {
|
|
146
|
+
switch (cmd) {
|
|
147
|
+
case "lsof":
|
|
148
|
+
return `lsof -iTCP:${port} -sTCP:LISTEN -t`;
|
|
149
|
+
case "ss":
|
|
150
|
+
return `ss -tlnp | awk '$4 ~ /:${port}$/ {print}'`;
|
|
151
|
+
case "fuser":
|
|
152
|
+
return `fuser -n tcp ${port} 2>/dev/null`;
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`Unsupported prober: ${cmd}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function hasCommand(cmd) {
|
|
158
|
+
try {
|
|
159
|
+
execSync2(`command -v ${cmd}`, { stdio: "ignore" });
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isPortInUse(port) {
|
|
166
|
+
return listenersOnPort(port).length > 0;
|
|
167
|
+
}
|
|
168
|
+
async function killPort(port) {
|
|
169
|
+
const pids = listenersOnPort(port);
|
|
170
|
+
if (pids.length === 0) return;
|
|
171
|
+
for (const pid of pids) {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(pid, "SIGTERM");
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await sleep(1e3);
|
|
178
|
+
const stillAlive = listenersOnPort(port);
|
|
179
|
+
for (const pid of stillAlive) {
|
|
180
|
+
try {
|
|
181
|
+
process.kill(pid, "SIGKILL");
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/registry.ts
|
|
188
|
+
import { existsSync, mkdirSync, rmdirSync } from "fs";
|
|
189
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
190
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
191
|
+
async function ensureRegistry(cfg) {
|
|
192
|
+
if (!existsSync(cfg.portRegistry)) {
|
|
193
|
+
await writeFile(cfg.portRegistry, "{}");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function acquireLock(cfg) {
|
|
197
|
+
const dir = lockDir(cfg);
|
|
198
|
+
const maxTries = 100;
|
|
199
|
+
for (let i = 0; i < maxTries; i++) {
|
|
200
|
+
try {
|
|
201
|
+
mkdirSync(dir);
|
|
202
|
+
return;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err.code !== "EEXIST") throw err;
|
|
205
|
+
}
|
|
206
|
+
await sleep2(100);
|
|
207
|
+
}
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Timeout (~10s) waiting for lock at ${dir}. If no other session is running, remove it manually: rmdir ${dir}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
function releaseLock(cfg) {
|
|
213
|
+
try {
|
|
214
|
+
rmdirSync(lockDir(cfg));
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function withLock(cfg, fn) {
|
|
219
|
+
await ensureRegistry(cfg);
|
|
220
|
+
await acquireLock(cfg);
|
|
221
|
+
try {
|
|
222
|
+
return await fn();
|
|
223
|
+
} finally {
|
|
224
|
+
releaseLock(cfg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function readRegistry(cfg) {
|
|
228
|
+
await ensureRegistry(cfg);
|
|
229
|
+
const text = await readFile2(cfg.portRegistry, "utf-8");
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(text);
|
|
232
|
+
} catch {
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function writeRegistry(cfg, reg) {
|
|
237
|
+
await writeFile(cfg.portRegistry, `${JSON.stringify(reg, null, 2)}
|
|
238
|
+
`);
|
|
239
|
+
}
|
|
240
|
+
async function allocate(cfg, branch) {
|
|
241
|
+
return withLock(cfg, async () => {
|
|
242
|
+
const reg = await readRegistry(cfg);
|
|
243
|
+
if (reg[branch]) return reg[branch];
|
|
244
|
+
const usedSlots = new Set(Object.values(reg).map((e) => e.slot));
|
|
245
|
+
let slot = 1;
|
|
246
|
+
while (usedSlots.has(slot)) slot++;
|
|
247
|
+
const ports = {};
|
|
248
|
+
for (const svc of cfg.services) {
|
|
249
|
+
ports[svc.name] = svc.featureBase + (slot - 1) * increment(svc);
|
|
250
|
+
}
|
|
251
|
+
reg[branch] = { ports, slot };
|
|
252
|
+
await writeRegistry(cfg, reg);
|
|
253
|
+
return reg[branch];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async function free(cfg, branch) {
|
|
257
|
+
return withLock(cfg, async () => {
|
|
258
|
+
const reg = await readRegistry(cfg);
|
|
259
|
+
if (reg[branch]) {
|
|
260
|
+
delete reg[branch];
|
|
261
|
+
await writeRegistry(cfg, reg);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function getBranchEntry(cfg, branch) {
|
|
266
|
+
const reg = await readRegistry(cfg);
|
|
267
|
+
return reg[branch] ?? null;
|
|
268
|
+
}
|
|
269
|
+
function portFor(entry, svc) {
|
|
270
|
+
const port = entry.ports[svc.name];
|
|
271
|
+
if (typeof port !== "number") {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`No port allocated for service "${svc.name}" on this branch. Registry entry may be from an older config version \u2014 run teardown + setup again.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return port;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/commands/dev.ts
|
|
280
|
+
async function dev({
|
|
281
|
+
branch,
|
|
282
|
+
cfg,
|
|
283
|
+
detach = false,
|
|
284
|
+
force = false,
|
|
285
|
+
repoRoot
|
|
286
|
+
}) {
|
|
287
|
+
const entry = await getBranchEntry(cfg, branch);
|
|
288
|
+
if (!entry) {
|
|
289
|
+
console.error(chalk.red(`No ports allocated for '${branch}'. Run setup first.`));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const wtPath = worktreePath(cfg, repoRoot, branch);
|
|
293
|
+
if (!existsSync2(wtPath)) {
|
|
294
|
+
console.error(chalk.red(`Worktree not found at ${wtPath}`));
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
for (const svc of cfg.services) {
|
|
298
|
+
const port = portFor(entry, svc);
|
|
299
|
+
if (isPortInUse(port)) {
|
|
300
|
+
if (force) {
|
|
301
|
+
console.log(chalk.yellow(`Port ${port} in use \u2014 killing (--force)`));
|
|
302
|
+
await killPort(port);
|
|
303
|
+
} else {
|
|
304
|
+
console.error(
|
|
305
|
+
chalk.red(`Port ${port} (${svc.name}) already in use. Pass --force to kill.`)
|
|
306
|
+
);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log(chalk.bold(`Starting dev servers for '${branch}'...`));
|
|
312
|
+
for (const svc of cfg.services) {
|
|
313
|
+
const port = portFor(entry, svc);
|
|
314
|
+
console.log(` ${svc.name}: http://localhost:${port} (logs: ${logFile(svc, branch)})`);
|
|
315
|
+
}
|
|
316
|
+
console.log("");
|
|
317
|
+
const procs = cfg.services.map((svc) => spawnService(svc, entry, wtPath, branch, detach));
|
|
318
|
+
if (detach) {
|
|
319
|
+
for (const p of procs) p.unref();
|
|
320
|
+
console.log(chalk.green("Started detached. Use `precisa-worktree stop` to kill."));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const cleanup = (signal) => {
|
|
324
|
+
for (const p of procs) {
|
|
325
|
+
try {
|
|
326
|
+
if (!p.killed) p.kill(signal);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
332
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
333
|
+
const exitCode = await new Promise((resolvePromise) => {
|
|
334
|
+
let resolved = false;
|
|
335
|
+
for (const p of procs) {
|
|
336
|
+
p.on("exit", (code) => {
|
|
337
|
+
if (resolved) return;
|
|
338
|
+
resolved = true;
|
|
339
|
+
cleanup("SIGTERM");
|
|
340
|
+
resolvePromise(code ?? 1);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
process.exit(exitCode);
|
|
345
|
+
}
|
|
346
|
+
function spawnService(svc, entry, cwd, branch, detach) {
|
|
347
|
+
const port = portFor(entry, svc);
|
|
348
|
+
const interpolate = (s) => {
|
|
349
|
+
let out = s.replace("{port}", String(port));
|
|
350
|
+
for (const [otherName, otherPort] of Object.entries(entry.ports)) {
|
|
351
|
+
out = out.replaceAll(`{${otherName}_port}`, String(otherPort));
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
};
|
|
355
|
+
const rawArgs = devArgs(svc);
|
|
356
|
+
const args = ["--filter", svc.pnpmFilter, "dev", "--", ...rawArgs.map(interpolate)];
|
|
357
|
+
const env = { ...process.env };
|
|
358
|
+
for (const [k, v] of Object.entries(svc.env ?? {})) {
|
|
359
|
+
env[k] = interpolate(v);
|
|
360
|
+
}
|
|
361
|
+
if (detach) {
|
|
362
|
+
const logPath = logFile(svc, branch);
|
|
363
|
+
const out = createWriteStream(logPath, { flags: "a" });
|
|
364
|
+
const proc = spawn("pnpm", args, {
|
|
365
|
+
cwd,
|
|
366
|
+
detached: true,
|
|
367
|
+
env,
|
|
368
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
369
|
+
});
|
|
370
|
+
proc.stdout?.pipe(out);
|
|
371
|
+
proc.stderr?.pipe(out);
|
|
372
|
+
return proc;
|
|
373
|
+
}
|
|
374
|
+
return spawn("pnpm", args, { cwd, env, stdio: "inherit" });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/commands/list.ts
|
|
378
|
+
import { existsSync as existsSync3 } from "fs";
|
|
379
|
+
async function list({ cfg, repoRoot }) {
|
|
380
|
+
const reg = await readRegistry(cfg);
|
|
381
|
+
console.log(`Worktrees for ${cfg.directoryPrefix}:`);
|
|
382
|
+
console.log("");
|
|
383
|
+
const headers = ["BRANCH", ...cfg.services.map((s) => s.name.toUpperCase()), "STATUS"];
|
|
384
|
+
const widths = [40, ...cfg.services.map(() => 10), 10];
|
|
385
|
+
printRow(headers, widths);
|
|
386
|
+
printRow(
|
|
387
|
+
headers.map((h) => "-".repeat(Math.min(h.length, 6))),
|
|
388
|
+
widths
|
|
389
|
+
);
|
|
390
|
+
printRow(
|
|
391
|
+
[
|
|
392
|
+
"main (default)",
|
|
393
|
+
...cfg.services.map((s) => String(s.mainPort)),
|
|
394
|
+
statusOf(
|
|
395
|
+
cfg.services,
|
|
396
|
+
(s) => s.mainPort,
|
|
397
|
+
/* dirExists */
|
|
398
|
+
true
|
|
399
|
+
)
|
|
400
|
+
],
|
|
401
|
+
widths
|
|
402
|
+
);
|
|
403
|
+
for (const [branch, entry] of Object.entries(reg)) {
|
|
404
|
+
const wtPath = worktreePath(cfg, repoRoot, branch);
|
|
405
|
+
const dirExists = existsSync3(wtPath);
|
|
406
|
+
const ports = cfg.services.map((s) => entry.ports[s.name]);
|
|
407
|
+
printRow(
|
|
408
|
+
[
|
|
409
|
+
branch,
|
|
410
|
+
...ports.map((p) => String(p)),
|
|
411
|
+
dirExists ? statusOf(cfg.services, (s) => entry.ports[s.name], true) : "missing"
|
|
412
|
+
],
|
|
413
|
+
widths
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
console.log("");
|
|
417
|
+
console.log(`Registry: ${cfg.portRegistry}`);
|
|
418
|
+
}
|
|
419
|
+
function statusOf(services, portOf, dirExists) {
|
|
420
|
+
if (!dirExists) return "missing";
|
|
421
|
+
const anyRunning = services.some((s) => isPortInUse(portOf(s)));
|
|
422
|
+
return anyRunning ? "running" : "stopped";
|
|
423
|
+
}
|
|
424
|
+
function printRow(cells, widths) {
|
|
425
|
+
const line = cells.map((c, i) => c.padEnd(widths[i] ?? 10)).join(" ");
|
|
426
|
+
console.log(` ${line}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/commands/logs.ts
|
|
430
|
+
import { spawnSync } from "child_process";
|
|
431
|
+
import { existsSync as existsSync4 } from "fs";
|
|
432
|
+
import chalk2 from "chalk";
|
|
433
|
+
async function logs({ branch, cfg, service }) {
|
|
434
|
+
const svc = service ? cfg.services.find((s) => s.name === service) : cfg.services[0];
|
|
435
|
+
if (!svc) {
|
|
436
|
+
const names = cfg.services.map((s) => s.name).join(", ");
|
|
437
|
+
console.error(chalk2.red(`Unknown service "${service}". Valid services: ${names}`));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const path = logFile(svc, branch);
|
|
441
|
+
if (!existsSync4(path)) {
|
|
442
|
+
console.error(chalk2.red(`Log file not found: ${path}`));
|
|
443
|
+
console.error(`Start the dev server first: precisa-worktree dev ${branch}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
const result = spawnSync("tail", ["-f", path], { stdio: "inherit" });
|
|
447
|
+
process.exit(result.status ?? 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/setup.ts
|
|
451
|
+
import { execFileSync } from "child_process";
|
|
452
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
453
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
454
|
+
import chalk3 from "chalk";
|
|
455
|
+
async function setup({ branch, cfg, repoRoot }) {
|
|
456
|
+
const wtPath = worktreePath(cfg, repoRoot, branch);
|
|
457
|
+
if (existsSync5(wtPath)) {
|
|
458
|
+
console.error(chalk3.red(`Worktree already exists: ${wtPath}`));
|
|
459
|
+
console.error(`To start dev servers: precisa-worktree dev ${branch}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
console.log(chalk3.bold(`==> Creating worktree for '${branch}' at ${wtPath}...`));
|
|
463
|
+
execFileSync("git", ["-C", repoRoot, "fetch", "origin", "main"], { stdio: "inherit" });
|
|
464
|
+
execFileSync("git", ["-C", repoRoot, "worktree", "add", "-b", branch, wtPath, "origin/main"], {
|
|
465
|
+
stdio: "inherit"
|
|
466
|
+
});
|
|
467
|
+
const entry = await allocate(cfg, branch);
|
|
468
|
+
if (cfg.install !== false) {
|
|
469
|
+
console.log(chalk3.bold("==> Installing dependencies (pnpm install)..."));
|
|
470
|
+
execFileSync("pnpm", ["install"], { cwd: wtPath, stdio: "inherit" });
|
|
471
|
+
}
|
|
472
|
+
if (cfg.buildCommand) {
|
|
473
|
+
console.log(chalk3.bold(`==> Running build: ${cfg.buildCommand}`));
|
|
474
|
+
execFileSync("sh", ["-c", cfg.buildCommand], { cwd: wtPath, stdio: "inherit" });
|
|
475
|
+
}
|
|
476
|
+
if (cfg.writeFiles?.length) {
|
|
477
|
+
console.log(chalk3.bold("==> Writing worktree-scoped files"));
|
|
478
|
+
for (const { contents, path } of cfg.writeFiles) {
|
|
479
|
+
let interpolated = contents;
|
|
480
|
+
for (const [svcName, port] of Object.entries(entry.ports)) {
|
|
481
|
+
interpolated = interpolated.replaceAll(`{${svcName}_port}`, String(port)).replaceAll("{port}", String(port));
|
|
482
|
+
}
|
|
483
|
+
const fullPath = resolve3(wtPath, path);
|
|
484
|
+
mkdirSync2(dirname2(fullPath), { recursive: true });
|
|
485
|
+
writeFileSync(fullPath, interpolated);
|
|
486
|
+
console.log(` wrote ${path}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
console.log("");
|
|
490
|
+
console.log(chalk3.green(`Worktree ready: ${wtPath}`));
|
|
491
|
+
console.log(` Branch: ${branch}`);
|
|
492
|
+
for (const svc of cfg.services) {
|
|
493
|
+
const port = entry.ports[svc.name];
|
|
494
|
+
console.log(
|
|
495
|
+
` ${svc.name.padEnd(8)}: http://localhost:${port} (logs: ${logFile(svc, branch)})`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
console.log("");
|
|
499
|
+
console.log("To start the dev server(s):");
|
|
500
|
+
console.log(` cd ${wtPath} && precisa-worktree dev ${branch}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/commands/stop.ts
|
|
504
|
+
import chalk4 from "chalk";
|
|
505
|
+
async function stop({ branch, cfg }) {
|
|
506
|
+
const entry = await getBranchEntry(cfg, branch);
|
|
507
|
+
if (!entry) {
|
|
508
|
+
console.error(`No ports registered for '${branch}'.`);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
let anyRunning = false;
|
|
512
|
+
for (const svc of cfg.services) {
|
|
513
|
+
const port = portFor(entry, svc);
|
|
514
|
+
if (isPortInUse(port)) {
|
|
515
|
+
console.log(chalk4.bold(`Killing ${svc.name} dev server on port ${port}...`));
|
|
516
|
+
await killPort(port);
|
|
517
|
+
anyRunning = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (!anyRunning) {
|
|
521
|
+
console.log(`No dev servers running for '${branch}'.`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/commands/teardown.ts
|
|
526
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
527
|
+
import { existsSync as existsSync6, rmSync } from "fs";
|
|
528
|
+
import chalk5 from "chalk";
|
|
529
|
+
async function teardown({
|
|
530
|
+
branch,
|
|
531
|
+
cfg,
|
|
532
|
+
keepBranch = false,
|
|
533
|
+
repoRoot
|
|
534
|
+
}) {
|
|
535
|
+
const entry = await getBranchEntry(cfg, branch);
|
|
536
|
+
const wtPath = worktreePath(cfg, repoRoot, branch);
|
|
537
|
+
if (entry) {
|
|
538
|
+
for (const svc of cfg.services) {
|
|
539
|
+
const port = portFor(entry, svc);
|
|
540
|
+
if (isPortInUse(port)) {
|
|
541
|
+
console.log(chalk5.bold(`Stopping ${svc.name} dev server on port ${port}...`));
|
|
542
|
+
await killPort(port);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (existsSync6(wtPath)) {
|
|
547
|
+
console.log(chalk5.bold(`Removing worktree: ${wtPath}`));
|
|
548
|
+
execFileSync2("git", ["-C", repoRoot, "worktree", "remove", wtPath, "--force"], {
|
|
549
|
+
stdio: "inherit"
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
if (keepBranch) {
|
|
553
|
+
console.log(`Preserving local branch: ${branch}`);
|
|
554
|
+
} else {
|
|
555
|
+
const branchExists = branchRefExists(repoRoot, branch);
|
|
556
|
+
if (branchExists) {
|
|
557
|
+
console.log(chalk5.bold(`Deleting local branch: ${branch}`));
|
|
558
|
+
try {
|
|
559
|
+
execFileSync2("git", ["-C", repoRoot, "branch", "-d", branch], {
|
|
560
|
+
stdio: "ignore"
|
|
561
|
+
});
|
|
562
|
+
} catch {
|
|
563
|
+
execFileSync2("git", ["-C", repoRoot, "branch", "-D", branch], {
|
|
564
|
+
stdio: "inherit"
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
await free(cfg, branch);
|
|
570
|
+
for (const svc of cfg.services) {
|
|
571
|
+
const path = logFile(svc, branch);
|
|
572
|
+
if (existsSync6(path)) rmSync(path);
|
|
573
|
+
}
|
|
574
|
+
console.log(chalk5.green(`Teardown complete for '${branch}'.`));
|
|
575
|
+
}
|
|
576
|
+
function branchRefExists(repoRoot, branch) {
|
|
577
|
+
try {
|
|
578
|
+
execFileSync2(
|
|
579
|
+
"git",
|
|
580
|
+
["-C", repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
|
|
581
|
+
{
|
|
582
|
+
stdio: "ignore"
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
return true;
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/bin.ts
|
|
592
|
+
async function main() {
|
|
593
|
+
const program = new Command();
|
|
594
|
+
program.name("precisa-worktree").description("Git-worktree lifecycle CLI for Precisa Sa\xFAde repositories").version("1.0.4");
|
|
595
|
+
program.command("setup <branch>").description("Create a worktree, install deps, allocate ports").action(async (branch) => {
|
|
596
|
+
const { cfg, mainRoot } = await resolveContext({ requireMain: true });
|
|
597
|
+
await setup({ branch, cfg, repoRoot: mainRoot });
|
|
598
|
+
});
|
|
599
|
+
program.command("dev [branch]").description("Start dev server(s) on the allocated ports").option("-d, --detach", "Run detached (nohup-style); write to log files", false).option("-f, --force", "Kill any process already on the port", false).action(async (branchArg, opts) => {
|
|
600
|
+
const { branch, cfg, wtRoot } = await resolveContext({ allowDetect: true, branchArg });
|
|
601
|
+
await dev({ branch, cfg, detach: opts.detach, force: opts.force, repoRoot: wtRoot });
|
|
602
|
+
});
|
|
603
|
+
program.command("stop [branch]").description("Kill dev server(s) for a worktree").action(async (branchArg) => {
|
|
604
|
+
const { branch, cfg } = await resolveContext({ allowDetect: true, branchArg });
|
|
605
|
+
await stop({ branch, cfg });
|
|
606
|
+
});
|
|
607
|
+
program.command("teardown <branch>").description("Stop dev servers, remove worktree, delete branch, free ports").option("--keep-branch", "Preserve the local branch", false).action(async (branch, opts) => {
|
|
608
|
+
const { cfg, mainRoot } = await resolveContext({ requireMain: true });
|
|
609
|
+
await teardown({ branch, cfg, keepBranch: opts.keepBranch, repoRoot: mainRoot });
|
|
610
|
+
});
|
|
611
|
+
program.command("list").description("List all worktrees with ports and status").action(async () => {
|
|
612
|
+
const { cfg, mainRoot } = await resolveContext({ requireMain: true });
|
|
613
|
+
await list({ cfg, repoRoot: mainRoot });
|
|
614
|
+
});
|
|
615
|
+
program.command("logs [branch]").description("Tail the log for a worktree dev server").option("-s, --service <name>", "Service name (defaults to the first configured)").action(async (branchArg, opts) => {
|
|
616
|
+
const { branch, cfg } = await resolveContext({ allowDetect: true, branchArg });
|
|
617
|
+
await logs({ branch, cfg, service: opts.service });
|
|
618
|
+
});
|
|
619
|
+
process.on("exit", () => {
|
|
620
|
+
try {
|
|
621
|
+
} catch {
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
await program.parseAsync(process.argv);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.error(chalk6.red(err.message));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function resolveContext(opts) {
|
|
632
|
+
const wtRoot = findRepoRoot();
|
|
633
|
+
const mainRoot = mainWorktreeRoot();
|
|
634
|
+
const cfg = await loadConfig(mainRoot);
|
|
635
|
+
const lockPath = lockDir(cfg);
|
|
636
|
+
process.on("exit", () => {
|
|
637
|
+
try {
|
|
638
|
+
rmdirSync2(lockPath);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
let branch = opts.branchArg ?? "";
|
|
643
|
+
if (!branch && opts.allowDetect) {
|
|
644
|
+
branch = detectBranch(cfg) ?? "";
|
|
645
|
+
}
|
|
646
|
+
if (!branch && !opts.requireMain) {
|
|
647
|
+
console.error(
|
|
648
|
+
chalk6.red(
|
|
649
|
+
"Couldn't detect branch. Run from inside a linked worktree or pass the branch name."
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
return { branch, cfg, mainRoot, wtRoot };
|
|
655
|
+
}
|
|
656
|
+
void main();
|
|
657
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts","../src/commands/dev.ts","../src/config.ts","../src/paths.ts","../src/ports.ts","../src/registry.ts","../src/commands/list.ts","../src/commands/logs.ts","../src/commands/setup.ts","../src/commands/stop.ts","../src/commands/teardown.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { rmdirSync } from 'node:fs';\n\nimport chalk from 'chalk';\nimport { Command } from 'commander';\n\nimport { dev } from './commands/dev.js';\nimport { list } from './commands/list.js';\nimport { logs } from './commands/logs.js';\nimport { setup } from './commands/setup.js';\nimport { stop } from './commands/stop.js';\nimport { teardown } from './commands/teardown.js';\nimport { loadConfig, lockDir, type WorktreeConfig } from './config.js';\nimport { detectBranch, findRepoRoot, mainWorktreeRoot } from './paths.js';\n\nasync function main(): Promise<void> {\n const program = new Command();\n program\n .name('precisa-worktree')\n .description('Git-worktree lifecycle CLI for Precisa Saúde repositories')\n .version('1.0.4');\n\n program\n .command('setup <branch>')\n .description('Create a worktree, install deps, allocate ports')\n .action(async (branch: string) => {\n const { cfg, mainRoot } = await resolveContext({ requireMain: true });\n await setup({ branch, cfg, repoRoot: mainRoot });\n });\n\n program\n .command('dev [branch]')\n .description('Start dev server(s) on the allocated ports')\n .option('-d, --detach', 'Run detached (nohup-style); write to log files', false)\n .option('-f, --force', 'Kill any process already on the port', false)\n .action(async (branchArg: string | undefined, opts: { detach: boolean; force: boolean }) => {\n const { branch, cfg, wtRoot } = await resolveContext({ allowDetect: true, branchArg });\n await dev({ branch, cfg, detach: opts.detach, force: opts.force, repoRoot: wtRoot });\n });\n\n program\n .command('stop [branch]')\n .description('Kill dev server(s) for a worktree')\n .action(async (branchArg?: string) => {\n const { branch, cfg } = await resolveContext({ allowDetect: true, branchArg });\n await stop({ branch, cfg });\n });\n\n program\n .command('teardown <branch>')\n .description('Stop dev servers, remove worktree, delete branch, free ports')\n .option('--keep-branch', 'Preserve the local branch', false)\n .action(async (branch: string, opts: { keepBranch: boolean }) => {\n const { cfg, mainRoot } = await resolveContext({ requireMain: true });\n await teardown({ branch, cfg, keepBranch: opts.keepBranch, repoRoot: mainRoot });\n });\n\n program\n .command('list')\n .description('List all worktrees with ports and status')\n .action(async () => {\n const { cfg, mainRoot } = await resolveContext({ requireMain: true });\n await list({ cfg, repoRoot: mainRoot });\n });\n\n program\n .command('logs [branch]')\n .description('Tail the log for a worktree dev server')\n .option('-s, --service <name>', 'Service name (defaults to the first configured)')\n .action(async (branchArg: string | undefined, opts: { service?: string }) => {\n const { branch, cfg } = await resolveContext({ allowDetect: true, branchArg });\n await logs({ branch, cfg, service: opts.service });\n });\n\n // Global EXIT handler — release the lock if we died mid-operation.\n // Safe because `rmdir` only succeeds on an empty directory we created.\n process.on('exit', () => {\n try {\n // Best-effort; we don't know the config if we failed before loading it.\n // This handler is installed after config load via resolveContext.\n } catch {\n // ignore\n }\n });\n\n try {\n await program.parseAsync(process.argv);\n } catch (err) {\n console.error(chalk.red((err as Error).message));\n process.exit(1);\n }\n}\n\ninterface ResolveOpts {\n /** Allow `detectBranch()` when inside a linked worktree. */\n allowDetect?: boolean;\n branchArg?: string | undefined;\n /** Require the repo root to be the main worktree (setup/teardown/list). */\n requireMain?: boolean;\n}\n\nasync function resolveContext(opts: ResolveOpts): Promise<{\n cfg: WorktreeConfig;\n wtRoot: string;\n mainRoot: string;\n branch: string;\n}> {\n const wtRoot = findRepoRoot();\n const mainRoot = mainWorktreeRoot();\n const cfg = await loadConfig(mainRoot);\n\n // Install an exit handler now that we know the lock path.\n const lockPath = lockDir(cfg);\n process.on('exit', () => {\n try {\n rmdirSync(lockPath);\n } catch {\n // already released or never held\n }\n });\n\n let branch = opts.branchArg ?? '';\n if (!branch && opts.allowDetect) {\n branch = detectBranch(cfg) ?? '';\n }\n\n if (!branch && !opts.requireMain) {\n console.error(\n chalk.red(\n \"Couldn't detect branch. Run from inside a linked worktree or pass the branch name.\",\n ),\n );\n process.exit(1);\n }\n\n return { branch, cfg, mainRoot, wtRoot };\n}\n\nvoid main();\n","import { spawn } from 'node:child_process';\nimport { createWriteStream, existsSync } from 'node:fs';\n\nimport chalk from 'chalk';\n\nimport { devArgs, type ServiceConfig, type WorktreeConfig } from '../config.js';\nimport { logFile, worktreePath } from '../paths.js';\nimport { isPortInUse, killPort } from '../ports.js';\nimport { getBranchEntry, portFor } from '../registry.js';\n\nexport interface DevOptions {\n branch: string;\n cfg: WorktreeConfig;\n /** Run detached from the terminal; write stdout/stderr to log files. */\n detach?: boolean;\n /** If a port is already in use, kill the process instead of aborting. */\n force?: boolean;\n repoRoot: string;\n}\n\nexport async function dev({\n branch,\n cfg,\n detach = false,\n force = false,\n repoRoot,\n}: DevOptions): Promise<void> {\n const entry = await getBranchEntry(cfg, branch);\n if (!entry) {\n console.error(chalk.red(`No ports allocated for '${branch}'. Run setup first.`));\n process.exit(1);\n }\n\n const wtPath = worktreePath(cfg, repoRoot, branch);\n if (!existsSync(wtPath)) {\n console.error(chalk.red(`Worktree not found at ${wtPath}`));\n process.exit(1);\n }\n\n for (const svc of cfg.services) {\n const port = portFor(entry, svc);\n if (isPortInUse(port)) {\n if (force) {\n console.log(chalk.yellow(`Port ${port} in use — killing (--force)`));\n await killPort(port);\n } else {\n console.error(\n chalk.red(`Port ${port} (${svc.name}) already in use. Pass --force to kill.`),\n );\n process.exit(1);\n }\n }\n }\n\n console.log(chalk.bold(`Starting dev servers for '${branch}'...`));\n for (const svc of cfg.services) {\n const port = portFor(entry, svc);\n console.log(` ${svc.name}: http://localhost:${port} (logs: ${logFile(svc, branch)})`);\n }\n console.log('');\n\n const procs = cfg.services.map((svc) => spawnService(svc, entry, wtPath, branch, detach));\n\n if (detach) {\n for (const p of procs) p.unref();\n console.log(chalk.green('Started detached. Use `precisa-worktree stop` to kill.'));\n return;\n }\n\n // Foreground: forward signals, wait for first exit.\n const cleanup = (signal: NodeJS.Signals): void => {\n for (const p of procs) {\n try {\n if (!p.killed) p.kill(signal);\n } catch {\n // already dead\n }\n }\n };\n process.on('SIGINT', () => cleanup('SIGINT'));\n process.on('SIGTERM', () => cleanup('SIGTERM'));\n\n const exitCode = await new Promise<number>((resolvePromise) => {\n let resolved = false;\n for (const p of procs) {\n p.on('exit', (code) => {\n if (resolved) return;\n resolved = true;\n cleanup('SIGTERM');\n resolvePromise(code ?? 1);\n });\n }\n });\n process.exit(exitCode);\n}\n\nfunction spawnService(\n svc: ServiceConfig,\n entry: { ports: Record<string, number> },\n cwd: string,\n branch: string,\n detach: boolean,\n): ReturnType<typeof spawn> {\n const port = portFor(entry, svc);\n const interpolate = (s: string): string => {\n let out = s.replace('{port}', String(port));\n // Allow cross-service refs like `{api_port}` so one service's env\n // can embed another service's allocated port.\n for (const [otherName, otherPort] of Object.entries(entry.ports)) {\n out = out.replaceAll(`{${otherName}_port}`, String(otherPort));\n }\n return out;\n };\n\n const rawArgs = devArgs(svc);\n const args = ['--filter', svc.pnpmFilter, 'dev', '--', ...rawArgs.map(interpolate)];\n\n const env = { ...process.env };\n for (const [k, v] of Object.entries(svc.env ?? {})) {\n env[k] = interpolate(v);\n }\n\n if (detach) {\n const logPath = logFile(svc, branch);\n const out = createWriteStream(logPath, { flags: 'a' });\n const proc = spawn('pnpm', args, {\n cwd,\n detached: true,\n env,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n proc.stdout?.pipe(out);\n proc.stderr?.pipe(out);\n return proc;\n }\n\n return spawn('pnpm', args, { cwd, env, stdio: 'inherit' });\n}\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nexport interface ServiceConfig {\n /**\n * Extra args passed to the dev script. `{port}` is replaced with the\n * allocated port. Defaults to `['--port', '{port}']` (works with vite).\n */\n devArgs?: string[];\n /**\n * Optional: additional env vars to set for the dev process. The literal\n * `{port}` is substituted. Example: `{ VITE_API_URL: 'http://localhost:{port}/api' }`.\n */\n env?: Record<string, string>;\n /**\n * First port for feature worktrees. Slot 1 uses this port; slot 2 uses\n * `featureBase + increment`, slot 3 uses `featureBase + 2*increment`, etc.\n */\n featureBase: number;\n /** Increment between slots. Defaults to 10. */\n increment?: number;\n /**\n * Prefix for the log file path: `/tmp/<logPrefix>-<branch>.log`. Required\n * so that sibling services in a repo don't overwrite each other's logs.\n */\n logPrefix: string;\n /** Port used by the default (main) worktree. */\n mainPort: number;\n /** Service name used in CLI output, column headers, and log file naming. */\n name: string;\n /** pnpm filter pattern for `worktree dev`. Example: `@medbench-brasil/site`. */\n pnpmFilter: string;\n}\n\nexport interface WorktreeConfig {\n /**\n * Optional extra build command run during `setup`, after `pnpm install`.\n * Example: `pnpm turbo run build --filter=./packages/*`.\n */\n buildCommand?: string;\n /**\n * Prefix used for worktree directory names: `<prefix>-<branch>`.\n * Example: `medbench-brasil` → worktrees land at\n * `../medbench-brasil-feat-foo`.\n */\n directoryPrefix: string;\n /**\n * Whether to run `pnpm install` during setup. Defaults to true. Set to\n * false for repos where install should be manual or where the script\n * is invoked inside a container.\n */\n install?: boolean;\n /** Absolute path to the lock directory. Defaults to `<portRegistry>.lock.d`. */\n portLockDir?: string;\n /** Absolute path to the port registry JSON file. */\n portRegistry: string;\n /** Services managed by this repo. At least one required. */\n services: ServiceConfig[];\n /**\n * Extra per-service file writes performed during `setup`, after\n * install + build. Intended for repos that need `.env.local`-style\n * files seeded with the allocated ports so that running `dev` without\n * explicit env vars picks up the right values.\n *\n * Each entry is `{ path, contents }` where `contents` supports the\n * same `{port}` and `{<service>_port}` substitutions available in\n * `service.env` / `service.devArgs`.\n *\n * Example (platform):\n * writeFiles: [\n * { path: 'apps/web/.env.local',\n * contents: 'VITE_DEV_PORT={web_port}\\nVITE_API_URL=http://localhost:{api_port}/api\\n' },\n * { path: 'apps/api/.env.local',\n * contents: 'PORT={api_port}\\n' },\n * ]\n */\n writeFiles?: Array<{ path: string; contents: string }>;\n}\n\ninterface PackageJson {\n name?: string;\n worktree?: WorktreeConfig;\n}\n\nexport async function loadConfig(repoRoot: string): Promise<WorktreeConfig> {\n const pkgPath = resolve(repoRoot, 'package.json');\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(await readFile(pkgPath, 'utf-8')) as PackageJson;\n } catch (err) {\n throw new Error(`Failed to read ${pkgPath}: ${(err as Error).message}. Is this the repo root?`);\n }\n\n const cfg = pkg.worktree;\n if (!cfg) {\n throw new Error(\n `No \"worktree\" field in ${pkgPath}. Add a worktree config — see ` +\n `https://github.com/Precisa-Saude/tooling/tree/main/packages/worktree-cli#configuration`,\n );\n }\n\n if (!cfg.directoryPrefix) {\n throw new Error('worktree.directoryPrefix is required');\n }\n if (!cfg.portRegistry) {\n throw new Error('worktree.portRegistry is required');\n }\n if (!cfg.services?.length) {\n throw new Error('worktree.services must contain at least one entry');\n }\n for (const svc of cfg.services) {\n if (!svc.name) throw new Error('service.name is required');\n if (!svc.pnpmFilter) throw new Error(`service.${svc.name}.pnpmFilter is required`);\n if (!svc.logPrefix) throw new Error(`service.${svc.name}.logPrefix is required`);\n if (typeof svc.mainPort !== 'number') {\n throw new Error(`service.${svc.name}.mainPort must be a number`);\n }\n if (typeof svc.featureBase !== 'number') {\n throw new Error(`service.${svc.name}.featureBase must be a number`);\n }\n }\n\n return cfg;\n}\n\nexport function lockDir(cfg: WorktreeConfig): string {\n return cfg.portLockDir ?? `${cfg.portRegistry}.lock.d`;\n}\n\nexport function increment(svc: ServiceConfig): number {\n return svc.increment ?? 10;\n}\n\nexport function devArgs(svc: ServiceConfig): string[] {\n return svc.devArgs ?? ['--port', '{port}'];\n}\n","import { execSync } from 'node:child_process';\nimport { basename, dirname, resolve } from 'node:path';\n\nimport type { ServiceConfig, WorktreeConfig } from './config.js';\n\n/** Convert a branch name to a safe directory-suffix: `feat/foo` → `feat-foo`. */\nexport function branchToSuffix(branch: string): string {\n return branch.replace(/\\//g, '-');\n}\n\nexport function worktreeDirectoryName(cfg: WorktreeConfig, branch: string): string {\n return `${cfg.directoryPrefix}-${branchToSuffix(branch)}`;\n}\n\nexport function worktreePath(cfg: WorktreeConfig, repoRoot: string, branch: string): string {\n const parentDir = dirname(repoRoot);\n return resolve(parentDir, worktreeDirectoryName(cfg, branch));\n}\n\nexport function logFile(svc: ServiceConfig, branch: string): string {\n return `/tmp/${svc.logPrefix}-${branchToSuffix(branch)}.log`;\n}\n\n/**\n * Detect the current branch when invoked from inside a linked worktree.\n * Returns null from the main worktree — refusing auto-detect there avoids\n * accidentally acting on `main` when the user forgot to pass a branch.\n */\nexport function detectBranch(cfg: WorktreeConfig): string | null {\n const cwdBase = basename(process.cwd());\n if (!cwdBase.startsWith(`${cfg.directoryPrefix}-`)) return null;\n try {\n const out = execSync('git branch --show-current', { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .trim();\n return out || null;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the repo root by walking up from cwd looking for a `package.json`\n * with a `worktree` field. If invoked from inside a linked worktree, falls\n * back to the main worktree (identified by the directory NOT having the\n * `<prefix>-<branch>` suffix).\n */\nexport function findRepoRoot(): string {\n // `git rev-parse --show-toplevel` gives the worktree root (linked or main).\n try {\n return execSync('git rev-parse --show-toplevel', { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .trim();\n } catch {\n return process.cwd();\n }\n}\n\n/**\n * From a linked worktree, walk back to the main worktree using\n * `git worktree list --porcelain`. The first entry is always the main.\n */\nexport function mainWorktreeRoot(): string {\n try {\n const out = execSync('git worktree list --porcelain', {\n stdio: ['ignore', 'pipe', 'ignore'],\n }).toString();\n const firstLine = out.split('\\n')[0] ?? '';\n const match = firstLine.match(/^worktree (.+)$/);\n if (match) return match[1]!;\n } catch {\n // fall through\n }\n return findRepoRoot();\n}\n","import { execSync } from 'node:child_process';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\n/**\n * Return PIDs listening on `port` using the first available tool.\n * Prefers lsof (macOS default), falls back to ss then fuser (Linux).\n * Returns empty array if none can inspect ports or nothing is listening.\n */\nfunction listenersOnPort(port: number): number[] {\n for (const [cmd, parse] of probers) {\n if (!hasCommand(cmd)) continue;\n try {\n const out = execSync(probeCommand(cmd, port), { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .trim();\n return parse(out);\n } catch {\n // non-zero exit — no listeners found\n return [];\n }\n }\n console.error(`Warning: lsof/ss/fuser not available — can't inspect port ${port}`);\n return [];\n}\n\ntype Prober = [cmd: string, parse: (output: string) => number[]];\n\nconst probers: Prober[] = [\n [\n 'lsof',\n (out) =>\n out\n .split('\\n')\n .map((s) => parseInt(s.trim(), 10))\n .filter((n) => !isNaN(n)),\n ],\n [\n 'ss',\n (out) => {\n const pids: number[] = [];\n for (const line of out.split('\\n')) {\n const matches = line.match(/pid=(\\d+)/g) ?? [];\n for (const m of matches) pids.push(parseInt(m.slice(4), 10));\n }\n return pids;\n },\n ],\n [\n 'fuser',\n (out) =>\n out\n .split(/\\s+/)\n .map((s) => parseInt(s, 10))\n .filter((n) => !isNaN(n)),\n ],\n];\n\nfunction probeCommand(cmd: string, port: number): string {\n switch (cmd) {\n case 'lsof':\n return `lsof -iTCP:${port} -sTCP:LISTEN -t`;\n case 'ss':\n return `ss -tlnp | awk '$4 ~ /:${port}$/ {print}'`;\n case 'fuser':\n return `fuser -n tcp ${port} 2>/dev/null`;\n default:\n throw new Error(`Unsupported prober: ${cmd}`);\n }\n}\n\nfunction hasCommand(cmd: string): boolean {\n try {\n execSync(`command -v ${cmd}`, { stdio: 'ignore' });\n return true;\n } catch {\n return false;\n }\n}\n\nexport function isPortInUse(port: number): boolean {\n return listenersOnPort(port).length > 0;\n}\n\n/** Kill processes listening on `port`. SIGTERM first, then SIGKILL if any survive. */\nexport async function killPort(port: number): Promise<void> {\n const pids = listenersOnPort(port);\n if (pids.length === 0) return;\n\n for (const pid of pids) {\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // process may have exited between listing and kill — ignore\n }\n }\n\n await sleep(1000);\n\n const stillAlive = listenersOnPort(port);\n for (const pid of stillAlive) {\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n // ignore\n }\n }\n}\n","import { existsSync, mkdirSync, rmdirSync } from 'node:fs';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { setTimeout as sleep } from 'node:timers/promises';\n\nimport { increment, lockDir, type ServiceConfig, type WorktreeConfig } from './config.js';\n\nexport type Registry = Record<string, { slot: number; ports: Record<string, number> }>;\n\nasync function ensureRegistry(cfg: WorktreeConfig): Promise<void> {\n if (!existsSync(cfg.portRegistry)) {\n await writeFile(cfg.portRegistry, '{}');\n }\n}\n\n/**\n * Acquire an exclusive lock via `mkdir` — POSIX-atomic, portable across\n * macOS/Linux, no dependency on `flock` (which isn't on macOS by default).\n * Used to serialize registry read-modify-write across concurrent sessions.\n */\nasync function acquireLock(cfg: WorktreeConfig): Promise<void> {\n const dir = lockDir(cfg);\n const maxTries = 100;\n for (let i = 0; i < maxTries; i++) {\n try {\n mkdirSync(dir);\n return;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;\n }\n await sleep(100);\n }\n throw new Error(\n `Timeout (~10s) waiting for lock at ${dir}. If no other session is ` +\n `running, remove it manually: rmdir ${dir}`,\n );\n}\n\nfunction releaseLock(cfg: WorktreeConfig): void {\n try {\n rmdirSync(lockDir(cfg));\n } catch {\n // ignore — already released or never held\n }\n}\n\n/**\n * Run `fn` while holding the registry lock. Always releases, even on throw.\n * The release is also safe against signal interruption because the lock\n * directory will be removed at process exit via the handler in bin.ts.\n */\nexport async function withLock<T>(cfg: WorktreeConfig, fn: () => Promise<T> | T): Promise<T> {\n await ensureRegistry(cfg);\n await acquireLock(cfg);\n try {\n return await fn();\n } finally {\n releaseLock(cfg);\n }\n}\n\nexport async function readRegistry(cfg: WorktreeConfig): Promise<Registry> {\n await ensureRegistry(cfg);\n const text = await readFile(cfg.portRegistry, 'utf-8');\n try {\n return JSON.parse(text) as Registry;\n } catch {\n return {};\n }\n}\n\nasync function writeRegistry(cfg: WorktreeConfig, reg: Registry): Promise<void> {\n await writeFile(cfg.portRegistry, `${JSON.stringify(reg, null, 2)}\\n`);\n}\n\n/**\n * Allocate a slot+ports for `branch`. Idempotent: returns the existing\n * entry if the branch already has one. Reuses freed slots so slot numbers\n * don't grow unbounded over many create/teardown cycles.\n */\nexport async function allocate(\n cfg: WorktreeConfig,\n branch: string,\n): Promise<{ slot: number; ports: Record<string, number> }> {\n return withLock(cfg, async () => {\n const reg = await readRegistry(cfg);\n if (reg[branch]) return reg[branch];\n\n const usedSlots = new Set(Object.values(reg).map((e) => e.slot));\n let slot = 1;\n while (usedSlots.has(slot)) slot++;\n\n const ports: Record<string, number> = {};\n for (const svc of cfg.services) {\n ports[svc.name] = svc.featureBase + (slot - 1) * increment(svc);\n }\n\n reg[branch] = { ports, slot };\n await writeRegistry(cfg, reg);\n return reg[branch];\n });\n}\n\nexport async function free(cfg: WorktreeConfig, branch: string): Promise<void> {\n return withLock(cfg, async () => {\n const reg = await readRegistry(cfg);\n if (reg[branch]) {\n delete reg[branch];\n await writeRegistry(cfg, reg);\n }\n });\n}\n\nexport async function getBranchEntry(\n cfg: WorktreeConfig,\n branch: string,\n): Promise<{ slot: number; ports: Record<string, number> } | null> {\n const reg = await readRegistry(cfg);\n return reg[branch] ?? null;\n}\n\nexport function portFor(entry: { ports: Record<string, number> }, svc: ServiceConfig): number {\n const port = entry.ports[svc.name];\n if (typeof port !== 'number') {\n throw new Error(\n `No port allocated for service \"${svc.name}\" on this branch. ` +\n `Registry entry may be from an older config version — run teardown + setup again.`,\n );\n }\n return port;\n}\n","import { existsSync } from 'node:fs';\n\nimport { type ServiceConfig, type WorktreeConfig } from '../config.js';\nimport { worktreePath } from '../paths.js';\nimport { isPortInUse } from '../ports.js';\nimport { readRegistry } from '../registry.js';\n\nexport interface ListOptions {\n cfg: WorktreeConfig;\n repoRoot: string;\n}\n\nexport async function list({ cfg, repoRoot }: ListOptions): Promise<void> {\n const reg = await readRegistry(cfg);\n\n console.log(`Worktrees for ${cfg.directoryPrefix}:`);\n console.log('');\n\n const headers = ['BRANCH', ...cfg.services.map((s) => s.name.toUpperCase()), 'STATUS'];\n const widths = [40, ...cfg.services.map(() => 10), 10];\n printRow(headers, widths);\n printRow(\n headers.map((h) => '-'.repeat(Math.min(h.length, 6))),\n widths,\n );\n\n // Main worktree row\n printRow(\n [\n 'main (default)',\n ...cfg.services.map((s) => String(s.mainPort)),\n statusOf(cfg.services, (s) => s.mainPort, /* dirExists */ true),\n ],\n widths,\n );\n\n // Feature worktree rows\n for (const [branch, entry] of Object.entries(reg)) {\n const wtPath = worktreePath(cfg, repoRoot, branch);\n const dirExists = existsSync(wtPath);\n const ports = cfg.services.map((s) => entry.ports[s.name]!);\n printRow(\n [\n branch,\n ...ports.map((p) => String(p)),\n dirExists ? statusOf(cfg.services, (s) => entry.ports[s.name]!, true) : 'missing',\n ],\n widths,\n );\n }\n\n console.log('');\n console.log(`Registry: ${cfg.portRegistry}`);\n}\n\nfunction statusOf(\n services: ServiceConfig[],\n portOf: (s: ServiceConfig) => number,\n dirExists: boolean,\n): string {\n if (!dirExists) return 'missing';\n const anyRunning = services.some((s) => isPortInUse(portOf(s)));\n return anyRunning ? 'running' : 'stopped';\n}\n\nfunction printRow(cells: string[], widths: number[]): void {\n const line = cells.map((c, i) => c.padEnd(widths[i] ?? 10)).join(' ');\n console.log(` ${line}`);\n}\n","import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\n\nimport chalk from 'chalk';\n\nimport { type WorktreeConfig } from '../config.js';\nimport { logFile } from '../paths.js';\n\nexport interface LogsOptions {\n branch: string;\n cfg: WorktreeConfig;\n service?: string;\n}\n\nexport async function logs({ branch, cfg, service }: LogsOptions): Promise<void> {\n const svc = service ? cfg.services.find((s) => s.name === service) : cfg.services[0];\n\n if (!svc) {\n const names = cfg.services.map((s) => s.name).join(', ');\n console.error(chalk.red(`Unknown service \"${service}\". Valid services: ${names}`));\n process.exit(1);\n }\n\n const path = logFile(svc, branch);\n if (!existsSync(path)) {\n console.error(chalk.red(`Log file not found: ${path}`));\n console.error(`Start the dev server first: precisa-worktree dev ${branch}`);\n process.exit(1);\n }\n\n // Delegate to tail -f so Ctrl-C works and rotation is handled.\n const result = spawnSync('tail', ['-f', path], { stdio: 'inherit' });\n process.exit(result.status ?? 0);\n}\n","import { execFileSync } from 'node:child_process';\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\n\nimport chalk from 'chalk';\n\nimport { type WorktreeConfig } from '../config.js';\nimport { logFile, worktreePath } from '../paths.js';\nimport { allocate } from '../registry.js';\n\nexport interface SetupOptions {\n branch: string;\n cfg: WorktreeConfig;\n repoRoot: string;\n}\n\nexport async function setup({ branch, cfg, repoRoot }: SetupOptions): Promise<void> {\n const wtPath = worktreePath(cfg, repoRoot, branch);\n\n if (existsSync(wtPath)) {\n console.error(chalk.red(`Worktree already exists: ${wtPath}`));\n console.error(`To start dev servers: precisa-worktree dev ${branch}`);\n process.exit(1);\n }\n\n console.log(chalk.bold(`==> Creating worktree for '${branch}' at ${wtPath}...`));\n\n // Fetch origin/main from the main worktree; the new worktree branches from it.\n execFileSync('git', ['-C', repoRoot, 'fetch', 'origin', 'main'], { stdio: 'inherit' });\n execFileSync('git', ['-C', repoRoot, 'worktree', 'add', '-b', branch, wtPath, 'origin/main'], {\n stdio: 'inherit',\n });\n\n const entry = await allocate(cfg, branch);\n\n if (cfg.install !== false) {\n console.log(chalk.bold('==> Installing dependencies (pnpm install)...'));\n execFileSync('pnpm', ['install'], { cwd: wtPath, stdio: 'inherit' });\n }\n\n if (cfg.buildCommand) {\n console.log(chalk.bold(`==> Running build: ${cfg.buildCommand}`));\n execFileSync('sh', ['-c', cfg.buildCommand], { cwd: wtPath, stdio: 'inherit' });\n }\n\n // Write per-repo files with the allocated ports substituted in. Used\n // by platform to seed apps/*/.env.local so the running dev server\n // picks up the right ports without extra env-var plumbing.\n if (cfg.writeFiles?.length) {\n console.log(chalk.bold('==> Writing worktree-scoped files'));\n for (const { contents, path } of cfg.writeFiles) {\n let interpolated = contents;\n for (const [svcName, port] of Object.entries(entry.ports)) {\n interpolated = interpolated\n .replaceAll(`{${svcName}_port}`, String(port))\n .replaceAll('{port}', String(port));\n }\n const fullPath = resolve(wtPath, path);\n mkdirSync(dirname(fullPath), { recursive: true });\n writeFileSync(fullPath, interpolated);\n console.log(` wrote ${path}`);\n }\n }\n\n console.log('');\n console.log(chalk.green(`Worktree ready: ${wtPath}`));\n console.log(` Branch: ${branch}`);\n for (const svc of cfg.services) {\n const port = entry.ports[svc.name]!;\n console.log(\n ` ${svc.name.padEnd(8)}: http://localhost:${port} (logs: ${logFile(svc, branch)})`,\n );\n }\n console.log('');\n console.log('To start the dev server(s):');\n console.log(` cd ${wtPath} && precisa-worktree dev ${branch}`);\n}\n","import chalk from 'chalk';\n\nimport { type WorktreeConfig } from '../config.js';\nimport { isPortInUse, killPort } from '../ports.js';\nimport { getBranchEntry, portFor } from '../registry.js';\n\nexport interface StopOptions {\n branch: string;\n cfg: WorktreeConfig;\n}\n\nexport async function stop({ branch, cfg }: StopOptions): Promise<void> {\n const entry = await getBranchEntry(cfg, branch);\n if (!entry) {\n console.error(`No ports registered for '${branch}'.`);\n process.exit(1);\n }\n\n let anyRunning = false;\n for (const svc of cfg.services) {\n const port = portFor(entry, svc);\n if (isPortInUse(port)) {\n console.log(chalk.bold(`Killing ${svc.name} dev server on port ${port}...`));\n await killPort(port);\n anyRunning = true;\n }\n }\n\n if (!anyRunning) {\n console.log(`No dev servers running for '${branch}'.`);\n }\n}\n","import { execFileSync } from 'node:child_process';\nimport { existsSync, rmSync } from 'node:fs';\n\nimport chalk from 'chalk';\n\nimport { type WorktreeConfig } from '../config.js';\nimport { logFile, worktreePath } from '../paths.js';\nimport { isPortInUse, killPort } from '../ports.js';\nimport { free, getBranchEntry, portFor } from '../registry.js';\n\nexport interface TeardownOptions {\n branch: string;\n cfg: WorktreeConfig;\n keepBranch?: boolean;\n repoRoot: string;\n}\n\nexport async function teardown({\n branch,\n cfg,\n keepBranch = false,\n repoRoot,\n}: TeardownOptions): Promise<void> {\n const entry = await getBranchEntry(cfg, branch);\n const wtPath = worktreePath(cfg, repoRoot, branch);\n\n if (entry) {\n for (const svc of cfg.services) {\n const port = portFor(entry, svc);\n if (isPortInUse(port)) {\n console.log(chalk.bold(`Stopping ${svc.name} dev server on port ${port}...`));\n await killPort(port);\n }\n }\n }\n\n if (existsSync(wtPath)) {\n console.log(chalk.bold(`Removing worktree: ${wtPath}`));\n execFileSync('git', ['-C', repoRoot, 'worktree', 'remove', wtPath, '--force'], {\n stdio: 'inherit',\n });\n }\n\n if (keepBranch) {\n console.log(`Preserving local branch: ${branch}`);\n } else {\n const branchExists = branchRefExists(repoRoot, branch);\n if (branchExists) {\n console.log(chalk.bold(`Deleting local branch: ${branch}`));\n try {\n execFileSync('git', ['-C', repoRoot, 'branch', '-d', branch], {\n stdio: 'ignore',\n });\n } catch {\n // Not fully merged — force-delete since worktree was already removed.\n execFileSync('git', ['-C', repoRoot, 'branch', '-D', branch], {\n stdio: 'inherit',\n });\n }\n }\n }\n\n await free(cfg, branch);\n\n for (const svc of cfg.services) {\n const path = logFile(svc, branch);\n if (existsSync(path)) rmSync(path);\n }\n\n console.log(chalk.green(`Teardown complete for '${branch}'.`));\n}\n\nfunction branchRefExists(repoRoot: string, branch: string): boolean {\n try {\n execFileSync(\n 'git',\n ['-C', repoRoot, 'show-ref', '--verify', '--quiet', `refs/heads/${branch}`],\n {\n stdio: 'ignore',\n },\n );\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["rmdirSync","chalk","existsSync","resolve","execSync","readFile","sleep","sleep","readFile","existsSync","existsSync","existsSync","existsSync","chalk","chalk","existsSync","existsSync","mkdirSync","dirname","resolve","chalk","existsSync","chalk","resolve","mkdirSync","dirname","chalk","chalk","execFileSync","existsSync","chalk","chalk","existsSync","execFileSync","chalk","rmdirSync"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
interface ServiceConfig {
|
|
2
|
+
/**
|
|
3
|
+
* Extra args passed to the dev script. `{port}` is replaced with the
|
|
4
|
+
* allocated port. Defaults to `['--port', '{port}']` (works with vite).
|
|
5
|
+
*/
|
|
6
|
+
devArgs?: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Optional: additional env vars to set for the dev process. The literal
|
|
9
|
+
* `{port}` is substituted. Example: `{ VITE_API_URL: 'http://localhost:{port}/api' }`.
|
|
10
|
+
*/
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* First port for feature worktrees. Slot 1 uses this port; slot 2 uses
|
|
14
|
+
* `featureBase + increment`, slot 3 uses `featureBase + 2*increment`, etc.
|
|
15
|
+
*/
|
|
16
|
+
featureBase: number;
|
|
17
|
+
/** Increment between slots. Defaults to 10. */
|
|
18
|
+
increment?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Prefix for the log file path: `/tmp/<logPrefix>-<branch>.log`. Required
|
|
21
|
+
* so that sibling services in a repo don't overwrite each other's logs.
|
|
22
|
+
*/
|
|
23
|
+
logPrefix: string;
|
|
24
|
+
/** Port used by the default (main) worktree. */
|
|
25
|
+
mainPort: number;
|
|
26
|
+
/** Service name used in CLI output, column headers, and log file naming. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** pnpm filter pattern for `worktree dev`. Example: `@medbench-brasil/site`. */
|
|
29
|
+
pnpmFilter: string;
|
|
30
|
+
}
|
|
31
|
+
interface WorktreeConfig {
|
|
32
|
+
/**
|
|
33
|
+
* Optional extra build command run during `setup`, after `pnpm install`.
|
|
34
|
+
* Example: `pnpm turbo run build --filter=./packages/*`.
|
|
35
|
+
*/
|
|
36
|
+
buildCommand?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Prefix used for worktree directory names: `<prefix>-<branch>`.
|
|
39
|
+
* Example: `medbench-brasil` → worktrees land at
|
|
40
|
+
* `../medbench-brasil-feat-foo`.
|
|
41
|
+
*/
|
|
42
|
+
directoryPrefix: string;
|
|
43
|
+
/**
|
|
44
|
+
* Whether to run `pnpm install` during setup. Defaults to true. Set to
|
|
45
|
+
* false for repos where install should be manual or where the script
|
|
46
|
+
* is invoked inside a container.
|
|
47
|
+
*/
|
|
48
|
+
install?: boolean;
|
|
49
|
+
/** Absolute path to the lock directory. Defaults to `<portRegistry>.lock.d`. */
|
|
50
|
+
portLockDir?: string;
|
|
51
|
+
/** Absolute path to the port registry JSON file. */
|
|
52
|
+
portRegistry: string;
|
|
53
|
+
/** Services managed by this repo. At least one required. */
|
|
54
|
+
services: ServiceConfig[];
|
|
55
|
+
/**
|
|
56
|
+
* Extra per-service file writes performed during `setup`, after
|
|
57
|
+
* install + build. Intended for repos that need `.env.local`-style
|
|
58
|
+
* files seeded with the allocated ports so that running `dev` without
|
|
59
|
+
* explicit env vars picks up the right values.
|
|
60
|
+
*
|
|
61
|
+
* Each entry is `{ path, contents }` where `contents` supports the
|
|
62
|
+
* same `{port}` and `{<service>_port}` substitutions available in
|
|
63
|
+
* `service.env` / `service.devArgs`.
|
|
64
|
+
*
|
|
65
|
+
* Example (platform):
|
|
66
|
+
* writeFiles: [
|
|
67
|
+
* { path: 'apps/web/.env.local',
|
|
68
|
+
* contents: 'VITE_DEV_PORT={web_port}\nVITE_API_URL=http://localhost:{api_port}/api\n' },
|
|
69
|
+
* { path: 'apps/api/.env.local',
|
|
70
|
+
* contents: 'PORT={api_port}\n' },
|
|
71
|
+
* ]
|
|
72
|
+
*/
|
|
73
|
+
writeFiles?: Array<{
|
|
74
|
+
path: string;
|
|
75
|
+
contents: string;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
declare function loadConfig(repoRoot: string): Promise<WorktreeConfig>;
|
|
79
|
+
|
|
80
|
+
export { type ServiceConfig, type WorktreeConfig, loadConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
async function loadConfig(repoRoot) {
|
|
5
|
+
const pkgPath = resolve(repoRoot, "package.json");
|
|
6
|
+
let pkg;
|
|
7
|
+
try {
|
|
8
|
+
pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
9
|
+
} catch (err) {
|
|
10
|
+
throw new Error(`Failed to read ${pkgPath}: ${err.message}. Is this the repo root?`);
|
|
11
|
+
}
|
|
12
|
+
const cfg = pkg.worktree;
|
|
13
|
+
if (!cfg) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`No "worktree" field in ${pkgPath}. Add a worktree config \u2014 see https://github.com/Precisa-Saude/tooling/tree/main/packages/worktree-cli#configuration`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (!cfg.directoryPrefix) {
|
|
19
|
+
throw new Error("worktree.directoryPrefix is required");
|
|
20
|
+
}
|
|
21
|
+
if (!cfg.portRegistry) {
|
|
22
|
+
throw new Error("worktree.portRegistry is required");
|
|
23
|
+
}
|
|
24
|
+
if (!cfg.services?.length) {
|
|
25
|
+
throw new Error("worktree.services must contain at least one entry");
|
|
26
|
+
}
|
|
27
|
+
for (const svc of cfg.services) {
|
|
28
|
+
if (!svc.name) throw new Error("service.name is required");
|
|
29
|
+
if (!svc.pnpmFilter) throw new Error(`service.${svc.name}.pnpmFilter is required`);
|
|
30
|
+
if (!svc.logPrefix) throw new Error(`service.${svc.name}.logPrefix is required`);
|
|
31
|
+
if (typeof svc.mainPort !== "number") {
|
|
32
|
+
throw new Error(`service.${svc.name}.mainPort must be a number`);
|
|
33
|
+
}
|
|
34
|
+
if (typeof svc.featureBase !== "number") {
|
|
35
|
+
throw new Error(`service.${svc.name}.featureBase must be a number`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return cfg;
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
loadConfig
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nexport interface ServiceConfig {\n /**\n * Extra args passed to the dev script. `{port}` is replaced with the\n * allocated port. Defaults to `['--port', '{port}']` (works with vite).\n */\n devArgs?: string[];\n /**\n * Optional: additional env vars to set for the dev process. The literal\n * `{port}` is substituted. Example: `{ VITE_API_URL: 'http://localhost:{port}/api' }`.\n */\n env?: Record<string, string>;\n /**\n * First port for feature worktrees. Slot 1 uses this port; slot 2 uses\n * `featureBase + increment`, slot 3 uses `featureBase + 2*increment`, etc.\n */\n featureBase: number;\n /** Increment between slots. Defaults to 10. */\n increment?: number;\n /**\n * Prefix for the log file path: `/tmp/<logPrefix>-<branch>.log`. Required\n * so that sibling services in a repo don't overwrite each other's logs.\n */\n logPrefix: string;\n /** Port used by the default (main) worktree. */\n mainPort: number;\n /** Service name used in CLI output, column headers, and log file naming. */\n name: string;\n /** pnpm filter pattern for `worktree dev`. Example: `@medbench-brasil/site`. */\n pnpmFilter: string;\n}\n\nexport interface WorktreeConfig {\n /**\n * Optional extra build command run during `setup`, after `pnpm install`.\n * Example: `pnpm turbo run build --filter=./packages/*`.\n */\n buildCommand?: string;\n /**\n * Prefix used for worktree directory names: `<prefix>-<branch>`.\n * Example: `medbench-brasil` → worktrees land at\n * `../medbench-brasil-feat-foo`.\n */\n directoryPrefix: string;\n /**\n * Whether to run `pnpm install` during setup. Defaults to true. Set to\n * false for repos where install should be manual or where the script\n * is invoked inside a container.\n */\n install?: boolean;\n /** Absolute path to the lock directory. Defaults to `<portRegistry>.lock.d`. */\n portLockDir?: string;\n /** Absolute path to the port registry JSON file. */\n portRegistry: string;\n /** Services managed by this repo. At least one required. */\n services: ServiceConfig[];\n /**\n * Extra per-service file writes performed during `setup`, after\n * install + build. Intended for repos that need `.env.local`-style\n * files seeded with the allocated ports so that running `dev` without\n * explicit env vars picks up the right values.\n *\n * Each entry is `{ path, contents }` where `contents` supports the\n * same `{port}` and `{<service>_port}` substitutions available in\n * `service.env` / `service.devArgs`.\n *\n * Example (platform):\n * writeFiles: [\n * { path: 'apps/web/.env.local',\n * contents: 'VITE_DEV_PORT={web_port}\\nVITE_API_URL=http://localhost:{api_port}/api\\n' },\n * { path: 'apps/api/.env.local',\n * contents: 'PORT={api_port}\\n' },\n * ]\n */\n writeFiles?: Array<{ path: string; contents: string }>;\n}\n\ninterface PackageJson {\n name?: string;\n worktree?: WorktreeConfig;\n}\n\nexport async function loadConfig(repoRoot: string): Promise<WorktreeConfig> {\n const pkgPath = resolve(repoRoot, 'package.json');\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(await readFile(pkgPath, 'utf-8')) as PackageJson;\n } catch (err) {\n throw new Error(`Failed to read ${pkgPath}: ${(err as Error).message}. Is this the repo root?`);\n }\n\n const cfg = pkg.worktree;\n if (!cfg) {\n throw new Error(\n `No \"worktree\" field in ${pkgPath}. Add a worktree config — see ` +\n `https://github.com/Precisa-Saude/tooling/tree/main/packages/worktree-cli#configuration`,\n );\n }\n\n if (!cfg.directoryPrefix) {\n throw new Error('worktree.directoryPrefix is required');\n }\n if (!cfg.portRegistry) {\n throw new Error('worktree.portRegistry is required');\n }\n if (!cfg.services?.length) {\n throw new Error('worktree.services must contain at least one entry');\n }\n for (const svc of cfg.services) {\n if (!svc.name) throw new Error('service.name is required');\n if (!svc.pnpmFilter) throw new Error(`service.${svc.name}.pnpmFilter is required`);\n if (!svc.logPrefix) throw new Error(`service.${svc.name}.logPrefix is required`);\n if (typeof svc.mainPort !== 'number') {\n throw new Error(`service.${svc.name}.mainPort must be a number`);\n }\n if (typeof svc.featureBase !== 'number') {\n throw new Error(`service.${svc.name}.featureBase must be a number`);\n }\n }\n\n return cfg;\n}\n\nexport function lockDir(cfg: WorktreeConfig): string {\n return cfg.portLockDir ?? `${cfg.portRegistry}.lock.d`;\n}\n\nexport function increment(svc: ServiceConfig): number {\n return svc.increment ?? 10;\n}\n\nexport function devArgs(svc: ServiceConfig): string[] {\n return svc.devArgs ?? ['--port', '{port}'];\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AACzB,SAAS,eAAe;AAmFxB,eAAsB,WAAW,UAA2C;AAC1E,QAAM,UAAU,QAAQ,UAAU,cAAc;AAChD,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,MAAM,SAAS,SAAS,OAAO,CAAC;AAAA,EACnD,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,kBAAkB,OAAO,KAAM,IAAc,OAAO,0BAA0B;AAAA,EAChG;AAEA,QAAM,MAAM,IAAI;AAChB,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR,0BAA0B,OAAO;AAAA,IAEnC;AAAA,EACF;AAEA,MAAI,CAAC,IAAI,iBAAiB;AACxB,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AACA,MAAI,CAAC,IAAI,cAAc;AACrB,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AACA,MAAI,CAAC,IAAI,UAAU,QAAQ;AACzB,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AACA,aAAW,OAAO,IAAI,UAAU;AAC9B,QAAI,CAAC,IAAI,KAAM,OAAM,IAAI,MAAM,0BAA0B;AACzD,QAAI,CAAC,IAAI,WAAY,OAAM,IAAI,MAAM,WAAW,IAAI,IAAI,yBAAyB;AACjF,QAAI,CAAC,IAAI,UAAW,OAAM,IAAI,MAAM,WAAW,IAAI,IAAI,wBAAwB;AAC/E,QAAI,OAAO,IAAI,aAAa,UAAU;AACpC,YAAM,IAAI,MAAM,WAAW,IAAI,IAAI,4BAA4B;AAAA,IACjE;AACA,QAAI,OAAO,IAAI,gBAAgB,UAAU;AACvC,YAAM,IAAI,MAAM,WAAW,IAAI,IAAI,+BAA+B;AAAA,IACpE;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@precisa-saude/worktree-cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Shared git-worktree lifecycle CLI for Precisa Saúde repositories — creates/dev-runs/tears-down feature worktrees with per-repo port allocation. Drop-in replacement for each repo's `scripts/worktree.sh`.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git-worktree",
|
|
7
|
+
"worktree",
|
|
8
|
+
"monorepo",
|
|
9
|
+
"precisa-saude"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/Precisa-Saude/tooling.git",
|
|
14
|
+
"directory": "packages/worktree-cli"
|
|
15
|
+
},
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "Precisa Saúde",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"bin": {
|
|
28
|
+
"precisa-worktree": "./dist/bin.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^5.3.0",
|
|
36
|
+
"commander": "^12.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.10.0",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "~5.7.3",
|
|
42
|
+
"@precisa-saude/tsconfig": "1.1.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"clean": "rm -rf dist .turbo",
|
|
53
|
+
"dev": "tsup --watch",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|