@helmsmith/workbrokr 0.2.0 → 0.3.1
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/SKILL.md +12 -0
- package/dist/cli.js +529 -94
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +98 -21
- package/dist/core/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -61,7 +61,8 @@ var REFUSALS = /* @__PURE__ */ new Set([
|
|
|
61
61
|
"PORT_RANGE_TAKEN",
|
|
62
62
|
"SYNC_CONFLICT",
|
|
63
63
|
"DIRTY_WORKTREE",
|
|
64
|
-
"UNPUSHED"
|
|
64
|
+
"UNPUSHED",
|
|
65
|
+
"PR_CLOSED"
|
|
65
66
|
]);
|
|
66
67
|
function exitCodeFor(code) {
|
|
67
68
|
return REFUSALS.has(code) ? 2 : 1;
|
|
@@ -164,12 +165,43 @@ var Git = class _Git {
|
|
|
164
165
|
else if (this.remoteBranchExists(this.currentBranch())) ref = `origin/${this.currentBranch()}`;
|
|
165
166
|
return this.lines(["rev-list", `${ref}..HEAD`]).length;
|
|
166
167
|
}
|
|
168
|
+
/** Non-merge commits on HEAD that origin/<base> does not have: the branch's own work (sync merges do not count). */
|
|
169
|
+
ownCommitCount(base) {
|
|
170
|
+
const r = this.tryRun(["rev-list", "--count", "--no-merges", `origin/${base}..HEAD`]);
|
|
171
|
+
return r.code === 0 ? Number(r.out.trim()) || 0 : 0;
|
|
172
|
+
}
|
|
173
|
+
/** `branch` is already in origin/<base>: an ancestor of it, patch-equivalent (squash-merged), or without own commits. */
|
|
174
|
+
branchMergedInto(branch, base) {
|
|
175
|
+
const target = `origin/${base}`;
|
|
176
|
+
if (this.isAncestor(branch, target)) return true;
|
|
177
|
+
const own = this.tryRun(["rev-list", "--count", "--no-merges", `${target}..${branch}`]);
|
|
178
|
+
if (own.code === 0 && Number(own.out.trim()) === 0) return true;
|
|
179
|
+
const cherry = this.tryRun(["cherry", target, branch]);
|
|
180
|
+
if (cherry.code !== 0) return false;
|
|
181
|
+
const lines = cherry.out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
182
|
+
return lines.length > 0 && lines.every((l) => l.startsWith("- "));
|
|
183
|
+
}
|
|
184
|
+
/** Commits on origin/<base> that HEAD does not have (0 when the ref is unknown). */
|
|
185
|
+
behindCount(base) {
|
|
186
|
+
const r = this.tryRun(["rev-list", "--count", `HEAD..origin/${base}`]);
|
|
187
|
+
return r.code === 0 ? Number(r.out.trim()) || 0 : 0;
|
|
188
|
+
}
|
|
167
189
|
isDirty() {
|
|
168
190
|
return this.run(["status", "--porcelain"]).trim().length > 0;
|
|
169
191
|
}
|
|
170
192
|
changedPathsStaged() {
|
|
171
193
|
return this.lines(["diff", "--cached", "--name-only"]);
|
|
172
194
|
}
|
|
195
|
+
/** The other parent of a merge in progress, or undefined when none is. */
|
|
196
|
+
mergeHead() {
|
|
197
|
+
const r = this.tryRun(["rev-parse", "-q", "--verify", "MERGE_HEAD"]);
|
|
198
|
+
return r.code === 0 ? r.out.trim() : void 0;
|
|
199
|
+
}
|
|
200
|
+
/** Staged paths whose index content differs from `ref` (so, authored here rather than taken from `ref`). */
|
|
201
|
+
stagedDifferingFrom(ref, paths) {
|
|
202
|
+
if (paths.length === 0) return [];
|
|
203
|
+
return this.lines(["diff", "--cached", "--name-only", "--no-renames", ref, "--", ...paths]);
|
|
204
|
+
}
|
|
173
205
|
changedPaths(from, to) {
|
|
174
206
|
return this.lines(["diff", "--name-only", "--no-renames", from, to]);
|
|
175
207
|
}
|
|
@@ -202,18 +234,45 @@ var Git = class _Git {
|
|
|
202
234
|
const conflicted = this.lines(["diff", "--name-only", "--diff-filter=U"]);
|
|
203
235
|
return { kind: "conflict", before, after: before, conflicted };
|
|
204
236
|
}
|
|
237
|
+
/** `origin/<branch>` after a fetch attempt, when the remote has it; the local copy may have been retired. */
|
|
238
|
+
remoteRef(branch) {
|
|
239
|
+
this.tryRun(["fetch", "-q", "origin", branch]);
|
|
240
|
+
const ref = `refs/remotes/origin/${branch}`;
|
|
241
|
+
return this.tryRun(["rev-parse", "--verify", "-q", ref]).code === 0 ? `origin/${branch}` : void 0;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Add a worktree on `branch`: the local branch if it exists, else a tracking
|
|
245
|
+
* branch from origin/<branch> if the remote has it (so retiring the local
|
|
246
|
+
* branch while its PR was open never recreates it from the base), else a
|
|
247
|
+
* new branch from `startPoint`.
|
|
248
|
+
*/
|
|
205
249
|
worktreeAdd(path, branch, startPoint) {
|
|
206
250
|
mkdirSync(dirname(path), { recursive: true });
|
|
207
|
-
if (this.branchExists(branch))
|
|
208
|
-
|
|
209
|
-
|
|
251
|
+
if (this.branchExists(branch)) {
|
|
252
|
+
this.run(["worktree", "add", "-q", path, branch]);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const remote = this.remoteRef(branch);
|
|
256
|
+
if (remote) {
|
|
257
|
+
this.run(["worktree", "add", "-q", "--track", "-b", branch, path, remote]);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.run(["worktree", "add", "-q", "--no-track", "-b", branch, path, startPoint ?? "HEAD"]);
|
|
210
261
|
}
|
|
211
262
|
worktreeRemove(path) {
|
|
212
263
|
this.run(["worktree", "remove", "--force", path]);
|
|
213
264
|
}
|
|
214
265
|
checkout(branch, createFrom) {
|
|
215
|
-
if (this.branchExists(branch))
|
|
216
|
-
|
|
266
|
+
if (this.branchExists(branch)) {
|
|
267
|
+
this.run(["checkout", "-q", branch]);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const remote = this.remoteRef(branch);
|
|
271
|
+
if (remote) {
|
|
272
|
+
this.run(["checkout", "-q", "--track", "-b", branch, remote]);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.run(["checkout", "-q", "--no-track", "-b", branch, createFrom ?? "HEAD"]);
|
|
217
276
|
}
|
|
218
277
|
push(branch) {
|
|
219
278
|
this.run(["push", "-q", "-u", "origin", branch]);
|
|
@@ -327,13 +386,14 @@ function slotVars(m, o) {
|
|
|
327
386
|
BRANCH: o.branch ?? "",
|
|
328
387
|
WORKTREE: o.worktree ?? "",
|
|
329
388
|
LOG_DIR: o.logDir ?? "",
|
|
330
|
-
PORT_STORYBOOK: o.storybookPort === void 0 ? "" : String(o.storybookPort),
|
|
331
389
|
STORYBROKR_HOST: storybookHostFor(m, o.worktree)
|
|
332
390
|
};
|
|
333
391
|
for (const service of Object.keys(m.ports.services)) {
|
|
334
392
|
const p = o.ports[service];
|
|
335
393
|
vars[`PORT_${service.toUpperCase()}`] = p === void 0 ? "" : String(p);
|
|
336
394
|
}
|
|
395
|
+
if (o.storybookPort !== void 0) vars.PORT_STORYBOOK = String(o.storybookPort);
|
|
396
|
+
else if (vars.PORT_STORYBOOK === void 0) vars.PORT_STORYBOOK = "";
|
|
337
397
|
return vars;
|
|
338
398
|
}
|
|
339
399
|
function ensureExcluded(worktree, patterns) {
|
|
@@ -558,8 +618,15 @@ function checkCommit(c) {
|
|
|
558
618
|
`branch "${c.branch}" is protected (${c.m.protectedBranches.join(", ")}): work happens on <type>/<area> branches, never here`
|
|
559
619
|
);
|
|
560
620
|
}
|
|
621
|
+
const mergeHead = c.git.mergeHead();
|
|
622
|
+
const authored = mergeHead ? new Set(c.git.stagedDifferingFrom(mergeHead, c.stagedPaths)) : new Set(c.stagedPaths);
|
|
623
|
+
const ownPaths = c.stagedPaths.filter((p) => authored.has(p));
|
|
561
624
|
const allEfforts = c.registry.efforts({ project: c.project });
|
|
562
625
|
const efforts = new Map(allEfforts.map((e) => [e.id, e]));
|
|
626
|
+
const openEfforts = allEfforts.filter((e) => e.state === "active" || e.state === "starting");
|
|
627
|
+
const mine = openEfforts.find(
|
|
628
|
+
(e) => c.checkoutRoot && existsSync3(e.worktree) ? same(e.worktree, c.checkoutRoot) : e.branch === c.branch
|
|
629
|
+
);
|
|
563
630
|
for (const lease of c.registry.leases({ project: c.project, kind: "commons" })) {
|
|
564
631
|
const holder = efforts.get(lease.effort);
|
|
565
632
|
const holderHasWorktree = Boolean(holder && existsSync3(holder.worktree));
|
|
@@ -568,34 +635,31 @@ function checkCommit(c) {
|
|
|
568
635
|
} else if (lease.owner.session === c.ownerSession) {
|
|
569
636
|
continue;
|
|
570
637
|
}
|
|
638
|
+
if (holder && mine && holder.area === mine.area) continue;
|
|
571
639
|
const globs = c.m.commons[String(lease.value)] ?? [];
|
|
572
|
-
const hit = pathsMatching(
|
|
640
|
+
const hit = pathsMatching(ownPaths, globs);
|
|
573
641
|
if (hit.length > 0) {
|
|
574
642
|
refusals.push(
|
|
575
643
|
`commons "${lease.value}" is write-locked by ${lease.owner.session} (effort ${lease.effort}, since ${lease.touched}); staged: ${hit.join(", ")}`
|
|
576
644
|
);
|
|
577
645
|
}
|
|
578
646
|
}
|
|
579
|
-
const openEfforts = allEfforts.filter((e) => e.state === "active" || e.state === "starting");
|
|
580
647
|
for (const other of openEfforts) {
|
|
581
648
|
if (c.checkoutRoot && existsSync3(other.worktree) && same(other.worktree, c.checkoutRoot)) {
|
|
582
649
|
continue;
|
|
583
650
|
}
|
|
584
651
|
if (!c.checkoutRoot && other.branch === c.branch) continue;
|
|
585
|
-
const hit = pathsMatching(
|
|
652
|
+
const hit = pathsMatching(ownPaths, other.scopes);
|
|
586
653
|
if (hit.length > 0) {
|
|
587
654
|
refusals.push(
|
|
588
655
|
`path(s) ${hit.join(", ")} are inside the active scope of ${effortLabel(other)} (${other.branch}, owned by ${other.owner.session})`
|
|
589
656
|
);
|
|
590
657
|
}
|
|
591
658
|
}
|
|
592
|
-
|
|
593
|
-
(e) => c.checkoutRoot && existsSync3(e.worktree) ? same(e.worktree, c.checkoutRoot) : e.branch === c.branch
|
|
594
|
-
);
|
|
595
|
-
if (mine && c.stagedPaths.length > 0) {
|
|
659
|
+
if (mine && ownPaths.length > 0) {
|
|
596
660
|
const allowed = allowedScopesForEffort(c.m, mine);
|
|
597
|
-
const inScope = new Set(pathsMatching(
|
|
598
|
-
const outside =
|
|
661
|
+
const inScope = new Set(pathsMatching(ownPaths, allowed));
|
|
662
|
+
const outside = ownPaths.filter((p) => !inScope.has(p));
|
|
599
663
|
if (outside.length > 0) {
|
|
600
664
|
warnings.push(
|
|
601
665
|
`${outside.length} staged path(s) are outside the effort's scope (${effortLabel(mine)}): ${outside.join(", ")}`
|
|
@@ -922,8 +986,19 @@ function rangesFor(m) {
|
|
|
922
986
|
function rangesOverlap(a, b) {
|
|
923
987
|
return a.base < b.base + b.count && b.base < a.base + a.count;
|
|
924
988
|
}
|
|
989
|
+
function ensureRanges(registry, m, repoRoot) {
|
|
990
|
+
const mine = rangesFor(m);
|
|
991
|
+
assertNoOverlap(registry, m, mine);
|
|
992
|
+
const current = registry.project(m.project);
|
|
993
|
+
if (current && JSON.stringify(current.ranges) === JSON.stringify(mine)) return;
|
|
994
|
+
registry.upsertProject(m.project, { root: repoRoot, ranges: mine });
|
|
995
|
+
}
|
|
925
996
|
function claimRanges(registry, m, repoRoot) {
|
|
926
997
|
const mine = rangesFor(m);
|
|
998
|
+
assertNoOverlap(registry, m, mine);
|
|
999
|
+
registry.upsertProject(m.project, { root: repoRoot, ranges: mine });
|
|
1000
|
+
}
|
|
1001
|
+
function assertNoOverlap(registry, m, mine) {
|
|
927
1002
|
const others = Object.entries(registry.read().projects).filter(([name]) => name !== m.project);
|
|
928
1003
|
for (const [service, range] of Object.entries(mine)) {
|
|
929
1004
|
for (const [name, entry] of others) {
|
|
@@ -938,7 +1013,6 @@ function claimRanges(registry, m, repoRoot) {
|
|
|
938
1013
|
}
|
|
939
1014
|
}
|
|
940
1015
|
}
|
|
941
|
-
registry.upsertProject(m.project, { root: repoRoot, ranges: mine });
|
|
942
1016
|
}
|
|
943
1017
|
|
|
944
1018
|
// src/core/refresh.ts
|
|
@@ -1070,8 +1144,8 @@ import {
|
|
|
1070
1144
|
} from "fs";
|
|
1071
1145
|
var newId = (prefix) => `${prefix}_${randomBytes(4).toString("hex")}`;
|
|
1072
1146
|
var EMPTY = { version: 1, projects: {}, leases: [], efforts: [] };
|
|
1073
|
-
var LOCK_STALE_MS =
|
|
1074
|
-
var LOCK_WAIT_MS =
|
|
1147
|
+
var LOCK_STALE_MS = 1e4;
|
|
1148
|
+
var LOCK_WAIT_MS = 15e3;
|
|
1075
1149
|
function sleep(ms) {
|
|
1076
1150
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1077
1151
|
}
|
|
@@ -1127,7 +1201,9 @@ var Registry = class _Registry {
|
|
|
1127
1201
|
} catch {
|
|
1128
1202
|
try {
|
|
1129
1203
|
if (Date.now() - statSync2(lock).mtimeMs > LOCK_STALE_MS) {
|
|
1130
|
-
|
|
1204
|
+
const claimed = `${lock}.stale.${process.pid}.${Date.now()}`;
|
|
1205
|
+
renameSync(lock, claimed);
|
|
1206
|
+
rmSync2(claimed, { recursive: true, force: true });
|
|
1131
1207
|
continue;
|
|
1132
1208
|
}
|
|
1133
1209
|
} catch {
|
|
@@ -1237,7 +1313,7 @@ var Registry = class _Registry {
|
|
|
1237
1313
|
if (slot < 0) {
|
|
1238
1314
|
throw new WorkkitError(
|
|
1239
1315
|
"NO_FREE_SLOT",
|
|
1240
|
-
`no free slot in ${project} (0..${m.slots.count - 1}, reserved ${[...reserved].join(",")})`
|
|
1316
|
+
`no free slot in ${project} (0..${m.slots.count - 1}, reserved ${[...reserved].join(",")}): \`work status\` lists the efforts holding them; \`work finish\` or \`work reap\` frees one`
|
|
1241
1317
|
);
|
|
1242
1318
|
}
|
|
1243
1319
|
const lease = this.lease(
|
|
@@ -1480,10 +1556,14 @@ function currentOwner(env = process.env) {
|
|
|
1480
1556
|
return { session: named ?? `${userInfo().username}@${hostname()}`, pid: process.pid };
|
|
1481
1557
|
}
|
|
1482
1558
|
|
|
1559
|
+
// src/lib/pritty.ts
|
|
1560
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
1561
|
+
|
|
1483
1562
|
// src/lib/storybrokr.ts
|
|
1484
1563
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
1485
1564
|
var MIN_STORYBROKR_VERSION = "0.3.0";
|
|
1486
1565
|
var versionChecked = false;
|
|
1566
|
+
var versionFound = "";
|
|
1487
1567
|
function compareSemver(a, b) {
|
|
1488
1568
|
const pa = a.trim().replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
1489
1569
|
const pb = b.trim().replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -1514,12 +1594,14 @@ function assertStorybrokrVersion() {
|
|
|
1514
1594
|
);
|
|
1515
1595
|
}
|
|
1516
1596
|
versionChecked = true;
|
|
1597
|
+
versionFound = found;
|
|
1517
1598
|
}
|
|
1518
1599
|
var storybrokrCli = {
|
|
1519
1600
|
async up(story, hostRoot, opts) {
|
|
1520
1601
|
assertStorybrokrVersion();
|
|
1521
1602
|
const args = ["up", story, "--host", hostRoot, "--json"];
|
|
1522
1603
|
if (opts?.ttlMinutes !== void 0) args.push("--ttl", String(opts.ttlMinutes));
|
|
1604
|
+
if (opts?.includeChildren) args.push("--include-children");
|
|
1523
1605
|
const r = run(args);
|
|
1524
1606
|
if (r.code !== 0) {
|
|
1525
1607
|
throw new WorkkitError(
|
|
@@ -1536,8 +1618,26 @@ var storybrokrCli = {
|
|
|
1536
1618
|
throw new WorkkitError("STORYBROKR_FAILED", `storybrokr down ${id} failed: ${r.err.trim()}`);
|
|
1537
1619
|
},
|
|
1538
1620
|
async downHost(hostRoot) {
|
|
1539
|
-
const r = spawnSync4("storybrokr", ["down", "--host", hostRoot, "--json"], {
|
|
1540
|
-
|
|
1621
|
+
const r = spawnSync4("storybrokr", ["down", "--host", hostRoot, "--if-running", "--json"], {
|
|
1622
|
+
encoding: "utf8"
|
|
1623
|
+
});
|
|
1624
|
+
if (r.error) return [];
|
|
1625
|
+
if (r.status !== 0) {
|
|
1626
|
+
throw new WorkkitError(
|
|
1627
|
+
"STORYBROKR_FAILED",
|
|
1628
|
+
`storybrokr down --host ${hostRoot} failed: ${(r.stderr || r.stdout || "").trim()}`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
const parsed = JSON.parse(r.stdout);
|
|
1633
|
+
return parsed.stopped ?? [];
|
|
1634
|
+
} catch {
|
|
1635
|
+
return [];
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
version() {
|
|
1639
|
+
assertStorybrokrVersion();
|
|
1640
|
+
return versionFound;
|
|
1541
1641
|
},
|
|
1542
1642
|
async touch(id) {
|
|
1543
1643
|
const r = spawnSync4("storybrokr", ["touch", id], { encoding: "utf8" });
|
|
@@ -1545,6 +1645,81 @@ var storybrokrCli = {
|
|
|
1545
1645
|
}
|
|
1546
1646
|
};
|
|
1547
1647
|
|
|
1648
|
+
// src/lib/pritty.ts
|
|
1649
|
+
var MIN_PRITTY_VERSION = "0.1.2";
|
|
1650
|
+
function assertPrittyVersion(client) {
|
|
1651
|
+
const found = client.version();
|
|
1652
|
+
if (compareSemver(found, MIN_PRITTY_VERSION) < 0) {
|
|
1653
|
+
throw new WorkkitError(
|
|
1654
|
+
"PRITTY_FAILED",
|
|
1655
|
+
`pritty >= ${MIN_PRITTY_VERSION} required (found ${found || "unknown"}); npm i -g @helmsmith/pritty`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
function run2(args, cwd) {
|
|
1660
|
+
const r = spawnSync5("pritty", args, { cwd, encoding: "utf8", env: process.env });
|
|
1661
|
+
if (r.error) {
|
|
1662
|
+
throw new WorkkitError(
|
|
1663
|
+
"PRITTY_FAILED",
|
|
1664
|
+
`pritty is not installed (npm i -g @helmsmith/pritty): ${r.error.message}`
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
return { code: r.status ?? 1, out: r.stdout ?? "", err: r.stderr ?? "" };
|
|
1668
|
+
}
|
|
1669
|
+
function lastJsonLine(out, what) {
|
|
1670
|
+
const line = out.trim().split("\n").filter(Boolean).pop() ?? "";
|
|
1671
|
+
try {
|
|
1672
|
+
return JSON.parse(line);
|
|
1673
|
+
} catch {
|
|
1674
|
+
throw new WorkkitError("PRITTY_FAILED", `pritty ${what} printed no JSON: ${out.trim()}`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
var versionFound2;
|
|
1678
|
+
var prittyCli = {
|
|
1679
|
+
version() {
|
|
1680
|
+
if (versionFound2 === void 0) {
|
|
1681
|
+
const r = run2(["--version"]);
|
|
1682
|
+
versionFound2 = r.code === 0 ? r.out.trim() : "";
|
|
1683
|
+
}
|
|
1684
|
+
return versionFound2;
|
|
1685
|
+
},
|
|
1686
|
+
async pr(o) {
|
|
1687
|
+
const args = ["pr", "--base", o.base, "--no-push", "--auto-approve", "--json"];
|
|
1688
|
+
if (o.draft) args.push("--draft");
|
|
1689
|
+
if (o.title !== void 0) args.push("--title", o.title);
|
|
1690
|
+
if (o.body !== void 0) args.push("--body", o.body);
|
|
1691
|
+
if (o.bodyFile !== void 0) args.push("--body-file", o.bodyFile);
|
|
1692
|
+
const r = run2(args, o.cwd);
|
|
1693
|
+
if (r.code !== 0) {
|
|
1694
|
+
throw new WorkkitError("PRITTY_FAILED", `pritty pr failed: ${(r.err || r.out).trim()}`, {
|
|
1695
|
+
stderr: r.err
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
const j = lastJsonLine(
|
|
1699
|
+
r.out,
|
|
1700
|
+
"pr"
|
|
1701
|
+
);
|
|
1702
|
+
return { number: j.number, url: j.url, draft: Boolean(j.draft), created: Boolean(j.created) };
|
|
1703
|
+
},
|
|
1704
|
+
async check(o) {
|
|
1705
|
+
const r = run2(["pr", "--check", "--base", o.base], o.cwd);
|
|
1706
|
+
if (r.code !== 0) {
|
|
1707
|
+
throw new WorkkitError(
|
|
1708
|
+
"PRITTY_FAILED",
|
|
1709
|
+
`pritty pr --check failed: ${(r.err || r.out).trim()}`,
|
|
1710
|
+
{
|
|
1711
|
+
stderr: r.err
|
|
1712
|
+
}
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
const j = lastJsonLine(r.out, "pr --check");
|
|
1716
|
+
return {
|
|
1717
|
+
open: j.open ? { number: j.open.number, url: j.open.url, draft: Boolean(j.open.draft) } : null,
|
|
1718
|
+
last: j.last ?? null
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1548
1723
|
// src/lib/context.ts
|
|
1549
1724
|
function resolveRepoRoot(cwd, override) {
|
|
1550
1725
|
if (override) return override;
|
|
@@ -1555,7 +1730,7 @@ async function buildCtx(opts = {}) {
|
|
|
1555
1730
|
const manifest = await loadManifest(repoRoot);
|
|
1556
1731
|
const home = homeDir();
|
|
1557
1732
|
const registry = Registry.open(home);
|
|
1558
|
-
|
|
1733
|
+
ensureRanges(registry, manifest, repoRoot);
|
|
1559
1734
|
return {
|
|
1560
1735
|
home,
|
|
1561
1736
|
repoRoot,
|
|
@@ -1564,6 +1739,7 @@ async function buildCtx(opts = {}) {
|
|
|
1564
1739
|
owner: currentOwner(),
|
|
1565
1740
|
runner: { start: startServices, stop: stopServices },
|
|
1566
1741
|
storybrokr: storybrokrCli,
|
|
1742
|
+
pritty: prittyCli,
|
|
1567
1743
|
log: opts.quiet ? () => {
|
|
1568
1744
|
} : (line) => console.error(chalk.gray(line))
|
|
1569
1745
|
};
|
|
@@ -1576,9 +1752,11 @@ import {
|
|
|
1576
1752
|
existsSync as existsSync7,
|
|
1577
1753
|
mkdirSync as mkdirSync6,
|
|
1578
1754
|
readdirSync,
|
|
1755
|
+
readFileSync as readFileSync5,
|
|
1579
1756
|
realpathSync as realpathSync4,
|
|
1580
1757
|
rmSync as rmSync3,
|
|
1581
|
-
statSync as statSync3
|
|
1758
|
+
statSync as statSync3,
|
|
1759
|
+
writeFileSync as writeFileSync5
|
|
1582
1760
|
} from "fs";
|
|
1583
1761
|
import { connect, createServer } from "net";
|
|
1584
1762
|
import { dirname as dirname3, join as join8 } from "path";
|
|
@@ -1623,22 +1801,26 @@ function touchEffort(ctx, effort) {
|
|
|
1623
1801
|
}
|
|
1624
1802
|
ctx.registry.updateEffort(effort.id, { lastActivity: now() });
|
|
1625
1803
|
}
|
|
1626
|
-
function
|
|
1804
|
+
function bindFails(port, host) {
|
|
1627
1805
|
return new Promise((resolve4) => {
|
|
1628
1806
|
const srv = createServer();
|
|
1629
|
-
srv.once("error", () => resolve4(
|
|
1630
|
-
srv.listen(port,
|
|
1807
|
+
srv.once("error", (err) => resolve4(err.code === "EADDRINUSE"));
|
|
1808
|
+
srv.listen(port, host, () => srv.close(() => resolve4(false)));
|
|
1631
1809
|
});
|
|
1632
1810
|
}
|
|
1811
|
+
async function portBusy(port) {
|
|
1812
|
+
for (const host of ["127.0.0.1", "0.0.0.0", "::"]) if (await bindFails(port, host)) return true;
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1633
1815
|
async function busyPorts(ports) {
|
|
1634
1816
|
const out = [];
|
|
1635
1817
|
for (const [service, port] of Object.entries(ports))
|
|
1636
1818
|
if (await portBusy(port)) out.push(`${service}=${port}`);
|
|
1637
1819
|
return out;
|
|
1638
1820
|
}
|
|
1639
|
-
function
|
|
1821
|
+
function portAnswersOn(port, host) {
|
|
1640
1822
|
return new Promise((resolve4) => {
|
|
1641
|
-
const sock = connect({ port, host
|
|
1823
|
+
const sock = connect({ port, host });
|
|
1642
1824
|
const done = (v) => {
|
|
1643
1825
|
sock.destroy();
|
|
1644
1826
|
resolve4(v);
|
|
@@ -1648,6 +1830,9 @@ function portAnswers(port) {
|
|
|
1648
1830
|
sock.once("error", () => done(false));
|
|
1649
1831
|
});
|
|
1650
1832
|
}
|
|
1833
|
+
async function portAnswers(port) {
|
|
1834
|
+
return await portAnswersOn(port, "127.0.0.1") || await portAnswersOn(port, "::1");
|
|
1835
|
+
}
|
|
1651
1836
|
async function anyPortAnswers(ports) {
|
|
1652
1837
|
for (const port of Object.values(ports ?? {})) if (await portAnswers(port)) return true;
|
|
1653
1838
|
return false;
|
|
@@ -1764,6 +1949,15 @@ function reserveEffort(ctx, o, branch, scopes, commons) {
|
|
|
1764
1949
|
});
|
|
1765
1950
|
return effort;
|
|
1766
1951
|
}
|
|
1952
|
+
function areaBranchFor(ctx, area, preferredType) {
|
|
1953
|
+
const main = new Git(ctx.repoRoot);
|
|
1954
|
+
const types = [preferredType, ...ctx.manifest.branch.types.filter((t) => t !== preferredType)];
|
|
1955
|
+
for (const type of types) {
|
|
1956
|
+
const b = branchFor(type, area);
|
|
1957
|
+
if (main.branchExists(b) || main.remoteRef(b)) return b;
|
|
1958
|
+
}
|
|
1959
|
+
return branchFor(preferredType, area);
|
|
1960
|
+
}
|
|
1767
1961
|
function ensureWorktree(ctx, area, branch, task) {
|
|
1768
1962
|
const m = ctx.manifest;
|
|
1769
1963
|
const path = worktreePathFor(ctx, area, task);
|
|
@@ -1815,8 +2009,25 @@ function appendLog(file, line) {
|
|
|
1815
2009
|
appendFileSync2(file, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
1816
2010
|
`);
|
|
1817
2011
|
}
|
|
2012
|
+
var PENDING_MARKS = ".pending-marks.json";
|
|
2013
|
+
function readPendingMarks(logDir) {
|
|
2014
|
+
try {
|
|
2015
|
+
const raw = JSON.parse(readFileSync5(join8(logDir, PENDING_MARKS), "utf8"));
|
|
2016
|
+
return Array.isArray(raw) ? raw.filter((x) => typeof x === "string") : [];
|
|
2017
|
+
} catch {
|
|
2018
|
+
return [];
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function writePendingMarks(logDir, marks) {
|
|
2022
|
+
mkdirSync6(logDir, { recursive: true });
|
|
2023
|
+
writeFileSync5(join8(logDir, PENDING_MARKS), `${JSON.stringify(marks)}
|
|
2024
|
+
`);
|
|
2025
|
+
}
|
|
1818
2026
|
async function startEffort(ctx, o) {
|
|
1819
2027
|
const m = ctx.manifest;
|
|
2028
|
+
await reapLeases(ctx).catch(
|
|
2029
|
+
(err) => ctx.log(`pre-flight reap: ${err instanceof Error ? err.message : String(err)}`)
|
|
2030
|
+
);
|
|
1820
2031
|
fetchBaseOnce(ctx);
|
|
1821
2032
|
const { branch, scopes, commons, warnings } = guardStart(ctx, o);
|
|
1822
2033
|
const notices = ctx.registry.notices(m.project, o.area);
|
|
@@ -1960,6 +2171,9 @@ async function startEffort(ctx, o) {
|
|
|
1960
2171
|
}
|
|
1961
2172
|
);
|
|
1962
2173
|
const baseSha = wg.rev("HEAD");
|
|
2174
|
+
if (o.mode === "frontend" && sel.marks.length > 0) {
|
|
2175
|
+
writePendingMarks(logDir, [.../* @__PURE__ */ new Set([...readPendingMarks(logDir), ...sel.marks])]);
|
|
2176
|
+
}
|
|
1963
2177
|
for (const name of commons)
|
|
1964
2178
|
leases.push(
|
|
1965
2179
|
ctx.registry.addLease(
|
|
@@ -1983,8 +2197,13 @@ async function startEffort(ctx, o) {
|
|
|
1983
2197
|
branch
|
|
1984
2198
|
});
|
|
1985
2199
|
const env = { ...process.env, ...cfg.vars };
|
|
1986
|
-
|
|
2200
|
+
const owedMarks = [.../* @__PURE__ */ new Set([...sel.marks, ...readPendingMarks(logDir)])];
|
|
2201
|
+
if (owedMarks.includes("db-reset") && m.services.postgres?.reset) {
|
|
1987
2202
|
runRefresh([m.services.postgres.reset], worktree, (l) => appendLog(refreshLog, l), env);
|
|
2203
|
+
writePendingMarks(
|
|
2204
|
+
logDir,
|
|
2205
|
+
owedMarks.filter((x) => x !== "db-reset")
|
|
2206
|
+
);
|
|
1988
2207
|
}
|
|
1989
2208
|
const pids = {};
|
|
1990
2209
|
const signatures = {};
|
|
@@ -2073,7 +2292,7 @@ async function promoteEffort(ctx, o) {
|
|
|
2073
2292
|
}
|
|
2074
2293
|
const modeScopes = expandScopes(m, o.area, o.mode);
|
|
2075
2294
|
const nextScopes = open.task ? [.../* @__PURE__ */ new Set([...open.scopes, ...modeScopes])] : modeScopes;
|
|
2076
|
-
|
|
2295
|
+
const refuseOverlap = (r) => {
|
|
2077
2296
|
const others = r.efforts.filter(
|
|
2078
2297
|
(e) => e.project === m.project && e.id !== open.id && (e.state === "active" || e.state === "starting")
|
|
2079
2298
|
);
|
|
@@ -2088,17 +2307,49 @@ async function promoteEffort(ctx, o) {
|
|
|
2088
2307
|
);
|
|
2089
2308
|
}
|
|
2090
2309
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
r.efforts[idx] = { ...r.efforts[idx], mode: o.mode, scopes: nextScopes, lastActivity: now() };
|
|
2094
|
-
}
|
|
2095
|
-
});
|
|
2310
|
+
};
|
|
2311
|
+
ctx.registry.update(refuseOverlap);
|
|
2096
2312
|
const warnings = [];
|
|
2097
2313
|
let slot = ctx.registry.leases({ effort: open.id, kind: "slot" })[0];
|
|
2098
2314
|
const sb = ctx.registry.leases({ effort: open.id, kind: "storybook" })[0];
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2315
|
+
const claimed = !slot;
|
|
2316
|
+
if (!slot) slot = await claimFreeSlot(ctx, open.id, warnings);
|
|
2317
|
+
try {
|
|
2318
|
+
ctx.registry.update((r) => {
|
|
2319
|
+
refuseOverlap(r);
|
|
2320
|
+
const idx = r.efforts.findIndex((e) => e.id === open.id);
|
|
2321
|
+
if (idx >= 0) {
|
|
2322
|
+
r.efforts[idx] = {
|
|
2323
|
+
...r.efforts[idx],
|
|
2324
|
+
mode: o.mode,
|
|
2325
|
+
scopes: nextScopes,
|
|
2326
|
+
leases: claimed ? [...open.leases, slot.id] : r.efforts[idx].leases,
|
|
2327
|
+
lastActivity: now()
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
if (claimed) ctx.registry.release(slot.id);
|
|
2333
|
+
throw err;
|
|
2334
|
+
}
|
|
2335
|
+
const owed = readPendingMarks(join8(open.worktree, m.logs.dir));
|
|
2336
|
+
if (owed.includes("db-reset") && m.services.postgres?.reset) {
|
|
2337
|
+
const cfgForReset = emitWorktreeConfig({
|
|
2338
|
+
m,
|
|
2339
|
+
worktree: open.worktree,
|
|
2340
|
+
slot: slot.value,
|
|
2341
|
+
ports: slot.ports ?? {},
|
|
2342
|
+
area: o.area,
|
|
2343
|
+
branch: open.branch
|
|
2344
|
+
});
|
|
2345
|
+
runRefresh([m.services.postgres.reset], open.worktree, ctx.log, {
|
|
2346
|
+
...process.env,
|
|
2347
|
+
...cfgForReset.vars
|
|
2348
|
+
});
|
|
2349
|
+
writePendingMarks(
|
|
2350
|
+
join8(open.worktree, m.logs.dir),
|
|
2351
|
+
owed.filter((x) => x !== "db-reset")
|
|
2352
|
+
);
|
|
2102
2353
|
}
|
|
2103
2354
|
const cfg = emitWorktreeConfig({
|
|
2104
2355
|
m,
|
|
@@ -2149,9 +2400,7 @@ async function promoteEffort(ctx, o) {
|
|
|
2149
2400
|
}
|
|
2150
2401
|
function behindBase(ctx, worktree) {
|
|
2151
2402
|
if (!existsSync7(worktree)) return 0;
|
|
2152
|
-
|
|
2153
|
-
const r = g.tryRun(["rev-list", "--count", `HEAD..origin/${ctx.manifest.baseBranch}`]);
|
|
2154
|
-
return r.code === 0 ? Number(r.out.trim()) || 0 : 0;
|
|
2403
|
+
return new Git(worktree).behindCount(ctx.manifest.baseBranch);
|
|
2155
2404
|
}
|
|
2156
2405
|
function statusFor(ctx, area, task) {
|
|
2157
2406
|
const m = ctx.manifest;
|
|
@@ -2233,6 +2482,15 @@ async function releaseLease(ctx, effort, lease) {
|
|
|
2233
2482
|
}
|
|
2234
2483
|
ctx.registry.release(lease.id);
|
|
2235
2484
|
}
|
|
2485
|
+
async function sweepStorybook(ctx, effort) {
|
|
2486
|
+
if (effort.mode === "backend" || !ctx.storybrokr.downHost) return;
|
|
2487
|
+
try {
|
|
2488
|
+
const stopped = await ctx.storybrokr.downHost(storybookHostFor(ctx.manifest, effort.worktree));
|
|
2489
|
+
if (stopped.length > 0) ctx.log(`storybrokr: stopped ${stopped.join(", ")}`);
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
ctx.log(`storybrokr sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2236
2494
|
function leaveNotices(ctx, effort, commons) {
|
|
2237
2495
|
if (commons.length === 0) return 0;
|
|
2238
2496
|
const notice = {
|
|
@@ -2301,7 +2559,7 @@ async function finishEffort(ctx, o) {
|
|
|
2301
2559
|
);
|
|
2302
2560
|
}
|
|
2303
2561
|
fetchBaseOnce(ctx);
|
|
2304
|
-
const parentBranch =
|
|
2562
|
+
const parentBranch = areaBranchFor(ctx, effort.area, effort.type);
|
|
2305
2563
|
const { wg: areaGit } = ensureWorktree(ctx, effort.area, parentBranch);
|
|
2306
2564
|
if (areaGit.isDirty()) {
|
|
2307
2565
|
throw new WorkkitError(
|
|
@@ -2321,14 +2579,24 @@ async function finishEffort(ctx, o) {
|
|
|
2321
2579
|
);
|
|
2322
2580
|
}
|
|
2323
2581
|
}
|
|
2582
|
+
fetchBaseOnce(ctx);
|
|
2583
|
+
const shouldPush = !o.intoArea && (g.ownCommitCount(m.baseBranch) > 0 || g.remoteBranchExists(effort.branch));
|
|
2584
|
+
if (shouldPush) {
|
|
2585
|
+
try {
|
|
2586
|
+
g.push(effort.branch);
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
throw new WorkkitError(
|
|
2589
|
+
"PUSH_REJECTED",
|
|
2590
|
+
`push of ${effort.branch} was rejected (${err instanceof Error ? err.message : String(err)}); nothing was stopped or released. Merge origin/${effort.branch} into the worktree and run \`work finish\` again.`,
|
|
2591
|
+
{ branch: effort.branch, worktree: effort.worktree }
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2324
2595
|
for (const lease of ctx.registry.leases({ effort: effort.id }))
|
|
2325
2596
|
await releaseLease(ctx, effort, lease);
|
|
2326
|
-
await
|
|
2327
|
-
() => void 0
|
|
2328
|
-
);
|
|
2597
|
+
await sweepStorybook(ctx, effort);
|
|
2329
2598
|
let notices = 0;
|
|
2330
2599
|
if (!o.intoArea) {
|
|
2331
|
-
g.push(effort.branch);
|
|
2332
2600
|
if (g.commitsNotOn(m.baseBranch).length > 0 && !g.isEquivalentToBase(m.baseBranch)) {
|
|
2333
2601
|
const since = g.changedSince(m.baseBranch);
|
|
2334
2602
|
const sinceSet = new Set(since);
|
|
@@ -2347,14 +2615,14 @@ async function finishEffort(ctx, o) {
|
|
|
2347
2615
|
ctx.registry.removeEfforts((e) => e.id === effort.id);
|
|
2348
2616
|
return {
|
|
2349
2617
|
effort: finished2,
|
|
2350
|
-
pushed:
|
|
2618
|
+
pushed: shouldPush,
|
|
2351
2619
|
parked,
|
|
2352
2620
|
mergedIntoArea: Boolean(o.intoArea),
|
|
2353
2621
|
notices
|
|
2354
2622
|
};
|
|
2355
2623
|
}
|
|
2356
2624
|
const finished = ctx.registry.updateEffort(effort.id, { state: "finished", lastActivity: now() });
|
|
2357
|
-
return { effort: finished, pushed:
|
|
2625
|
+
return { effort: finished, pushed: shouldPush, parked, notices };
|
|
2358
2626
|
}
|
|
2359
2627
|
async function reapLeases(ctx, o = {}) {
|
|
2360
2628
|
const m = ctx.manifest;
|
|
@@ -2371,19 +2639,24 @@ async function reapLeases(ctx, o = {}) {
|
|
|
2371
2639
|
const garbage = orphaned && !await anyPortAnswers(l.ports);
|
|
2372
2640
|
if (expired || garbage) doomed.add(l.effort);
|
|
2373
2641
|
}
|
|
2642
|
+
for (const e of efforts.values()) {
|
|
2643
|
+
if (e.state !== "starting" || doomed.has(e.id)) continue;
|
|
2644
|
+
if (leases.some((l) => l.effort === e.id)) continue;
|
|
2645
|
+
if (Date.now() - Date.parse(e.started) < STARTING_GRACE_MS) continue;
|
|
2646
|
+
doomed.add(e.id);
|
|
2647
|
+
}
|
|
2374
2648
|
const reaped = leases.filter((l) => doomed.has(l.effort));
|
|
2375
2649
|
if (o.dryRun) return { reaped };
|
|
2376
2650
|
for (const lease of reaped) await releaseLease(ctx, efforts.get(lease.effort), lease);
|
|
2377
2651
|
for (const id of doomed) {
|
|
2378
2652
|
const effort = efforts.get(id);
|
|
2379
2653
|
if (effort) {
|
|
2380
|
-
await
|
|
2381
|
-
() => void 0
|
|
2382
|
-
);
|
|
2654
|
+
await sweepStorybook(ctx, effort);
|
|
2383
2655
|
if (effort.state === "active" || effort.state === "starting") {
|
|
2656
|
+
const hadLease = leases.some((l) => l.effort === effort.id);
|
|
2384
2657
|
ctx.registry.updateEffort(effort.id, {
|
|
2385
2658
|
state: "parked",
|
|
2386
|
-
parkedReason: `idle: a lease expired by ${at.toISOString()}`,
|
|
2659
|
+
parkedReason: hadLease ? `idle: a lease expired by ${at.toISOString()}` : `start died before its first lease (started ${effort.started})`,
|
|
2387
2660
|
lastActivity: now()
|
|
2388
2661
|
});
|
|
2389
2662
|
}
|
|
@@ -2441,9 +2714,6 @@ async function retireArea(ctx, o) {
|
|
|
2441
2714
|
const worktree = realpathOr2(worktreePathFor(ctx, o.area));
|
|
2442
2715
|
const main = new Git(ctx.repoRoot);
|
|
2443
2716
|
let archived = [];
|
|
2444
|
-
await Promise.resolve(ctx.storybrokr.downHost?.(storybookHostFor(m, worktree))).catch(
|
|
2445
|
-
() => void 0
|
|
2446
|
-
);
|
|
2447
2717
|
if (existsSync7(worktree)) {
|
|
2448
2718
|
const g = new Git(worktree);
|
|
2449
2719
|
if (g.isDirty())
|
|
@@ -2459,16 +2729,20 @@ async function retireArea(ctx, o) {
|
|
|
2459
2729
|
`${g.currentBranch()} has ${unpushed} commit(s) not on origin; push before retiring`,
|
|
2460
2730
|
{ worktree, unpushed }
|
|
2461
2731
|
);
|
|
2732
|
+
const hadStorybook = ctx.registry.efforts({ project: m.project, area: o.area }).some((e) => e.mode !== "backend");
|
|
2733
|
+
if (hadStorybook) await sweepStorybook(ctx, { worktree, mode: "frontend" });
|
|
2462
2734
|
archived = archiveWorktree(ctx, o.area, worktree);
|
|
2463
2735
|
main.worktreeRemove(worktree);
|
|
2464
2736
|
}
|
|
2465
2737
|
const branchesDeleted = [];
|
|
2466
2738
|
const branchesKept = [];
|
|
2467
2739
|
const allAreaBranches = m.branch.types.map((type) => `${type}/${o.area}`);
|
|
2740
|
+
fetchBaseOnce(ctx);
|
|
2468
2741
|
for (const branch of allAreaBranches) {
|
|
2469
2742
|
if (!main.branchExists(branch)) continue;
|
|
2470
2743
|
const onOrigin = main.remoteBranchExists(branch) && main.tryRun(["rev-list", "--count", `origin/${branch}..${branch}`]).out.trim() === "0";
|
|
2471
|
-
|
|
2744
|
+
const mergedIntoBase = !onOrigin && main.branchMergedInto(branch, m.baseBranch);
|
|
2745
|
+
if (onOrigin || mergedIntoBase) {
|
|
2472
2746
|
main.deleteLocalBranch(branch);
|
|
2473
2747
|
branchesDeleted.push(branch);
|
|
2474
2748
|
} else branchesKept.push(branch);
|
|
@@ -2478,6 +2752,94 @@ async function retireArea(ctx, o) {
|
|
|
2478
2752
|
ctx.registry.clearNoticesFrom(m.project, allAreaBranches);
|
|
2479
2753
|
return { archived, removed: worktree, branchesDeleted, branchesKept };
|
|
2480
2754
|
}
|
|
2755
|
+
function effortAt(ctx, cwd) {
|
|
2756
|
+
const top = new Git(cwd).tryRun(["rev-parse", "--show-toplevel"]);
|
|
2757
|
+
if (top.code !== 0) return void 0;
|
|
2758
|
+
const root = realpathOr2(top.out.trim());
|
|
2759
|
+
return ctx.registry.efforts({ project: ctx.manifest.project }).filter((e) => e.state === "active" || e.state === "starting" || e.state === "parked").sort((a, b) => b.lastActivity.localeCompare(a.lastActivity)).find((e) => realpathOr2(e.worktree) === root);
|
|
2760
|
+
}
|
|
2761
|
+
async function prEffort(ctx, o) {
|
|
2762
|
+
const m = ctx.manifest;
|
|
2763
|
+
assertPrittyVersion(ctx.pritty);
|
|
2764
|
+
const effort = o.area ? ctx.registry.openEffortFor(m.project, o.area, o.task) ?? ctx.registry.efforts({ project: m.project, area: o.area, state: "parked" }).filter((e) => (e.task ?? void 0) === (o.task ?? void 0)).sort((a, b) => b.started.localeCompare(a.started))[0] : effortAt(ctx, o.cwd ?? process.cwd());
|
|
2765
|
+
if (!effort) {
|
|
2766
|
+
throw new WorkkitError(
|
|
2767
|
+
"EFFORT_NOT_FOUND",
|
|
2768
|
+
o.area ? `no open or parked effort on ${effortLabel2(o.area, o.task)}` : `${o.cwd ?? process.cwd()} is not inside an effort's worktree; pass the area`,
|
|
2769
|
+
{ area: o.area, task: o.task }
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
const label = effortLabel2(effort.area, effort.task);
|
|
2773
|
+
const g = new Git(effort.worktree);
|
|
2774
|
+
if (g.isDirty()) {
|
|
2775
|
+
throw new WorkkitError(
|
|
2776
|
+
"DIRTY_WORKTREE",
|
|
2777
|
+
`${effort.worktree} has uncommitted changes: commit them, or \`work finish --park\` to commit them as wip(${label}) and push`,
|
|
2778
|
+
{ worktree: effort.worktree }
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
fetchBaseOnce(ctx);
|
|
2782
|
+
const warnings = [];
|
|
2783
|
+
if (!g.hasUpstream() || g.unpushedCount(m.baseBranch) > 0) {
|
|
2784
|
+
try {
|
|
2785
|
+
g.push(effort.branch);
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
throw new WorkkitError(
|
|
2788
|
+
"PUSH_REJECTED",
|
|
2789
|
+
`push of ${effort.branch} was rejected (${err instanceof Error ? err.message : String(err)}); merge origin/${effort.branch} into the worktree and run \`work pr\` again`,
|
|
2790
|
+
{ branch: effort.branch, worktree: effort.worktree }
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
const behind = g.behindCount(m.baseBranch);
|
|
2795
|
+
if (behind > BEHIND_LIMIT) {
|
|
2796
|
+
warnings.push(
|
|
2797
|
+
`${effort.branch} is ${behind} commits behind ${m.baseBranch}; consider merging origin/${m.baseBranch} before review`
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
const given = o.title !== void 0 || o.body !== void 0 || o.bodyFile !== void 0;
|
|
2801
|
+
const store = (pr2) => {
|
|
2802
|
+
const updated = ctx.registry.updateEffort(effort.id, { pr: pr2, lastActivity: now() });
|
|
2803
|
+
touchEffort(ctx, updated);
|
|
2804
|
+
return updated;
|
|
2805
|
+
};
|
|
2806
|
+
if (effort.pr) {
|
|
2807
|
+
const { open, last } = await ctx.pritty.check({ cwd: effort.worktree, base: m.baseBranch });
|
|
2808
|
+
if (open) {
|
|
2809
|
+
if (!given) {
|
|
2810
|
+
return { effort: store(open), pr: open, created: false, behindBase: behind, warnings };
|
|
2811
|
+
}
|
|
2812
|
+
const updated = await ctx.pritty.pr({
|
|
2813
|
+
cwd: effort.worktree,
|
|
2814
|
+
base: m.baseBranch,
|
|
2815
|
+
draft: o.draft,
|
|
2816
|
+
title: o.title,
|
|
2817
|
+
body: o.body,
|
|
2818
|
+
bodyFile: o.bodyFile
|
|
2819
|
+
});
|
|
2820
|
+
const pr2 = { number: updated.number, url: updated.url, draft: updated.draft };
|
|
2821
|
+
return { effort: store(pr2), pr: pr2, created: false, behindBase: behind, warnings };
|
|
2822
|
+
}
|
|
2823
|
+
if (!o.force) {
|
|
2824
|
+
const fate = last?.number === effort.pr.number ? last.state === "MERGED" ? "was merged" : "was closed without merging" : "is no longer open";
|
|
2825
|
+
throw new WorkkitError(
|
|
2826
|
+
"PR_CLOSED",
|
|
2827
|
+
`PR #${effort.pr.number} for ${label} ${fate} (${effort.pr.url}); pass --force to open a new one`,
|
|
2828
|
+
{ pr: effort.pr, state: last?.state }
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
const res = await ctx.pritty.pr({
|
|
2833
|
+
cwd: effort.worktree,
|
|
2834
|
+
base: m.baseBranch,
|
|
2835
|
+
draft: o.draft,
|
|
2836
|
+
title: o.title,
|
|
2837
|
+
body: o.body,
|
|
2838
|
+
bodyFile: o.bodyFile
|
|
2839
|
+
});
|
|
2840
|
+
const pr = { number: res.number, url: res.url, draft: res.draft };
|
|
2841
|
+
return { effort: store(pr), pr, created: res.created, behindBase: behind, warnings };
|
|
2842
|
+
}
|
|
2481
2843
|
var BEHIND_LIMIT = 20;
|
|
2482
2844
|
var NOTICE_STALE_MS = 7 * 864e5;
|
|
2483
2845
|
var ARCHIVE_LIMIT_BYTES = 5 * 1024 ** 3;
|
|
@@ -2665,7 +3027,7 @@ function registerFinish(program2) {
|
|
|
2665
3027
|
}
|
|
2666
3028
|
|
|
2667
3029
|
// src/commands/hook.ts
|
|
2668
|
-
import { readFileSync as
|
|
3030
|
+
import { readFileSync as readFileSync6, realpathSync as realpathSync5 } from "fs";
|
|
2669
3031
|
import { resolve as resolve2 } from "path";
|
|
2670
3032
|
import chalk5 from "chalk";
|
|
2671
3033
|
var ZEROS = /^0+$/;
|
|
@@ -2763,7 +3125,7 @@ function registerHook(program2) {
|
|
|
2763
3125
|
"Refuse pushes to protected branches; warn when paths leave the effort scope or touch commons"
|
|
2764
3126
|
).argument("[remote]", "passed by git, unused").argument("[url]", "passed by git, unused").action(async () => {
|
|
2765
3127
|
try {
|
|
2766
|
-
const stdin = process.stdin.isTTY ? "" :
|
|
3128
|
+
const stdin = process.stdin.isTTY ? "" : readFileSync6(0, "utf8");
|
|
2767
3129
|
process.exit(await runPrePush(process.cwd(), stdin));
|
|
2768
3130
|
} catch (err) {
|
|
2769
3131
|
fail(err, false);
|
|
@@ -2786,7 +3148,7 @@ function registerHook(program2) {
|
|
|
2786
3148
|
}
|
|
2787
3149
|
|
|
2788
3150
|
// src/commands/init.ts
|
|
2789
|
-
import { existsSync as existsSync8, writeFileSync as
|
|
3151
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
2790
3152
|
import { join as join9, resolve as resolve3 } from "path";
|
|
2791
3153
|
var STORYBOOK_BASE = 6100;
|
|
2792
3154
|
function defaultService(name) {
|
|
@@ -2862,7 +3224,7 @@ function runInit(o) {
|
|
|
2862
3224
|
}
|
|
2863
3225
|
const manifest = buildStarterManifest(o);
|
|
2864
3226
|
const manifestPath = join9(repoRoot, "helmsmith.config.mjs");
|
|
2865
|
-
|
|
3227
|
+
writeFileSync6(manifestPath, `${HEADER}export default ${JSON.stringify(manifest, null, 2)};
|
|
2866
3228
|
`);
|
|
2867
3229
|
claimRanges(Registry.open(o.home), validateManifest(manifest), repoRoot);
|
|
2868
3230
|
const hooks = o.hooks ? installHooks(repoRoot, { protectedBranches: [o.baseBranch] }).files : [];
|
|
@@ -2930,7 +3292,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2930
3292
|
import { z as z2 } from "zod";
|
|
2931
3293
|
|
|
2932
3294
|
// src/version.ts
|
|
2933
|
-
import { existsSync as existsSync9, readFileSync as
|
|
3295
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2934
3296
|
import { dirname as dirname4, join as join10 } from "path";
|
|
2935
3297
|
import { fileURLToPath } from "url";
|
|
2936
3298
|
function findPackageJson(startDir) {
|
|
@@ -2944,7 +3306,7 @@ function findPackageJson(startDir) {
|
|
|
2944
3306
|
}
|
|
2945
3307
|
}
|
|
2946
3308
|
var pkgFile = findPackageJson(dirname4(fileURLToPath(import.meta.url)));
|
|
2947
|
-
var VERSION = JSON.parse(
|
|
3309
|
+
var VERSION = JSON.parse(readFileSync7(pkgFile, "utf8")).version;
|
|
2948
3310
|
|
|
2949
3311
|
// src/mcp/server.ts
|
|
2950
3312
|
var ok = (value) => ({
|
|
@@ -2969,7 +3331,9 @@ function buildMcpServer(ctxFactory) {
|
|
|
2969
3331
|
description: "Start an effort on an area (or intra-area task): its worktree on <type>/<area> (or <type>/<area>--<task>), synced from the base branch, refreshed, leased (a slot for fullstack/backend, a storybrokr instance for frontend with a story) and running. Refuses with EFFORT_OPEN (another effort is open; join=true to share it), SCOPE_CONFLICT, SYNC_CONFLICT (resolve in the worktree, rerun with sync=false).",
|
|
2970
3332
|
inputSchema: {
|
|
2971
3333
|
area: z2.string().describe("a leaf area from helmsmith.config (e.g. org/applications)"),
|
|
2972
|
-
task: z2.string().optional().describe(
|
|
3334
|
+
task: z2.string().optional().describe(
|
|
3335
|
+
"optional intra-area task slug for parallel sub-efforts (<type>/<area>--<task>)"
|
|
3336
|
+
),
|
|
2973
3337
|
type: z2.string().describe("feature | chore | fix"),
|
|
2974
3338
|
mode: z2.enum(["frontend", "backend", "fullstack"]),
|
|
2975
3339
|
scopes: z2.array(z2.string()).optional().describe("optional narrowed scope globs for a task sub-effort"),
|
|
@@ -3002,11 +3366,38 @@ function buildMcpServer(ctxFactory) {
|
|
|
3002
3366
|
area: z2.string().describe("the area of the open effort"),
|
|
3003
3367
|
task: z2.string().optional().describe("optional intra-area task slug"),
|
|
3004
3368
|
mode: z2.enum(["backend", "fullstack"]).describe("target mode to promote into"),
|
|
3005
|
-
story: z2.string().optional().describe("optional story path"),
|
|
3006
3369
|
projectRoot: z2.string().optional().describe("optional repository root path override")
|
|
3007
3370
|
}
|
|
3008
3371
|
},
|
|
3009
|
-
async ({ area, task, mode,
|
|
3372
|
+
async ({ area, task, mode, projectRoot }) => guard(async () => promoteEffort(await ctxFactory(projectRoot), { area, task, mode }))
|
|
3373
|
+
);
|
|
3374
|
+
server.registerTool(
|
|
3375
|
+
"work_pr",
|
|
3376
|
+
{
|
|
3377
|
+
description: "Open the effort's pull request through pritty: pushes the branch first, reuses a stored PR that is still open (updating its title/body when given), refuses with PR_CLOSED when it was merged or closed unless force, and stores { number, url, draft } on the effort. Pass title and body: the agent writes the words, pritty's AI path is only the fallback.",
|
|
3378
|
+
inputSchema: {
|
|
3379
|
+
area: z2.string().optional().describe("the area of the effort (default: the effort of the cwd worktree)"),
|
|
3380
|
+
task: z2.string().optional().describe("optional intra-area task slug"),
|
|
3381
|
+
draft: z2.boolean().optional().describe("open a draft PR"),
|
|
3382
|
+
title: z2.string().optional().describe("PR title"),
|
|
3383
|
+
// body text only: a file path here would let any MCP caller have pritty read an
|
|
3384
|
+
// arbitrary file and publish it as a PR body; read the file yourself and pass the text
|
|
3385
|
+
body: z2.string().optional().describe("PR body text (read your body file and pass its text)"),
|
|
3386
|
+
force: z2.boolean().optional().describe("open a new PR when the stored one was closed"),
|
|
3387
|
+
projectRoot: z2.string().optional().describe("optional repository root path override")
|
|
3388
|
+
}
|
|
3389
|
+
},
|
|
3390
|
+
async ({ area, task, draft, title, body, force, projectRoot }) => guard(
|
|
3391
|
+
async () => prEffort(await ctxFactory(projectRoot), {
|
|
3392
|
+
area,
|
|
3393
|
+
task,
|
|
3394
|
+
cwd: process.cwd(),
|
|
3395
|
+
draft,
|
|
3396
|
+
title,
|
|
3397
|
+
body,
|
|
3398
|
+
force
|
|
3399
|
+
})
|
|
3400
|
+
)
|
|
3010
3401
|
);
|
|
3011
3402
|
server.registerTool(
|
|
3012
3403
|
"work_status",
|
|
@@ -3084,24 +3475,56 @@ function registerMcp(program2) {
|
|
|
3084
3475
|
});
|
|
3085
3476
|
}
|
|
3086
3477
|
|
|
3087
|
-
// src/commands/
|
|
3478
|
+
// src/commands/pr.ts
|
|
3088
3479
|
import chalk6 from "chalk";
|
|
3480
|
+
function registerPr(program2) {
|
|
3481
|
+
program2.command("pr [area]").description(
|
|
3482
|
+
"Open the effort's pull request through pritty (push first, reuse an open PR, store it on the effort). No area: the effort of the cwd's worktree."
|
|
3483
|
+
).option("--task <slug>", "intra-area task slug").option("--draft", "open a draft PR").option("--title <title>", "PR title (with --body or --body-file: no AI involved)").option("--body <text>", "PR body text").option("--body-file <path>", "PR body from a file").option("--force", "open a new PR when the stored one was merged or closed").option("--project-root <dir>", "repository root (default: the main checkout of the cwd)").option("--json", "print the result as JSON").action(
|
|
3484
|
+
async (area, o) => {
|
|
3485
|
+
try {
|
|
3486
|
+
const ctx = await buildCtx({ projectRoot: o.projectRoot, quiet: Boolean(o.json) });
|
|
3487
|
+
const r = await prEffort(ctx, {
|
|
3488
|
+
area,
|
|
3489
|
+
task: o.task,
|
|
3490
|
+
cwd: process.cwd(),
|
|
3491
|
+
draft: o.draft,
|
|
3492
|
+
title: o.title,
|
|
3493
|
+
body: o.body,
|
|
3494
|
+
bodyFile: o.bodyFile,
|
|
3495
|
+
force: o.force
|
|
3496
|
+
});
|
|
3497
|
+
if (o.json) printJson(r);
|
|
3498
|
+
else {
|
|
3499
|
+
for (const w of r.warnings) console.error(chalk6.yellow(w));
|
|
3500
|
+
console.log(
|
|
3501
|
+
`${r.created ? "opened" : "open"} PR #${r.pr.number}${r.pr.draft ? " (draft)" : ""} for ${r.effort.branch}: ${r.pr.url}`
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
} catch (err) {
|
|
3505
|
+
fail(err, Boolean(o.json));
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// src/commands/promote.ts
|
|
3512
|
+
import chalk7 from "chalk";
|
|
3089
3513
|
function registerPromote(program2) {
|
|
3090
|
-
program2.command("promote").description("Promote an open effort in place to backend or fullstack mode").requiredOption("--area <area>", "the area of the open effort").option("--task <slug>", "intra-area task slug if promoting a task sub-effort").requiredOption("--mode <mode>", "target mode: backend | fullstack").option("--
|
|
3514
|
+
program2.command("promote").description("Promote an open effort in place to backend or fullstack mode").requiredOption("--area <area>", "the area of the open effort").option("--task <slug>", "intra-area task slug if promoting a task sub-effort").requiredOption("--mode <mode>", "target mode: backend | fullstack").option("--project-root <dir>", "repository root (default: the main checkout of the cwd)").option("--json", "print the result as JSON").action(
|
|
3091
3515
|
async (o) => {
|
|
3092
3516
|
try {
|
|
3093
3517
|
const ctx = await buildCtx({ projectRoot: o.projectRoot, quiet: Boolean(o.json) });
|
|
3094
3518
|
const r = await promoteEffort(ctx, {
|
|
3095
3519
|
area: o.area,
|
|
3096
3520
|
task: o.task,
|
|
3097
|
-
mode: o.mode
|
|
3098
|
-
story: o.story
|
|
3521
|
+
mode: o.mode
|
|
3099
3522
|
});
|
|
3100
3523
|
if (o.json) printJson(r);
|
|
3101
3524
|
else {
|
|
3102
3525
|
const label = r.effort.task ? `${r.effort.area}--${r.effort.task}` : r.effort.area;
|
|
3103
3526
|
console.log(
|
|
3104
|
-
`${
|
|
3527
|
+
`${chalk7.bold(label)} promoted to ${r.effort.mode}${r.startedServices.length ? ` (started: ${r.startedServices.join(", ")})` : ""}`
|
|
3105
3528
|
);
|
|
3106
3529
|
for (const w of r.warnings) warn(w);
|
|
3107
3530
|
}
|
|
@@ -3113,7 +3536,7 @@ function registerPromote(program2) {
|
|
|
3113
3536
|
}
|
|
3114
3537
|
|
|
3115
3538
|
// src/commands/reap.ts
|
|
3116
|
-
import
|
|
3539
|
+
import chalk8 from "chalk";
|
|
3117
3540
|
function registerReap(program2) {
|
|
3118
3541
|
program2.command("reap").description(
|
|
3119
3542
|
"Expire idle leases: stop only what they started, release them, park their efforts"
|
|
@@ -3122,7 +3545,7 @@ function registerReap(program2) {
|
|
|
3122
3545
|
const ctx = await buildCtx({ projectRoot: o.projectRoot, quiet: Boolean(o.json) });
|
|
3123
3546
|
const r = await reapLeases(ctx, { dryRun: o.dryRun });
|
|
3124
3547
|
if (o.json) printJson(r);
|
|
3125
|
-
else if (r.reaped.length === 0) console.log(
|
|
3548
|
+
else if (r.reaped.length === 0) console.log(chalk8.gray("nothing to reap"));
|
|
3126
3549
|
else
|
|
3127
3550
|
for (const l of r.reaped)
|
|
3128
3551
|
console.log(
|
|
@@ -3135,7 +3558,7 @@ function registerReap(program2) {
|
|
|
3135
3558
|
}
|
|
3136
3559
|
|
|
3137
3560
|
// src/commands/retire.ts
|
|
3138
|
-
import
|
|
3561
|
+
import chalk9 from "chalk";
|
|
3139
3562
|
function registerRetire(program2) {
|
|
3140
3563
|
program2.command("retire").description(
|
|
3141
3564
|
"Archive an area's evidence, remove its worktree and its local branches that are on origin (the only destructive verb)"
|
|
@@ -3145,11 +3568,11 @@ function registerRetire(program2) {
|
|
|
3145
3568
|
const r = await retireArea(ctx, { area: o.area });
|
|
3146
3569
|
if (o.json) printJson(r);
|
|
3147
3570
|
else {
|
|
3148
|
-
console.log(`${
|
|
3571
|
+
console.log(`${chalk9.bold(o.area)} retired: removed ${r.removed}`);
|
|
3149
3572
|
for (const a of r.archived) console.log(` archived ${a}`);
|
|
3150
3573
|
for (const b of r.branchesDeleted) console.log(` deleted local branch ${b}`);
|
|
3151
3574
|
for (const b of r.branchesKept)
|
|
3152
|
-
console.log(
|
|
3575
|
+
console.log(chalk9.yellow(` kept ${b}: not fully on origin`));
|
|
3153
3576
|
}
|
|
3154
3577
|
} catch (err) {
|
|
3155
3578
|
fail(err, Boolean(o.json));
|
|
@@ -3158,12 +3581,12 @@ function registerRetire(program2) {
|
|
|
3158
3581
|
}
|
|
3159
3582
|
|
|
3160
3583
|
// src/commands/start.ts
|
|
3161
|
-
import
|
|
3584
|
+
import chalk10 from "chalk";
|
|
3162
3585
|
function printStart(r) {
|
|
3163
3586
|
const e = r.effort;
|
|
3164
3587
|
const label = e.task ? `${e.area}--${e.task}` : e.area;
|
|
3165
3588
|
console.log(
|
|
3166
|
-
`${
|
|
3589
|
+
`${chalk10.bold(label)} ${e.type}/${e.mode} ${e.branch}${r.joined ? chalk10.yellow(" (joined)") : ""}`
|
|
3167
3590
|
);
|
|
3168
3591
|
console.log(` worktree ${e.worktree}`);
|
|
3169
3592
|
console.log(
|
|
@@ -3174,7 +3597,7 @@ function printStart(r) {
|
|
|
3174
3597
|
console.log(` lease ${l.kind} ${l.value}${ports} expires ${l.expires}`);
|
|
3175
3598
|
}
|
|
3176
3599
|
for (const w of r.warnings) warn(w);
|
|
3177
|
-
for (const n of r.notices) console.log(
|
|
3600
|
+
for (const n of r.notices) console.log(chalk10.yellow(` notice ${n.text} (${n.at})`));
|
|
3178
3601
|
}
|
|
3179
3602
|
function registerStart(program2) {
|
|
3180
3603
|
program2.command("start").description(
|
|
@@ -3206,36 +3629,47 @@ function registerStart(program2) {
|
|
|
3206
3629
|
}
|
|
3207
3630
|
|
|
3208
3631
|
// src/commands/status.ts
|
|
3209
|
-
import
|
|
3632
|
+
import chalk11 from "chalk";
|
|
3210
3633
|
var age = (iso) => {
|
|
3211
3634
|
const mins = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 6e4));
|
|
3212
3635
|
return mins < 60 ? `${mins}m` : mins < 1440 ? `${Math.round(mins / 60)}h` : `${Math.round(mins / 1440)}d`;
|
|
3213
3636
|
};
|
|
3214
3637
|
var expiresIn = (iso) => {
|
|
3215
3638
|
const mins = Math.round((Date.parse(iso) - Date.now()) / 6e4);
|
|
3216
|
-
return mins <= 0 ?
|
|
3639
|
+
return mins <= 0 ? chalk11.red("expired") : mins < 60 ? `${mins}m` : `${Math.round(mins / 60)}h`;
|
|
3217
3640
|
};
|
|
3218
3641
|
function printStatus(r) {
|
|
3219
|
-
console.log(
|
|
3220
|
-
const cols = [
|
|
3642
|
+
console.log(chalk11.bold(`${r.project}`));
|
|
3643
|
+
const cols = [
|
|
3644
|
+
"AREA",
|
|
3645
|
+
"TYPE/MODE",
|
|
3646
|
+
"BRANCH",
|
|
3647
|
+
"OWNER",
|
|
3648
|
+
"STARTED",
|
|
3649
|
+
"LEASES",
|
|
3650
|
+
"PR",
|
|
3651
|
+
"BEHIND",
|
|
3652
|
+
"NOTICES"
|
|
3653
|
+
];
|
|
3221
3654
|
const rows = r.areas.map((a) => [
|
|
3222
3655
|
a.area,
|
|
3223
|
-
a.effort ? `${a.effort.type}/${a.effort.mode}` :
|
|
3224
|
-
a.effort?.branch ??
|
|
3225
|
-
a.effort?.owner.session ??
|
|
3226
|
-
a.effort ? age(a.effort.started) :
|
|
3227
|
-
a.leases.length ? a.leases.map((l) => `${l.kind}:${l.value}`).join(",") :
|
|
3228
|
-
a.
|
|
3229
|
-
a.
|
|
3656
|
+
a.effort ? `${a.effort.type}/${a.effort.mode}` : chalk11.gray("-"),
|
|
3657
|
+
a.effort?.branch ?? chalk11.gray("-"),
|
|
3658
|
+
a.effort?.owner.session ?? chalk11.gray("-"),
|
|
3659
|
+
a.effort ? age(a.effort.started) : chalk11.gray("-"),
|
|
3660
|
+
a.leases.length ? a.leases.map((l) => `${l.kind}:${l.value}`).join(",") : chalk11.gray("-"),
|
|
3661
|
+
a.effort?.pr ? `#${a.effort.pr.number}${a.effort.pr.draft ? " draft" : ""}` : chalk11.gray("-"),
|
|
3662
|
+
a.behindBase > 0 ? chalk11.yellow(String(a.behindBase)) : "0",
|
|
3663
|
+
a.notices.length ? chalk11.yellow(String(a.notices.length)) : "0"
|
|
3230
3664
|
]);
|
|
3231
3665
|
const plain = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3232
3666
|
const widths = cols.map((c, i) => Math.max(c.length, ...rows.map((row) => plain(row[i]).length)));
|
|
3233
3667
|
const line = (cells) => cells.map((c, i) => c + " ".repeat(widths[i] - plain(c).length)).join(" ");
|
|
3234
|
-
console.log(
|
|
3668
|
+
console.log(chalk11.bold(line(cols)));
|
|
3235
3669
|
for (const row of rows) console.log(line(row));
|
|
3236
3670
|
if (r.leases.length) {
|
|
3237
3671
|
console.log("");
|
|
3238
|
-
console.log(
|
|
3672
|
+
console.log(chalk11.bold("KIND VALUE OWNER AGE EXPIRES-IN"));
|
|
3239
3673
|
for (const l of r.leases) {
|
|
3240
3674
|
console.log(
|
|
3241
3675
|
`${l.kind.padEnd(10)} ${String(l.value).padEnd(12)} ${l.owner.session.padEnd(21)} ${age(l.created).padEnd(5)} ${expiresIn(l.expires)}`
|
|
@@ -3260,13 +3694,14 @@ function registerStatus(program2) {
|
|
|
3260
3694
|
var { program } = createCli({
|
|
3261
3695
|
name: "work",
|
|
3262
3696
|
version: VERSION,
|
|
3263
|
-
description: "Area-based work: start, promote, status, finish, reap, retire, audit."
|
|
3697
|
+
description: "Area-based work: start, promote, status, pr, finish, reap, retire, audit."
|
|
3264
3698
|
});
|
|
3265
3699
|
registerInit(program);
|
|
3266
3700
|
registerStart(program);
|
|
3267
3701
|
registerPromote(program);
|
|
3268
3702
|
registerStatus(program);
|
|
3269
3703
|
registerEnv(program);
|
|
3704
|
+
registerPr(program);
|
|
3270
3705
|
registerFinish(program);
|
|
3271
3706
|
registerReap(program);
|
|
3272
3707
|
registerRetire(program);
|