@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 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
@@ -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"]}
@@ -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
+ }