@brainervirus/workit-claude-code 7.2.0 → 7.3.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/.claude-plugin/plugin.json +1 -1
- package/dist/workit.js +1095 -106
- package/package.json +3 -3
- package/skills/fanout/SKILL.md +29 -20
- package/skills/fanout/references/brief.md +45 -0
package/dist/workit.js
CHANGED
|
@@ -65,7 +65,7 @@ var package_default;
|
|
|
65
65
|
var init_package = __esm(() => {
|
|
66
66
|
package_default = {
|
|
67
67
|
name: "@brainervirus/workit-cli",
|
|
68
|
-
version: "7.
|
|
68
|
+
version: "7.3.0",
|
|
69
69
|
private: false,
|
|
70
70
|
description: "Workit CLI — setup wizard, doctor, and task control for agentic coding workflows",
|
|
71
71
|
keywords: [
|
|
@@ -103,9 +103,9 @@ var init_package = __esm(() => {
|
|
|
103
103
|
},
|
|
104
104
|
dependencies: {
|
|
105
105
|
"@babel/parser": "7.29.9",
|
|
106
|
-
"@brainervirus/workit-core": "^7.
|
|
107
|
-
"@brainervirus/workit-cursor": "^7.
|
|
108
|
-
"@brainervirus/workit-opencode": "^7.
|
|
106
|
+
"@brainervirus/workit-core": "^7.3.0",
|
|
107
|
+
"@brainervirus/workit-cursor": "^7.3.0",
|
|
108
|
+
"@brainervirus/workit-opencode": "^7.3.0",
|
|
109
109
|
"@inkjs/ui": "2.0.0",
|
|
110
110
|
ink: "7.1.1",
|
|
111
111
|
react: "19.2.8"
|
|
@@ -73017,6 +73017,20 @@ function parseDuration2(value) {
|
|
|
73017
73017
|
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
|
|
73018
73018
|
return Math.round(Number(match[1]) * factor);
|
|
73019
73019
|
}
|
|
73020
|
+
function releaseTrunk(io, track, branch) {
|
|
73021
|
+
const resolved = vcsConfig("resolve", io.cwd, {
|
|
73022
|
+
track,
|
|
73023
|
+
...branch ? { branch } : {}
|
|
73024
|
+
});
|
|
73025
|
+
if (resolved.ok === false)
|
|
73026
|
+
return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null, tracked: false };
|
|
73027
|
+
if (resolved.releaseTrack?.blocking)
|
|
73028
|
+
return { error: String(resolved.releaseTrack.blocking) };
|
|
73029
|
+
return {
|
|
73030
|
+
trunk: String(resolved.defaultTargetBranch ?? "") || null,
|
|
73031
|
+
tracked: Boolean(resolved.releaseTrack)
|
|
73032
|
+
};
|
|
73033
|
+
}
|
|
73020
73034
|
function connect(io, branch) {
|
|
73021
73035
|
const resolved = resolveForge(io.cwd, {
|
|
73022
73036
|
branch,
|
|
@@ -73084,6 +73098,7 @@ function renderStatus(doc) {
|
|
|
73084
73098
|
var forgeDeps, usage3 = (io, message, line) => emit(io, fail("invalid_input", message, { unblock: line })), forgeFail = (io, result, data) => emit(io, fail(result.code, result.error, { data, unblock: result.unblock })), short = (sha) => sha ? sha.slice(0, 7) : "?";
|
|
73085
73099
|
var init_forge_common = __esm(() => {
|
|
73086
73100
|
init_resolve();
|
|
73101
|
+
init_vcs_config();
|
|
73087
73102
|
init_output();
|
|
73088
73103
|
forgeDeps = {
|
|
73089
73104
|
runner: undefined,
|
|
@@ -75207,7 +75222,7 @@ async function plan(argv, io) {
|
|
|
75207
75222
|
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
|
|
75208
75223
|
let trunk = flags.values.trunk ?? existing?.trunk ?? null;
|
|
75209
75224
|
if (trunk === null || track !== null) {
|
|
75210
|
-
const derived =
|
|
75225
|
+
const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
|
|
75211
75226
|
if ("error" in derived)
|
|
75212
75227
|
return emit(io, fail("blocked", derived.error, {
|
|
75213
75228
|
unblock: "workit stack plan --track <name> … # or --trunk <branch>"
|
|
@@ -75375,17 +75390,7 @@ async function run14(argv, io) {
|
|
|
75375
75390
|
return land(rest, io);
|
|
75376
75391
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
|
|
75377
75392
|
}
|
|
75378
|
-
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })),
|
|
75379
|
-
const resolved = vcsConfig("resolve", io.cwd, {
|
|
75380
|
-
track,
|
|
75381
|
-
...branch ? { branch } : {}
|
|
75382
|
-
});
|
|
75383
|
-
if (resolved.ok === false)
|
|
75384
|
-
return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
|
|
75385
|
-
if (resolved.releaseTrack?.blocking)
|
|
75386
|
-
return { error: String(resolved.releaseTrack.blocking) };
|
|
75387
|
-
return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
|
|
75388
|
-
}, label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
75393
|
+
var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<bottom> … <top>] [--json]", STATUS_USAGE2 = "workit stack status [--name <n>] [--json]", SYNC_USAGE = "workit stack sync [--name <n>] [--local] [--dry-run] [--force <branch>]… [--json]", LAND_USAGE = "workit stack land [--name <n>] [--dry-run] [--max <n>] [--method squash|merge|rebase] [--timeout 20m] [--interval 30s] [--json]", USAGE7 = "workit stack plan|status|sync|land ... (workit help stack)", short4 = (sha) => sha ? sha.slice(0, 12) : "?", stackFailed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), label = (forge, pr) => pr === null ? "no PR" : `${forge === "gitlab" ? "MR !" : "PR #"}${pr}`, renderSync = (data) => [
|
|
75389
75394
|
`${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
|
|
75390
75395
|
...data.steps.map((step) => [
|
|
75391
75396
|
` ${step.branch} on ${step.parent}:`,
|
|
@@ -75397,16 +75402,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
75397
75402
|
var init_stack2 = __esm(() => {
|
|
75398
75403
|
init_ledger();
|
|
75399
75404
|
init_stack();
|
|
75400
|
-
init_vcs_config();
|
|
75401
75405
|
init_output();
|
|
75402
75406
|
init_forge_common();
|
|
75403
75407
|
METHODS3 = new Set(["squash", "merge", "rebase"]);
|
|
75404
75408
|
});
|
|
75405
75409
|
|
|
75410
|
+
// packages/workit-core/src/fanout.ts
|
|
75411
|
+
import { spawnSync as spawnSync20 } from "node:child_process";
|
|
75412
|
+
import fs22 from "node:fs";
|
|
75413
|
+
import path42 from "node:path";
|
|
75414
|
+
function scopeSamples(pattern) {
|
|
75415
|
+
if (!hasWildcard(pattern))
|
|
75416
|
+
return [unescape2(pattern)];
|
|
75417
|
+
let out = "";
|
|
75418
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
75419
|
+
const char = pattern[index];
|
|
75420
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
75421
|
+
out += pattern[index + 1];
|
|
75422
|
+
index += 1;
|
|
75423
|
+
} else if (char === "*" && pattern[index + 1] === "*") {
|
|
75424
|
+
const atSegmentStart = index === 0 || pattern[index - 1] === "/";
|
|
75425
|
+
if (atSegmentStart && pattern[index + 2] === "/")
|
|
75426
|
+
index += 2;
|
|
75427
|
+
else {
|
|
75428
|
+
out += "<any>";
|
|
75429
|
+
index += 1;
|
|
75430
|
+
}
|
|
75431
|
+
} else if (char === "*")
|
|
75432
|
+
out += "<any>";
|
|
75433
|
+
else if (char === "?")
|
|
75434
|
+
out += "x";
|
|
75435
|
+
else if (char === "{" && pattern.indexOf("}", index) > index) {
|
|
75436
|
+
const close = pattern.indexOf("}", index);
|
|
75437
|
+
out += pattern.slice(index + 1, close).split(",")[0];
|
|
75438
|
+
index = close;
|
|
75439
|
+
} else
|
|
75440
|
+
out += char;
|
|
75441
|
+
}
|
|
75442
|
+
return [out];
|
|
75443
|
+
}
|
|
75444
|
+
function globSource(pattern) {
|
|
75445
|
+
let out = "";
|
|
75446
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
75447
|
+
const char = pattern[index];
|
|
75448
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
75449
|
+
out += escapeRegExp4(pattern[index + 1]);
|
|
75450
|
+
index += 1;
|
|
75451
|
+
} else if (char === "*" && pattern[index + 1] === "*") {
|
|
75452
|
+
const atSegmentStart = index === 0 || pattern[index - 1] === "/";
|
|
75453
|
+
if (atSegmentStart && pattern[index + 2] === "/") {
|
|
75454
|
+
out += "(?:[^/]*/)*";
|
|
75455
|
+
index += 2;
|
|
75456
|
+
} else {
|
|
75457
|
+
out += ".*";
|
|
75458
|
+
index += 1;
|
|
75459
|
+
}
|
|
75460
|
+
} else if (char === "*")
|
|
75461
|
+
out += "[^/]*";
|
|
75462
|
+
else if (char === "?")
|
|
75463
|
+
out += "[^/]";
|
|
75464
|
+
else if (char === "{" && pattern.indexOf("}", index) > index) {
|
|
75465
|
+
const close = pattern.indexOf("}", index);
|
|
75466
|
+
const alternatives = pattern.slice(index + 1, close).split(",").map(globSource);
|
|
75467
|
+
out += `(?:${alternatives.join("|")})`;
|
|
75468
|
+
index = close;
|
|
75469
|
+
} else
|
|
75470
|
+
out += escapeRegExp4(char);
|
|
75471
|
+
}
|
|
75472
|
+
return out;
|
|
75473
|
+
}
|
|
75474
|
+
function matchesScope(pattern, file) {
|
|
75475
|
+
let regex = compiled.get(pattern);
|
|
75476
|
+
if (!regex) {
|
|
75477
|
+
regex = new RegExp(`^${globSource(pattern)}$`, "u");
|
|
75478
|
+
compiled.set(pattern, regex);
|
|
75479
|
+
}
|
|
75480
|
+
return regex.test(file) || !hasWildcard(pattern) && file.startsWith(`${unescape2(pattern)}/`);
|
|
75481
|
+
}
|
|
75482
|
+
function normalizePattern(raw) {
|
|
75483
|
+
let pattern = raw.trim().replace(/\\(?![[\]{}*?])/gu, "/");
|
|
75484
|
+
while (pattern.startsWith("./"))
|
|
75485
|
+
pattern = pattern.slice(2);
|
|
75486
|
+
if (pattern === "." || pattern === "")
|
|
75487
|
+
pattern = raw.trim() ? "**" : "";
|
|
75488
|
+
if (pattern.endsWith("/"))
|
|
75489
|
+
pattern = `${pattern}**`;
|
|
75490
|
+
if (!pattern)
|
|
75491
|
+
return { error: "is empty" };
|
|
75492
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/u.test(pattern))
|
|
75493
|
+
return { error: "must be relative to the repository root" };
|
|
75494
|
+
if (pattern.split("/").includes(".."))
|
|
75495
|
+
return { error: "must not contain .." };
|
|
75496
|
+
const bare = pattern.replace(/\\./gu, "");
|
|
75497
|
+
if (/[[\]]/u.test(bare))
|
|
75498
|
+
return {
|
|
75499
|
+
error: 'has an unescaped [ or ]: write \\[ and \\] for literal brackets (app/\\[id\\]/page.tsx; in JSON "app/\\\\[id\\\\]/page.tsx")'
|
|
75500
|
+
};
|
|
75501
|
+
if (bare.startsWith("!"))
|
|
75502
|
+
return { error: "uses ! (negation is not supported)" };
|
|
75503
|
+
let depth = 0;
|
|
75504
|
+
for (const char of bare) {
|
|
75505
|
+
if (char === "{")
|
|
75506
|
+
depth += 1;
|
|
75507
|
+
if (char === "}")
|
|
75508
|
+
depth -= 1;
|
|
75509
|
+
if (depth < 0 || depth > 1)
|
|
75510
|
+
return { error: "has unbalanced or nested braces" };
|
|
75511
|
+
}
|
|
75512
|
+
if (depth !== 0)
|
|
75513
|
+
return { error: "has unbalanced or nested braces" };
|
|
75514
|
+
return { pattern };
|
|
75515
|
+
}
|
|
75516
|
+
function inScope(plan, slice, file) {
|
|
75517
|
+
if (matchesAny(slice.owns, file))
|
|
75518
|
+
return true;
|
|
75519
|
+
if (!matchesAny(slice.scope, file))
|
|
75520
|
+
return false;
|
|
75521
|
+
return !plan.slices.some((other) => other.id !== slice.id && matchesAny(other.owns, file));
|
|
75522
|
+
}
|
|
75523
|
+
function parsePlan(raw, defaults, name) {
|
|
75524
|
+
const problems = [];
|
|
75525
|
+
if (!isRecord8(raw))
|
|
75526
|
+
return { ok: false, problems: ["the plan must be a JSON object"] };
|
|
75527
|
+
const planName = name ?? (typeof raw.name === "string" ? raw.name.trim() : "");
|
|
75528
|
+
if (!planName || planName.length > 200 || /\p{Cc}/u.test(planName))
|
|
75529
|
+
problems.push('name: pass --name or set a 1-200 character "name"');
|
|
75530
|
+
const trunk = typeof raw.trunk === "string" && raw.trunk.trim() ? raw.trunk.trim() : defaults.trunk;
|
|
75531
|
+
const shared = [];
|
|
75532
|
+
for (const entry of textList(raw.shared ?? []) ?? ["<not a list>"]) {
|
|
75533
|
+
const normalized = normalizePattern(entry);
|
|
75534
|
+
if ("error" in normalized)
|
|
75535
|
+
problems.push(`shared "${entry}" ${normalized.error}`);
|
|
75536
|
+
else
|
|
75537
|
+
shared.push(normalized.pattern);
|
|
75538
|
+
}
|
|
75539
|
+
if (!Array.isArray(raw.slices) || raw.slices.length === 0)
|
|
75540
|
+
return { ok: false, problems: [...problems, "slices: list at least one slice"] };
|
|
75541
|
+
const slices = [];
|
|
75542
|
+
const seenIds = new Set;
|
|
75543
|
+
const seenBranches = new Map;
|
|
75544
|
+
raw.slices.forEach((item, index) => {
|
|
75545
|
+
const where = isRecord8(item) && typeof item.id === "string" ? `slice ${item.id}` : `slice #${index + 1}`;
|
|
75546
|
+
if (!isRecord8(item)) {
|
|
75547
|
+
problems.push(`${where}: must be an object`);
|
|
75548
|
+
return;
|
|
75549
|
+
}
|
|
75550
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
75551
|
+
if (!ID.test(id))
|
|
75552
|
+
problems.push(`${where}: id must match ${ID.source}`);
|
|
75553
|
+
else if (seenIds.has(id))
|
|
75554
|
+
problems.push(`${where}: duplicate id`);
|
|
75555
|
+
seenIds.add(id);
|
|
75556
|
+
const branch = typeof item.branch === "string" ? item.branch.trim() : "";
|
|
75557
|
+
if (!branch)
|
|
75558
|
+
problems.push(`${where}: branch is empty`);
|
|
75559
|
+
else if (seenBranches.has(branch))
|
|
75560
|
+
problems.push(`${where}: branch ${branch} is also slice ${seenBranches.get(branch)}'s`);
|
|
75561
|
+
else
|
|
75562
|
+
seenBranches.set(branch, id);
|
|
75563
|
+
if (branch === trunk)
|
|
75564
|
+
problems.push(`${where}: branch must not be the trunk ${trunk}`);
|
|
75565
|
+
const goal = typeof item.goal === "string" ? item.goal.trim() : "";
|
|
75566
|
+
if (!filled(goal))
|
|
75567
|
+
problems.push(`${where}: goal is empty`);
|
|
75568
|
+
const brief = (field) => {
|
|
75569
|
+
const list = textList(item[field]);
|
|
75570
|
+
const kept = (list ?? []).map((entry) => entry.trim()).filter(filled);
|
|
75571
|
+
if (kept.length === 0)
|
|
75572
|
+
problems.push(`${where}: ${field} is empty`);
|
|
75573
|
+
return kept;
|
|
75574
|
+
};
|
|
75575
|
+
const acceptance = brief("acceptance");
|
|
75576
|
+
const verify = brief("verify");
|
|
75577
|
+
const forbidden = brief("forbidden");
|
|
75578
|
+
const scope = [];
|
|
75579
|
+
const owns = [];
|
|
75580
|
+
for (const [field, out] of [
|
|
75581
|
+
["scope", scope],
|
|
75582
|
+
["owns", owns]
|
|
75583
|
+
]) {
|
|
75584
|
+
const list = textList(item[field] ?? []);
|
|
75585
|
+
if (list === null) {
|
|
75586
|
+
problems.push(`${where}: ${field} must be a list of globs`);
|
|
75587
|
+
continue;
|
|
75588
|
+
}
|
|
75589
|
+
for (const entry of list.filter(filled)) {
|
|
75590
|
+
const normalized = normalizePattern(entry);
|
|
75591
|
+
if ("error" in normalized)
|
|
75592
|
+
problems.push(`${where}: ${field} "${entry}" ${normalized.error}`);
|
|
75593
|
+
else
|
|
75594
|
+
out.push(normalized.pattern);
|
|
75595
|
+
}
|
|
75596
|
+
}
|
|
75597
|
+
if (scope.length === 0 && textList(item.scope ?? []) !== null)
|
|
75598
|
+
problems.push(`${where}: scope is empty`);
|
|
75599
|
+
const tier = item.tier;
|
|
75600
|
+
if (typeof tier !== "string" || !TIERS.includes(tier))
|
|
75601
|
+
problems.push(`${where}: tier must be one of ${TIERS.join(", ")}`);
|
|
75602
|
+
const dependsOn = textList(item.dependsOn ?? []);
|
|
75603
|
+
if (dependsOn === null)
|
|
75604
|
+
problems.push(`${where}: dependsOn must be a list of slice ids`);
|
|
75605
|
+
const optional = (field) => typeof item[field] === "string" && filled(item[field]) ? item[field].trim() : null;
|
|
75606
|
+
slices.push({
|
|
75607
|
+
id,
|
|
75608
|
+
branch,
|
|
75609
|
+
base: optional("base") ?? "",
|
|
75610
|
+
worktree: optional("worktree") ?? path42.join(defaults.worktreeRoot, id),
|
|
75611
|
+
tier,
|
|
75612
|
+
dependsOn: [...new Set(dependsOn ?? [])],
|
|
75613
|
+
scope,
|
|
75614
|
+
owns,
|
|
75615
|
+
goal,
|
|
75616
|
+
acceptance,
|
|
75617
|
+
verify,
|
|
75618
|
+
forbidden,
|
|
75619
|
+
context: optional("context"),
|
|
75620
|
+
timebox: optional("timebox")
|
|
75621
|
+
});
|
|
75622
|
+
});
|
|
75623
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75624
|
+
const byBranch = new Map(slices.map((slice) => [slice.branch, slice]));
|
|
75625
|
+
for (const slice of slices) {
|
|
75626
|
+
for (const dep of slice.dependsOn) {
|
|
75627
|
+
if (dep === slice.id)
|
|
75628
|
+
problems.push(`slice ${slice.id}: depends on itself`);
|
|
75629
|
+
else if (!byId.has(dep))
|
|
75630
|
+
problems.push(`slice ${slice.id}: depends on unknown slice ${dep}`);
|
|
75631
|
+
}
|
|
75632
|
+
const explicitBase = slice.base;
|
|
75633
|
+
if (explicitBase) {
|
|
75634
|
+
const parent = byBranch.get(explicitBase);
|
|
75635
|
+
if (parent && !slice.dependsOn.includes(parent.id))
|
|
75636
|
+
problems.push(`slice ${slice.id}: base ${explicitBase} is slice ${parent.id}'s branch; add "${parent.id}" to its dependsOn`);
|
|
75637
|
+
} else {
|
|
75638
|
+
const single = slice.dependsOn.length === 1 ? byId.get(slice.dependsOn[0]) : undefined;
|
|
75639
|
+
slice.base = single ? single.branch : trunk;
|
|
75640
|
+
}
|
|
75641
|
+
}
|
|
75642
|
+
const cycle = findCycle(slices);
|
|
75643
|
+
if (cycle)
|
|
75644
|
+
problems.push(`dependency cycle: ${cycle.join(" -> ")}`);
|
|
75645
|
+
if (problems.length)
|
|
75646
|
+
return { ok: false, problems };
|
|
75647
|
+
return {
|
|
75648
|
+
ok: true,
|
|
75649
|
+
plan: {
|
|
75650
|
+
v: FANOUT_VERSION,
|
|
75651
|
+
name: planName,
|
|
75652
|
+
trunk,
|
|
75653
|
+
shared,
|
|
75654
|
+
slices,
|
|
75655
|
+
createdAt: "",
|
|
75656
|
+
updatedAt: ""
|
|
75657
|
+
}
|
|
75658
|
+
};
|
|
75659
|
+
}
|
|
75660
|
+
function findCycle(slices) {
|
|
75661
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75662
|
+
const state = new Map;
|
|
75663
|
+
const trail = [];
|
|
75664
|
+
const visit = (id) => {
|
|
75665
|
+
if (state.get(id) === "done")
|
|
75666
|
+
return null;
|
|
75667
|
+
if (state.get(id) === "open")
|
|
75668
|
+
return [...trail.slice(trail.indexOf(id)), id];
|
|
75669
|
+
state.set(id, "open");
|
|
75670
|
+
trail.push(id);
|
|
75671
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
75672
|
+
if (!byId.has(dep) || dep === id)
|
|
75673
|
+
continue;
|
|
75674
|
+
const found = visit(dep);
|
|
75675
|
+
if (found)
|
|
75676
|
+
return found;
|
|
75677
|
+
}
|
|
75678
|
+
trail.pop();
|
|
75679
|
+
state.set(id, "done");
|
|
75680
|
+
return null;
|
|
75681
|
+
};
|
|
75682
|
+
for (const slice of slices) {
|
|
75683
|
+
const found = visit(slice.id);
|
|
75684
|
+
if (found)
|
|
75685
|
+
return found;
|
|
75686
|
+
}
|
|
75687
|
+
return null;
|
|
75688
|
+
}
|
|
75689
|
+
function landingOrder(slices) {
|
|
75690
|
+
const order = [];
|
|
75691
|
+
const placed = new Set;
|
|
75692
|
+
const known = new Set(slices.map((slice) => slice.id));
|
|
75693
|
+
while (order.length < slices.length) {
|
|
75694
|
+
const next = slices.find((slice) => !placed.has(slice.id) && slice.dependsOn.every((dep) => placed.has(dep) || !known.has(dep)));
|
|
75695
|
+
if (!next)
|
|
75696
|
+
break;
|
|
75697
|
+
order.push(next.id);
|
|
75698
|
+
placed.add(next.id);
|
|
75699
|
+
}
|
|
75700
|
+
return order;
|
|
75701
|
+
}
|
|
75702
|
+
function waves(slices) {
|
|
75703
|
+
const level = new Map;
|
|
75704
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75705
|
+
for (const id of landingOrder(slices)) {
|
|
75706
|
+
const deps = byId.get(id)?.dependsOn ?? [];
|
|
75707
|
+
level.set(id, deps.length ? Math.max(...deps.map((dep) => level.get(dep) ?? 0)) + 1 : 0);
|
|
75708
|
+
}
|
|
75709
|
+
const out = [];
|
|
75710
|
+
for (const slice of slices)
|
|
75711
|
+
(out[level.get(slice.id) ?? 0] ??= []).push(slice.id);
|
|
75712
|
+
return out.filter(Boolean);
|
|
75713
|
+
}
|
|
75714
|
+
function ancestors(slices) {
|
|
75715
|
+
const byId = new Map(slices.map((slice) => [slice.id, slice]));
|
|
75716
|
+
const out = new Map;
|
|
75717
|
+
for (const id of landingOrder(slices)) {
|
|
75718
|
+
const set = new Set;
|
|
75719
|
+
for (const dep of byId.get(id)?.dependsOn ?? []) {
|
|
75720
|
+
set.add(dep);
|
|
75721
|
+
for (const deeper of out.get(dep) ?? [])
|
|
75722
|
+
set.add(deeper);
|
|
75723
|
+
}
|
|
75724
|
+
out.set(id, set);
|
|
75725
|
+
}
|
|
75726
|
+
return out;
|
|
75727
|
+
}
|
|
75728
|
+
function analyzeOverlap(plan, candidates) {
|
|
75729
|
+
const shared = [...DEFAULT_SHARED, ...plan.shared];
|
|
75730
|
+
const before = ancestors(plan.slices);
|
|
75731
|
+
const related = (a, b) => Boolean(before.get(a)?.has(b) || before.get(b)?.has(a));
|
|
75732
|
+
const overlaps = [];
|
|
75733
|
+
const groups = new Map;
|
|
75734
|
+
const byCase = new Map;
|
|
75735
|
+
for (const file of [...new Set(candidates)].toSorted())
|
|
75736
|
+
(byCase.get(file.toLowerCase()) ?? byCase.set(file.toLowerCase(), []).get(file.toLowerCase()))?.push(file);
|
|
75737
|
+
for (const spellings of byCase.values()) {
|
|
75738
|
+
const file = spellings.join(" ~ ");
|
|
75739
|
+
const any = (patterns) => spellings.some((spelling) => matchesAny(patterns, spelling));
|
|
75740
|
+
const matching = plan.slices.filter((slice) => any(slice.scope) || any(slice.owns));
|
|
75741
|
+
if (matching.length < 2)
|
|
75742
|
+
continue;
|
|
75743
|
+
const ids = matching.map((slice) => slice.id);
|
|
75744
|
+
const owners = matching.filter((slice) => any(slice.owns));
|
|
75745
|
+
if (owners.length === 1) {
|
|
75746
|
+
overlaps.push({ path: file, slices: ids, resolution: "owner", owner: owners[0].id });
|
|
75747
|
+
continue;
|
|
75748
|
+
}
|
|
75749
|
+
if (owners.length === 0 && matching.every((a, i) => matching.slice(i + 1).every((b) => related(a.id, b.id)))) {
|
|
75750
|
+
overlaps.push({ path: file, slices: ids, resolution: "serialized" });
|
|
75751
|
+
continue;
|
|
75752
|
+
}
|
|
75753
|
+
const members = owners.length > 1 ? owners : matching;
|
|
75754
|
+
const reason = owners.length > 1 ? "several_owners" : "overlap";
|
|
75755
|
+
const isShared = any(shared);
|
|
75756
|
+
const key = `${reason}|${isShared}|${members.map((slice) => slice.id).join(",")}`;
|
|
75757
|
+
const group = groups.get(key);
|
|
75758
|
+
if (group)
|
|
75759
|
+
group.paths.push(file);
|
|
75760
|
+
else
|
|
75761
|
+
groups.set(key, {
|
|
75762
|
+
slices: members.map((slice) => slice.id),
|
|
75763
|
+
paths: [file],
|
|
75764
|
+
shared: isShared,
|
|
75765
|
+
reason,
|
|
75766
|
+
members,
|
|
75767
|
+
suggestion: ownerSuggestion(members[0].id, [])
|
|
75768
|
+
});
|
|
75769
|
+
}
|
|
75770
|
+
const conflicts = [...groups.values()].map(({ members, ...group }) => {
|
|
75771
|
+
const literal = members.find((slice) => slice.scope.some((pattern) => group.paths.includes(pattern)));
|
|
75772
|
+
const owner = ownerSuggestion((literal ?? members[0]).id, group.paths);
|
|
75773
|
+
if (group.reason === "several_owners")
|
|
75774
|
+
return {
|
|
75775
|
+
...group,
|
|
75776
|
+
suggestion: {
|
|
75777
|
+
...owner,
|
|
75778
|
+
text: `keep ${quoteList(group.paths)} in "owns" of slice ${owner.slice} only`
|
|
75779
|
+
}
|
|
75780
|
+
};
|
|
75781
|
+
const serialize = serializeSuggestion(group.slices);
|
|
75782
|
+
return group.shared ? { ...group, suggestion: owner, alternative: serialize } : { ...group, suggestion: serialize, alternative: owner };
|
|
75783
|
+
});
|
|
75784
|
+
return { overlaps, conflicts };
|
|
75785
|
+
}
|
|
75786
|
+
function branchRef(cwd, branch) {
|
|
75787
|
+
if (!branch || branch.startsWith("-"))
|
|
75788
|
+
return null;
|
|
75789
|
+
const local = resolveRef(cwd, `refs/heads/${branch}`);
|
|
75790
|
+
if (local)
|
|
75791
|
+
return local;
|
|
75792
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
75793
|
+
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`);
|
|
75794
|
+
}
|
|
75795
|
+
function trunkRef(cwd, trunk) {
|
|
75796
|
+
if (!trunk || trunk.startsWith("-"))
|
|
75797
|
+
return null;
|
|
75798
|
+
const remote = pushRemoteName(cwd) ?? "origin";
|
|
75799
|
+
for (const ref of [`refs/remotes/${remote}/${trunk}`, `refs/heads/${trunk}`, trunk])
|
|
75800
|
+
if (resolveRef(cwd, ref))
|
|
75801
|
+
return ref;
|
|
75802
|
+
return null;
|
|
75803
|
+
}
|
|
75804
|
+
function defaultTrunk(cwd) {
|
|
75805
|
+
const base = defaultBase(cwd);
|
|
75806
|
+
return base ? base.replace(/^origin\//u, "") : "main";
|
|
75807
|
+
}
|
|
75808
|
+
function worktreeRoot(cwd) {
|
|
75809
|
+
const common = gitCommonDir(cwd);
|
|
75810
|
+
const main = common && path42.basename(common) === ".git" ? path42.dirname(common) : cwd;
|
|
75811
|
+
return path42.join(path42.dirname(main), `${path42.basename(main)}-wt`);
|
|
75812
|
+
}
|
|
75813
|
+
function fanoutsDir(cwd) {
|
|
75814
|
+
const root = storeRoot(cwd);
|
|
75815
|
+
if (!root.ok)
|
|
75816
|
+
return fanoutFail(root.code, root.error, root.unblock);
|
|
75817
|
+
return { ok: true, data: path42.join(root.value.root, "fanouts") };
|
|
75818
|
+
}
|
|
75819
|
+
function parseStored(raw) {
|
|
75820
|
+
if (!isRecord8(raw) || typeof raw.name !== "string" || typeof raw.trunk !== "string")
|
|
75821
|
+
return null;
|
|
75822
|
+
if (!Array.isArray(raw.slices))
|
|
75823
|
+
return null;
|
|
75824
|
+
const list = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
75825
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
75826
|
+
let globsValid = true;
|
|
75827
|
+
const globs = (value) => list(value).map((entry) => {
|
|
75828
|
+
const normalized = normalizePattern(entry);
|
|
75829
|
+
if ("error" in normalized) {
|
|
75830
|
+
globsValid = false;
|
|
75831
|
+
return entry;
|
|
75832
|
+
}
|
|
75833
|
+
return normalized.pattern;
|
|
75834
|
+
});
|
|
75835
|
+
const slices = [];
|
|
75836
|
+
for (const item of raw.slices) {
|
|
75837
|
+
if (!isRecord8(item) || typeof item.id !== "string" || typeof item.branch !== "string")
|
|
75838
|
+
return null;
|
|
75839
|
+
slices.push({
|
|
75840
|
+
id: item.id,
|
|
75841
|
+
branch: item.branch,
|
|
75842
|
+
base: text(item.base) || raw.trunk,
|
|
75843
|
+
worktree: text(item.worktree),
|
|
75844
|
+
tier: TIERS.includes(item.tier) ? item.tier : "standard",
|
|
75845
|
+
dependsOn: list(item.dependsOn),
|
|
75846
|
+
scope: globs(item.scope),
|
|
75847
|
+
owns: globs(item.owns),
|
|
75848
|
+
goal: text(item.goal),
|
|
75849
|
+
acceptance: list(item.acceptance),
|
|
75850
|
+
verify: list(item.verify),
|
|
75851
|
+
forbidden: list(item.forbidden),
|
|
75852
|
+
context: typeof item.context === "string" ? item.context : null,
|
|
75853
|
+
timebox: typeof item.timebox === "string" ? item.timebox : null
|
|
75854
|
+
});
|
|
75855
|
+
}
|
|
75856
|
+
const shared = globs(raw.shared);
|
|
75857
|
+
const ids = new Set(slices.map((slice) => slice.id));
|
|
75858
|
+
if (!globsValid || ids.size !== slices.length || slices.some((slice) => slice.dependsOn.some((dep) => dep === slice.id || !ids.has(dep))) || findCycle(slices))
|
|
75859
|
+
return null;
|
|
75860
|
+
return {
|
|
75861
|
+
v: typeof raw.v === "number" ? raw.v : FANOUT_VERSION,
|
|
75862
|
+
name: raw.name,
|
|
75863
|
+
trunk: raw.trunk,
|
|
75864
|
+
shared,
|
|
75865
|
+
slices,
|
|
75866
|
+
createdAt: text(raw.createdAt),
|
|
75867
|
+
updatedAt: text(raw.updatedAt)
|
|
75868
|
+
};
|
|
75869
|
+
}
|
|
75870
|
+
function readFanout(cwd, name) {
|
|
75871
|
+
const dir = fanoutsDir(cwd);
|
|
75872
|
+
if (!dir.ok)
|
|
75873
|
+
return dir;
|
|
75874
|
+
const file = path42.join(dir.data, stackFileName(name));
|
|
75875
|
+
let parsed = null;
|
|
75876
|
+
try {
|
|
75877
|
+
parsed = parseStored(JSON.parse(fs22.readFileSync(file, "utf8")));
|
|
75878
|
+
} catch (error) {
|
|
75879
|
+
if (error.code === "ENOENT")
|
|
75880
|
+
return { ok: true, data: null };
|
|
75881
|
+
parsed = null;
|
|
75882
|
+
}
|
|
75883
|
+
return parsed ? { ok: true, data: parsed } : fanoutFail("failed", `fanout file ${file} is not a valid plan`, `workit fanout plan <plan.json> --name ${name} # rewrite it`);
|
|
75884
|
+
}
|
|
75885
|
+
function listFanouts(cwd) {
|
|
75886
|
+
const dir = fanoutsDir(cwd);
|
|
75887
|
+
if (!dir.ok)
|
|
75888
|
+
return dir;
|
|
75889
|
+
let names = [];
|
|
75890
|
+
try {
|
|
75891
|
+
names = fs22.readdirSync(dir.data).filter((entry) => entry.endsWith(".json"));
|
|
75892
|
+
} catch {
|
|
75893
|
+
return { ok: true, data: [] };
|
|
75894
|
+
}
|
|
75895
|
+
const out = [];
|
|
75896
|
+
for (const entry of names) {
|
|
75897
|
+
let parsed = null;
|
|
75898
|
+
try {
|
|
75899
|
+
parsed = parseStored(JSON.parse(fs22.readFileSync(path42.join(dir.data, entry), "utf8")));
|
|
75900
|
+
} catch {
|
|
75901
|
+
parsed = null;
|
|
75902
|
+
}
|
|
75903
|
+
if (parsed && stackFileName(parsed.name) === entry)
|
|
75904
|
+
out.push(parsed);
|
|
75905
|
+
}
|
|
75906
|
+
return { ok: true, data: out.toSorted((a, b) => a.name.localeCompare(b.name)) };
|
|
75907
|
+
}
|
|
75908
|
+
function selectFanout(cwd, name, current) {
|
|
75909
|
+
if (name) {
|
|
75910
|
+
const read = readFanout(cwd, name);
|
|
75911
|
+
if (!read.ok)
|
|
75912
|
+
return read;
|
|
75913
|
+
return read.data ? { ok: true, data: read.data } : fanoutFail("not_found", `no fanout plan named ${name}`, "workit fanout plan <plan.json>");
|
|
75914
|
+
}
|
|
75915
|
+
const all = listFanouts(cwd);
|
|
75916
|
+
if (!all.ok)
|
|
75917
|
+
return all;
|
|
75918
|
+
const holding = all.data.filter((plan) => plan.slices.some((slice) => slice.branch === current));
|
|
75919
|
+
if (holding.length === 1)
|
|
75920
|
+
return { ok: true, data: holding[0] };
|
|
75921
|
+
if (all.data.length === 1)
|
|
75922
|
+
return { ok: true, data: all.data[0] };
|
|
75923
|
+
return all.data.length === 0 ? fanoutFail("not_found", "no fanout plan is recorded here", "workit fanout plan <plan.json>") : fanoutFail("invalid_input", `several fanout plans match; pick one (${all.data.map((plan) => plan.name).join(", ")})`, "pass --name <plan>");
|
|
75924
|
+
}
|
|
75925
|
+
function writeFanout(cwd, plan) {
|
|
75926
|
+
const dir = fanoutsDir(cwd);
|
|
75927
|
+
if (!dir.ok)
|
|
75928
|
+
return dir;
|
|
75929
|
+
const file = path42.join(dir.data, stackFileName(plan.name));
|
|
75930
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
75931
|
+
try {
|
|
75932
|
+
fs22.mkdirSync(dir.data, { recursive: true });
|
|
75933
|
+
fs22.writeFileSync(temp, `${JSON.stringify(plan, null, 2)}
|
|
75934
|
+
`);
|
|
75935
|
+
fs22.renameSync(temp, file);
|
|
75936
|
+
return { ok: true, data: file };
|
|
75937
|
+
} catch (error) {
|
|
75938
|
+
fs22.rmSync(temp, { force: true });
|
|
75939
|
+
return fanoutFail("unavailable", `cannot write ${file}: ${error.message}`);
|
|
75940
|
+
}
|
|
75941
|
+
}
|
|
75942
|
+
function planFanout(cwd, input, now = new Date) {
|
|
75943
|
+
const dir = fanoutsDir(cwd);
|
|
75944
|
+
if (!dir.ok)
|
|
75945
|
+
return dir;
|
|
75946
|
+
const parsed = parsePlan(input.trunk && isRecord8(input.raw) ? { ...input.raw, trunk: input.trunk } : input.raw, { trunk: input.trunk ?? defaultTrunk(cwd), worktreeRoot: worktreeRoot(cwd) }, input.name);
|
|
75947
|
+
if (!parsed.ok)
|
|
75948
|
+
return fanoutFail("invalid_input", `the plan is incomplete: ${parsed.problems[0]}${parsed.problems.length > 1 ? ` (+${parsed.problems.length - 1} more)` : ""}`, "fill every listed field, then run workit fanout plan again", { problems: parsed.problems });
|
|
75949
|
+
const plan = parsed.plan;
|
|
75950
|
+
const badBranches = plan.slices.filter((slice) => !gitRun4(cwd, ["check-ref-format", "--branch", slice.branch]).ok).map((slice) => `slice ${slice.id}: ${slice.branch} is not a valid branch name`);
|
|
75951
|
+
if (badBranches.length)
|
|
75952
|
+
return fanoutFail("invalid_input", badBranches[0], "rename the branch in the plan", {
|
|
75953
|
+
problems: badBranches
|
|
75954
|
+
});
|
|
75955
|
+
const ref = trunkRef(cwd, plan.trunk);
|
|
75956
|
+
const tracked = ref ? nulList2(gitRun4(cwd, ["ls-tree", "-r", "-z", "--name-only", ref]).stdout) : [];
|
|
75957
|
+
const samples = plan.slices.flatMap((slice) => [...slice.scope, ...slice.owns].flatMap(scopeSamples));
|
|
75958
|
+
const { overlaps, conflicts } = analyzeOverlap(plan, [...tracked, ...samples]);
|
|
75959
|
+
if (conflicts.length)
|
|
75960
|
+
return fanoutFail("blocked", `${conflicts.length} file-scope overlap${conflicts.length === 1 ? "" : "s"} between slices: ${conflicts[0].slices.join(" and ")} on ${conflicts[0].paths[0]}${conflicts[0].paths.length > 1 ? ` (+${conflicts[0].paths.length - 1})` : ""}`, conflicts[0].suggestion.text, { name: plan.name, conflicts, overlaps });
|
|
75961
|
+
const existing = readFanout(cwd, plan.name);
|
|
75962
|
+
const previous = existing.ok ? existing.data : null;
|
|
75963
|
+
plan.createdAt = previous?.createdAt || now.toISOString();
|
|
75964
|
+
plan.updatedAt = now.toISOString();
|
|
75965
|
+
const written = writeFanout(cwd, plan);
|
|
75966
|
+
if (!written.ok)
|
|
75967
|
+
return written;
|
|
75968
|
+
appendObserved(cwd, {
|
|
75969
|
+
type: "fanout.planned",
|
|
75970
|
+
actor: input.actor,
|
|
75971
|
+
fanout: plan.name,
|
|
75972
|
+
trunk: plan.trunk,
|
|
75973
|
+
slices: plan.slices.length,
|
|
75974
|
+
ids: plan.slices.slice(0, 20).map((slice) => slice.id)
|
|
75975
|
+
});
|
|
75976
|
+
const grouped = waves(plan.slices);
|
|
75977
|
+
const notes = [];
|
|
75978
|
+
if (!ref)
|
|
75979
|
+
notes.push(`trunk ${plan.trunk} does not resolve here; only sample paths of each glob were compared`);
|
|
75980
|
+
grouped.forEach((wave, index) => {
|
|
75981
|
+
if (wave.length > IN_FLIGHT_CAP)
|
|
75982
|
+
notes.push(`wave ${index + 1} has ${wave.length} slices; keep 4-${IN_FLIGHT_CAP} in flight and queue the rest`);
|
|
75983
|
+
});
|
|
75984
|
+
for (const slice of plan.slices)
|
|
75985
|
+
if (slice.dependsOn.length > 1 && slice.base === plan.trunk)
|
|
75986
|
+
notes.push(`slice ${slice.id} depends on ${slice.dependsOn.join(", ")}: start it from ${plan.trunk} after they land`);
|
|
75987
|
+
return {
|
|
75988
|
+
ok: true,
|
|
75989
|
+
data: {
|
|
75990
|
+
name: plan.name,
|
|
75991
|
+
trunk: plan.trunk,
|
|
75992
|
+
file: written.data,
|
|
75993
|
+
created: previous === null,
|
|
75994
|
+
slices: plan.slices.map(({ id, branch, base, worktree, tier, dependsOn, scope, owns }) => ({
|
|
75995
|
+
id,
|
|
75996
|
+
branch,
|
|
75997
|
+
base,
|
|
75998
|
+
worktree,
|
|
75999
|
+
tier,
|
|
76000
|
+
dependsOn,
|
|
76001
|
+
scope,
|
|
76002
|
+
owns
|
|
76003
|
+
})),
|
|
76004
|
+
overlaps,
|
|
76005
|
+
waves: grouped,
|
|
76006
|
+
landingOrder: landingOrder(plan.slices),
|
|
76007
|
+
notes
|
|
76008
|
+
}
|
|
76009
|
+
};
|
|
76010
|
+
}
|
|
76011
|
+
var FANOUT_VERSION = 1, TIERS, fanoutFail = (code, error, unblock, data) => ({
|
|
76012
|
+
ok: false,
|
|
76013
|
+
code,
|
|
76014
|
+
error,
|
|
76015
|
+
...unblock ? { unblock } : {},
|
|
76016
|
+
...data ? { data } : {}
|
|
76017
|
+
}), DEFAULT_SHARED, escapeRegExp4 = (text) => text.replace(/[.+^$()|[\]\\{}*?]/gu, "\\$&"), unescape2 = (pattern) => pattern.replace(/\\(.)/gu, "$1"), hasWildcard = (pattern) => /[*?{]/u.test(pattern.replace(/\\./gu, "")), compiled, matchesAny = (patterns, file) => patterns.some((pattern) => matchesScope(pattern, file)), isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value), PLACEHOLDER2, filled = (text) => text.trim() !== "" && !PLACEHOLDER2.test(text.trim()), textList = (value) => {
|
|
76018
|
+
if (typeof value === "string")
|
|
76019
|
+
return [value];
|
|
76020
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
76021
|
+
return value;
|
|
76022
|
+
return null;
|
|
76023
|
+
}, ID, quoteList = (items) => items.map((item) => `"${item}"`).join(", "), ownerSuggestion = (slice, paths) => ({
|
|
76024
|
+
kind: "owner",
|
|
76025
|
+
slice,
|
|
76026
|
+
paths,
|
|
76027
|
+
text: `add ${quoteList(paths)} to "owns" of slice ${slice}`
|
|
76028
|
+
}), serializeSuggestion = (ids) => {
|
|
76029
|
+
const edges = ids.slice(1).map((slice, index) => ({ slice, dependsOn: ids[index] }));
|
|
76030
|
+
return {
|
|
76031
|
+
kind: "serialize",
|
|
76032
|
+
edges,
|
|
76033
|
+
text: edges.map((edge) => `add "${edge.dependsOn}" to "dependsOn" of slice ${edge.slice}`).join("; ")
|
|
76034
|
+
};
|
|
76035
|
+
}, gitRun4 = (cwd, args) => {
|
|
76036
|
+
const run = spawnSync20("git", args, {
|
|
76037
|
+
cwd,
|
|
76038
|
+
encoding: "utf8",
|
|
76039
|
+
timeout: GIT_TIMEOUTS.local,
|
|
76040
|
+
killSignal: "SIGKILL",
|
|
76041
|
+
windowsHide: true,
|
|
76042
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
76043
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C", LANGUAGE: "C" }
|
|
76044
|
+
});
|
|
76045
|
+
return {
|
|
76046
|
+
ok: run.status === 0,
|
|
76047
|
+
status: run.status,
|
|
76048
|
+
stdout: run.stdout ?? "",
|
|
76049
|
+
stderr: run.error ? String(run.error.message) : run.stderr ?? ""
|
|
76050
|
+
};
|
|
76051
|
+
}, nulList2 = (text) => text.split("\x00").filter(Boolean), IN_FLIGHT_CAP = 6;
|
|
76052
|
+
var init_fanout = __esm(() => {
|
|
76053
|
+
init_rev();
|
|
76054
|
+
init_ledger();
|
|
76055
|
+
init_stack();
|
|
76056
|
+
TIERS = ["mundane", "standard", "hard"];
|
|
76057
|
+
DEFAULT_SHARED = [
|
|
76058
|
+
"**/package.json",
|
|
76059
|
+
"**/{package-lock.json,npm-shrinkwrap.json,bun.lock,bun.lockb,pnpm-lock.yaml,pnpm-workspace.yaml,yarn.lock}",
|
|
76060
|
+
"**/{Cargo.lock,go.mod,go.sum,poetry.lock,uv.lock,Gemfile.lock,composer.lock}",
|
|
76061
|
+
"**/index.{ts,tsx,js,jsx,mjs,cjs}",
|
|
76062
|
+
"**/tsconfig.json",
|
|
76063
|
+
".github/**",
|
|
76064
|
+
".gitlab-ci.yml",
|
|
76065
|
+
"CHANGELOG.md"
|
|
76066
|
+
];
|
|
76067
|
+
compiled = new Map;
|
|
76068
|
+
PLACEHOLDER2 = /^(?:<[^>]*>|tbd|todo|fixme|\?+|\.\.\.|…)$/iu;
|
|
76069
|
+
ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
76070
|
+
});
|
|
76071
|
+
|
|
76072
|
+
// packages/workit-core/src/fanout-check.ts
|
|
76073
|
+
function mergeConflicts(cwd, a, b) {
|
|
76074
|
+
const run = gitRun4(cwd, [
|
|
76075
|
+
"merge-tree",
|
|
76076
|
+
"--write-tree",
|
|
76077
|
+
"--name-only",
|
|
76078
|
+
"--no-messages",
|
|
76079
|
+
"-z",
|
|
76080
|
+
a,
|
|
76081
|
+
b
|
|
76082
|
+
]);
|
|
76083
|
+
if (run.status === 0)
|
|
76084
|
+
return [];
|
|
76085
|
+
if (run.status === 1)
|
|
76086
|
+
return [...new Set(nulList2(run.stdout).slice(1))];
|
|
76087
|
+
const first = run.stderr.trim().split(`
|
|
76088
|
+
`)[0] || `git merge-tree exited ${run.status}`;
|
|
76089
|
+
return {
|
|
76090
|
+
error: /write-tree|usage: git merge-tree/u.test(run.stderr) ? `${first} (needs git >= 2.38 for merge-tree --write-tree)` : first
|
|
76091
|
+
};
|
|
76092
|
+
}
|
|
76093
|
+
function checkFanout(cwd, plan, options = {}) {
|
|
76094
|
+
const unknown = (options.only ?? []).filter((id) => !plan.slices.some((s) => s.id === id));
|
|
76095
|
+
if (unknown.length)
|
|
76096
|
+
return fanoutFail("invalid_input", `${unknown.join(", ")}: not a slice of fanout ${plan.name}`, `slices: ${plan.slices.map((slice) => slice.id).join(", ")}`);
|
|
76097
|
+
const trunk = options.trunkRef ?? trunkRef(cwd, plan.trunk);
|
|
76098
|
+
if (!SAFE(trunk) || !resolves(cwd, trunk))
|
|
76099
|
+
return fanoutFail("unavailable", `trunk ${options.trunkRef ?? plan.trunk} does not resolve here`, `git fetch, or pass --base <ref>`);
|
|
76100
|
+
const order = landingOrder(plan.slices);
|
|
76101
|
+
const byId = new Map(plan.slices.map((slice) => [slice.id, slice]));
|
|
76102
|
+
const tips = new Map(plan.slices.map((slice) => [slice.id, branchRef(cwd, slice.branch)]));
|
|
76103
|
+
const ledger = readLedger(cwd);
|
|
76104
|
+
const rows = ledger.ok ? ledger.value.rows : [];
|
|
76105
|
+
const checks = new Map;
|
|
76106
|
+
const changedBy = new Map;
|
|
76107
|
+
for (const slice of plan.slices)
|
|
76108
|
+
checks.set(slice.id, inspect(cwd, plan, slice, trunk, tips, rows, changedBy));
|
|
76109
|
+
const conflicts = [];
|
|
76110
|
+
for (let i = 0;i < order.length; i += 1)
|
|
76111
|
+
for (let j = i + 1;j < order.length; j += 1) {
|
|
76112
|
+
const first = order[i];
|
|
76113
|
+
const second = order[j];
|
|
76114
|
+
const a = tips.get(first);
|
|
76115
|
+
const b = tips.get(second);
|
|
76116
|
+
if (!a || !b || isAncestor2(cwd, a, b) || isAncestor2(cwd, b, a))
|
|
76117
|
+
continue;
|
|
76118
|
+
const check = checks.get(second);
|
|
76119
|
+
const theirs = new Map((changedBy.get(first) ?? []).map((file) => [file.toLowerCase(), file]));
|
|
76120
|
+
const cased = (changedBy.get(second) ?? []).flatMap((file) => {
|
|
76121
|
+
const other = theirs.get(file.toLowerCase());
|
|
76122
|
+
return other !== undefined && other !== file ? [`${other} ~ ${file}`] : [];
|
|
76123
|
+
});
|
|
76124
|
+
if (cased.length) {
|
|
76125
|
+
conflicts.push({
|
|
76126
|
+
first,
|
|
76127
|
+
second,
|
|
76128
|
+
paths: cased,
|
|
76129
|
+
caseOnly: true,
|
|
76130
|
+
resolution: `use ${first}'s spelling on ${second} (git mv), then workit fanout check`
|
|
76131
|
+
});
|
|
76132
|
+
check.siblingConflicts.push({ with: first, paths: cased });
|
|
76133
|
+
check.reasons.push(`differs only in case from ${first}: ${cased.join(", ")}`);
|
|
76134
|
+
}
|
|
76135
|
+
const result = mergeConflicts(cwd, a, b);
|
|
76136
|
+
if (!Array.isArray(result)) {
|
|
76137
|
+
check.reasons.push(`cannot merge with ${first}: ${result.error}`);
|
|
76138
|
+
continue;
|
|
76139
|
+
}
|
|
76140
|
+
if (result.length === 0)
|
|
76141
|
+
continue;
|
|
76142
|
+
const related = byId.get(second)?.dependsOn.includes(first);
|
|
76143
|
+
conflicts.push({
|
|
76144
|
+
first,
|
|
76145
|
+
second,
|
|
76146
|
+
paths: result,
|
|
76147
|
+
resolution: related ? `rebase ${second} onto ${byId.get(first)?.branch} (its dependency), then workit fanout check` : `land ${first} first, then rebase ${second} onto ${plan.trunk} and run workit fanout check; or make ${second} depend on ${first}`
|
|
76148
|
+
});
|
|
76149
|
+
check.siblingConflicts.push({ with: first, paths: result });
|
|
76150
|
+
check.reasons.push(`conflicts with ${first} on ${result.join(", ")}`);
|
|
76151
|
+
}
|
|
76152
|
+
for (const id of order) {
|
|
76153
|
+
const check = checks.get(id);
|
|
76154
|
+
check.waitsFor = (byId.get(id)?.dependsOn ?? []).filter((dep) => checks.get(dep)?.status !== "ready");
|
|
76155
|
+
if (check.waitsFor.length)
|
|
76156
|
+
check.reasons.push(`waits for ${check.waitsFor.join(", ")}`);
|
|
76157
|
+
check.status = check.reasons.length ? "blocked" : "ready";
|
|
76158
|
+
}
|
|
76159
|
+
const selected = options.only?.length ? new Set(options.only) : null;
|
|
76160
|
+
const slices = order.filter((id) => !selected || selected.has(id)).map((id) => checks.get(id));
|
|
76161
|
+
const ready = slices.every((check) => check.status === "ready");
|
|
76162
|
+
const blocked = slices.find((check) => check.status === "blocked");
|
|
76163
|
+
const notes = plan.v === FANOUT_VERSION ? [] : [
|
|
76164
|
+
`plan file version ${plan.v} is not ${FANOUT_VERSION}; read leniently (re-plan to rewrite it)`
|
|
76165
|
+
];
|
|
76166
|
+
const next = blocked ? unblockFor(blocked, plan) : `land in this order: ${slices.map((check) => check.id).join(", ")} (workit pr merge each once verified; stacked slices with workit stack land)`;
|
|
76167
|
+
return {
|
|
76168
|
+
ok: true,
|
|
76169
|
+
data: {
|
|
76170
|
+
name: plan.name,
|
|
76171
|
+
trunk: plan.trunk,
|
|
76172
|
+
trunkRef: trunk,
|
|
76173
|
+
ready,
|
|
76174
|
+
slices,
|
|
76175
|
+
conflicts: conflicts.filter((conflict) => !selected || selected.has(conflict.first) || selected.has(conflict.second)),
|
|
76176
|
+
landingOrder: order,
|
|
76177
|
+
notes,
|
|
76178
|
+
next
|
|
76179
|
+
}
|
|
76180
|
+
};
|
|
76181
|
+
}
|
|
76182
|
+
function inspect(cwd, plan, slice, trunk, tips, rows, changedBy) {
|
|
76183
|
+
const head = tips.get(slice.id) ?? null;
|
|
76184
|
+
const check = {
|
|
76185
|
+
id: slice.id,
|
|
76186
|
+
branch: slice.branch,
|
|
76187
|
+
head,
|
|
76188
|
+
base: slice.base,
|
|
76189
|
+
status: "blocked",
|
|
76190
|
+
changed: 0,
|
|
76191
|
+
outOfScope: [],
|
|
76192
|
+
trunkConflicts: [],
|
|
76193
|
+
siblingConflicts: [],
|
|
76194
|
+
waitsFor: [],
|
|
76195
|
+
verdict: { accepted: false, basis: "missing" },
|
|
76196
|
+
reasons: []
|
|
76197
|
+
};
|
|
76198
|
+
if (!head) {
|
|
76199
|
+
check.reasons.push(`branch ${slice.branch} not found (not started, landed, or not fetched)`);
|
|
76200
|
+
return check;
|
|
76201
|
+
}
|
|
76202
|
+
const parent = plan.slices.find((other) => other.branch === slice.base);
|
|
76203
|
+
const baseRef = slice.base === plan.trunk ? trunk : parent ? tips.get(parent.id) ?? null : branchRef(cwd, slice.base);
|
|
76204
|
+
if (!SAFE(baseRef)) {
|
|
76205
|
+
check.reasons.push(`base ${slice.base} not found`);
|
|
76206
|
+
return check;
|
|
76207
|
+
}
|
|
76208
|
+
const diff = gitRun4(cwd, ["diff", "--name-only", "--no-renames", "-z", `${baseRef}...${head}`]);
|
|
76209
|
+
if (!diff.ok) {
|
|
76210
|
+
check.reasons.push(`cannot diff ${slice.base}...${slice.branch}: ${diff.stderr.trim()}`);
|
|
76211
|
+
return check;
|
|
76212
|
+
}
|
|
76213
|
+
const changed = nulList2(diff.stdout);
|
|
76214
|
+
changedBy.set(slice.id, changed);
|
|
76215
|
+
check.changed = changed.length;
|
|
76216
|
+
if (changed.length === 0)
|
|
76217
|
+
check.reasons.push(`no changes beyond ${slice.base}`);
|
|
76218
|
+
check.outOfScope = changed.filter((file) => !inScope(plan, slice, file)).toSorted();
|
|
76219
|
+
if (check.outOfScope.length)
|
|
76220
|
+
check.reasons.push(`edits outside its scope: ${check.outOfScope.join(", ")}`);
|
|
76221
|
+
const trunkResult = mergeConflicts(cwd, trunk, head);
|
|
76222
|
+
if (Array.isArray(trunkResult)) {
|
|
76223
|
+
check.trunkConflicts = trunkResult;
|
|
76224
|
+
if (trunkResult.length)
|
|
76225
|
+
check.reasons.push(`conflicts with ${plan.trunk} on ${trunkResult.join(", ")}`);
|
|
76226
|
+
} else
|
|
76227
|
+
check.reasons.push(`cannot merge with ${plan.trunk}: ${trunkResult.error}`);
|
|
76228
|
+
const verdict = checkVerdicts(cwd, slice.branch, rows);
|
|
76229
|
+
check.verdict = { accepted: verdict.accepted.accepted, basis: verdict.current.basis };
|
|
76230
|
+
return check;
|
|
76231
|
+
}
|
|
76232
|
+
function unblockFor(check, plan) {
|
|
76233
|
+
if (!check.head)
|
|
76234
|
+
return `start or fetch ${check.branch}, then workit fanout check; if it landed, re-plan without slice ${check.id}`;
|
|
76235
|
+
if (check.outOfScope.length)
|
|
76236
|
+
return `move ${check.outOfScope.join(", ")} off ${check.branch} (a follow-up slice), or widen slice ${check.id}'s scope with workit fanout plan, then workit fanout check`;
|
|
76237
|
+
if (check.trunkConflicts.length)
|
|
76238
|
+
return `rebase ${check.branch} onto ${plan.trunk} and resolve ${check.trunkConflicts.join(", ")}, then workit fanout check`;
|
|
76239
|
+
if (check.siblingConflicts.length) {
|
|
76240
|
+
const first = check.siblingConflicts[0].with;
|
|
76241
|
+
return `land ${first} first, then rebase ${check.branch} onto ${plan.trunk} and run workit fanout check`;
|
|
76242
|
+
}
|
|
76243
|
+
if (check.waitsFor.length)
|
|
76244
|
+
return `unblock ${check.waitsFor.join(", ")} first`;
|
|
76245
|
+
return check.reasons[0] ?? "workit fanout check";
|
|
76246
|
+
}
|
|
76247
|
+
var SAFE = (value) => Boolean(value) && !value?.startsWith("-"), isAncestor2 = (cwd, a, b) => gitRun4(cwd, ["merge-base", "--is-ancestor", a, b]).ok, resolves = (cwd, ref) => gitRun4(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).ok;
|
|
76248
|
+
var init_fanout_check = __esm(() => {
|
|
76249
|
+
init_ledger();
|
|
76250
|
+
init_fanout();
|
|
76251
|
+
});
|
|
76252
|
+
|
|
76253
|
+
// packages/workit-cli/src/verbs/fanout.ts
|
|
76254
|
+
var exports_fanout = {};
|
|
76255
|
+
__export(exports_fanout, {
|
|
76256
|
+
run: () => run15
|
|
76257
|
+
});
|
|
76258
|
+
import fs23 from "node:fs";
|
|
76259
|
+
import path43 from "node:path";
|
|
76260
|
+
function renderBlockedPlan(io, result) {
|
|
76261
|
+
if (io.json || result.code !== "blocked" && result.code !== "invalid_input")
|
|
76262
|
+
return;
|
|
76263
|
+
const problems = result.data?.problems;
|
|
76264
|
+
if (Array.isArray(problems))
|
|
76265
|
+
for (const problem of problems)
|
|
76266
|
+
io.stderr(` - ${problem}
|
|
76267
|
+
`);
|
|
76268
|
+
const conflicts = result.data?.conflicts;
|
|
76269
|
+
if (Array.isArray(conflicts))
|
|
76270
|
+
for (const conflict of conflicts)
|
|
76271
|
+
io.stderr(` - ${conflict.slices.join(" + ")}: ${conflict.paths.join(", ")}
|
|
76272
|
+
fix: ${conflict.suggestion.text}${conflict.alternative ? `
|
|
76273
|
+
or: ${conflict.alternative.text}` : ""}
|
|
76274
|
+
`);
|
|
76275
|
+
}
|
|
76276
|
+
function resolveTrunk(io, raw, flag, track) {
|
|
76277
|
+
if (flag !== null)
|
|
76278
|
+
return { trunk: flag };
|
|
76279
|
+
const named = typeof raw === "object" && raw !== null && typeof raw.trunk === "string" ? raw.trunk.trim() || null : null;
|
|
76280
|
+
if (named !== null && track === null)
|
|
76281
|
+
return { trunk: null };
|
|
76282
|
+
const derived = releaseTrunk(io, track, null);
|
|
76283
|
+
if ("error" in derived)
|
|
76284
|
+
return {
|
|
76285
|
+
code: emit(io, fail("blocked", derived.error, {
|
|
76286
|
+
unblock: "workit fanout plan <plan.json> --track <name> # or --trunk <branch>"
|
|
76287
|
+
}))
|
|
76288
|
+
};
|
|
76289
|
+
if (named !== null && derived.trunk !== named)
|
|
76290
|
+
return {
|
|
76291
|
+
code: usage3(io, `the plan names trunk ${named}; --track ${track} targets ${derived.trunk ?? "(none)"}`, PLAN_USAGE2)
|
|
76292
|
+
};
|
|
76293
|
+
return { trunk: derived.tracked || track !== null ? derived.trunk : null };
|
|
76294
|
+
}
|
|
76295
|
+
function plan2(argv, io) {
|
|
76296
|
+
const flags = parseFlags(argv, { name: "value", trunk: "value", track: "value" });
|
|
76297
|
+
if (typeof flags === "string")
|
|
76298
|
+
return usage3(io, flags, PLAN_USAGE2);
|
|
76299
|
+
const track = flags.values.track ?? null;
|
|
76300
|
+
if (track !== null && flags.values.trunk !== undefined)
|
|
76301
|
+
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE2);
|
|
76302
|
+
if (flags.positionals.length !== 1)
|
|
76303
|
+
return usage3(io, flags.positionals.length ? `unexpected argument ${flags.positionals[1]}` : "missing <plan.json>", PLAN_USAGE2);
|
|
76304
|
+
const file = path43.resolve(io.cwd, flags.positionals[0]);
|
|
76305
|
+
let raw;
|
|
76306
|
+
try {
|
|
76307
|
+
raw = JSON.parse(fs23.readFileSync(file, "utf8"));
|
|
76308
|
+
} catch (error) {
|
|
76309
|
+
return usage3(io, `cannot read ${file} as JSON: ${error.message}`, PLAN_USAGE2);
|
|
76310
|
+
}
|
|
76311
|
+
const trunk = resolveTrunk(io, raw, flags.values.trunk ?? null, track);
|
|
76312
|
+
if ("code" in trunk)
|
|
76313
|
+
return trunk.code;
|
|
76314
|
+
const result = planFanout(io.cwd, {
|
|
76315
|
+
raw,
|
|
76316
|
+
name: flags.values.name ?? null,
|
|
76317
|
+
trunk: trunk.trunk,
|
|
76318
|
+
actor: actorFromEnv(io.env)
|
|
76319
|
+
});
|
|
76320
|
+
if (!result.ok) {
|
|
76321
|
+
const code = failed(io, result);
|
|
76322
|
+
renderBlockedPlan(io, result);
|
|
76323
|
+
return code;
|
|
76324
|
+
}
|
|
76325
|
+
return emit(io, ok(result.data), (data) => [
|
|
76326
|
+
`${data.created ? "planned" : "re-planned"} fanout ${data.name} on ${data.trunk}: ${data.slices.length} slice${data.slices.length === 1 ? "" : "s"}`,
|
|
76327
|
+
...data.slices.map((slice) => ` ${slice.id} [${slice.tier}] ${slice.branch} <- ${slice.base}${slice.dependsOn.length ? ` after ${slice.dependsOn.join(", ")}` : ""}
|
|
76328
|
+
worktree ${slice.worktree}`),
|
|
76329
|
+
...data.overlaps.map((overlap) => ` shared ${overlap.path}: ${overlap.resolution === "owner" ? `owned by ${overlap.owner}` : `serialized (${overlap.slices.join(" -> ")})`}`),
|
|
76330
|
+
`waves: ${data.waves.map((wave) => wave.join(", ")).join(" | ")}`,
|
|
76331
|
+
`landing order: ${data.landingOrder.join(", ")}`,
|
|
76332
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
76333
|
+
]);
|
|
76334
|
+
}
|
|
76335
|
+
function check2(argv, io) {
|
|
76336
|
+
const flags = parseFlags(argv, { name: "value", base: "value" });
|
|
76337
|
+
if (typeof flags === "string")
|
|
76338
|
+
return usage3(io, flags, CHECK_USAGE);
|
|
76339
|
+
const selected = selectFanout(io.cwd, flags.values.name ?? null, currentBranch(io.cwd));
|
|
76340
|
+
if (!selected.ok)
|
|
76341
|
+
return failed(io, selected);
|
|
76342
|
+
const result = checkFanout(io.cwd, selected.data, {
|
|
76343
|
+
only: flags.positionals,
|
|
76344
|
+
trunkRef: flags.values.base ?? null
|
|
76345
|
+
});
|
|
76346
|
+
if (!result.ok)
|
|
76347
|
+
return failed(io, result);
|
|
76348
|
+
if (result.data.ready)
|
|
76349
|
+
return emit(io, ok(result.data), renderCheck);
|
|
76350
|
+
if (!io.json)
|
|
76351
|
+
for (const line of renderCheck(result.data))
|
|
76352
|
+
io.stdout(`${line}
|
|
76353
|
+
`);
|
|
76354
|
+
const first = result.data.slices.find((slice) => slice.status === "blocked");
|
|
76355
|
+
return emit(io, fail("blocked", `slice ${first?.id ?? "?"} is not ready: ${first?.reasons[0] ?? "blocked"}`, {
|
|
76356
|
+
data: result.data,
|
|
76357
|
+
unblock: result.data.next
|
|
76358
|
+
}));
|
|
76359
|
+
}
|
|
76360
|
+
async function run15(argv, io) {
|
|
76361
|
+
const [sub, ...rest] = argv.filter((arg) => arg !== "--json");
|
|
76362
|
+
if (sub === "plan")
|
|
76363
|
+
return plan2(rest, io);
|
|
76364
|
+
if (sub === "check")
|
|
76365
|
+
return check2(rest, io);
|
|
76366
|
+
return usage3(io, sub && !sub.startsWith("-") ? `unknown fanout subcommand "${sub}"` : "missing fanout subcommand", USAGE8);
|
|
76367
|
+
}
|
|
76368
|
+
var PLAN_USAGE2 = "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] [--json]", CHECK_USAGE = "workit fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]", USAGE8 = "workit fanout plan|check ... (workit help fanout)", failed = (io, result) => emit(io, fail(result.code, result.error, { unblock: result.unblock, data: result.data ?? {} })), renderCheck = (data) => [
|
|
76369
|
+
`fanout ${data.name} against ${data.trunkRef}: ${data.ready ? "ready to land" : "blocked"}`,
|
|
76370
|
+
...data.slices.map((slice) => ` ${slice.status === "ready" ? "ok " : "x "}${slice.id} (${slice.branch}) ${slice.changed} file${slice.changed === 1 ? "" : "s"} · verdict ${slice.verdict.accepted ? "accepted" : slice.verdict.basis}${slice.reasons.length ? `
|
|
76371
|
+
${slice.reasons.join(`
|
|
76372
|
+
`)}` : ""}`),
|
|
76373
|
+
...data.conflicts.map((conflict) => ` conflict ${conflict.first} x ${conflict.second}: ${conflict.paths.join(", ")}
|
|
76374
|
+
fix: ${conflict.resolution}`),
|
|
76375
|
+
`landing order: ${data.landingOrder.join(", ")}`,
|
|
76376
|
+
`next: ${data.next}`,
|
|
76377
|
+
...data.notes.map((note) => `note: ${note}`)
|
|
76378
|
+
];
|
|
76379
|
+
var init_fanout2 = __esm(() => {
|
|
76380
|
+
init_fanout_check();
|
|
76381
|
+
init_fanout();
|
|
76382
|
+
init_rev();
|
|
76383
|
+
init_ledger();
|
|
76384
|
+
init_output();
|
|
76385
|
+
init_forge_common();
|
|
76386
|
+
});
|
|
76387
|
+
|
|
75406
76388
|
// packages/workit-cli/src/verbs/ledger.ts
|
|
75407
76389
|
var exports_ledger = {};
|
|
75408
76390
|
__export(exports_ledger, {
|
|
75409
|
-
run: () =>
|
|
76391
|
+
run: () => run16
|
|
75410
76392
|
});
|
|
75411
76393
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
75412
76394
|
import { parseArgs } from "node:util";
|
|
@@ -75430,7 +76412,7 @@ function targetBranch(io, rows, branch, pr) {
|
|
|
75430
76412
|
}
|
|
75431
76413
|
return { ok: true, value: branch ?? null };
|
|
75432
76414
|
}
|
|
75433
|
-
async function
|
|
76415
|
+
async function run16(argv, io) {
|
|
75434
76416
|
let parsed;
|
|
75435
76417
|
try {
|
|
75436
76418
|
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
@@ -75458,7 +76440,7 @@ async function run15(argv, io) {
|
|
|
75458
76440
|
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
75459
76441
|
const ledger = readLedger(io.cwd);
|
|
75460
76442
|
if (!ledger.ok)
|
|
75461
|
-
return
|
|
76443
|
+
return failed2(io, ledger);
|
|
75462
76444
|
const { rows } = ledger.value;
|
|
75463
76445
|
if (sub === "list" || sub === "show") {
|
|
75464
76446
|
if (rest.length)
|
|
@@ -75480,7 +76462,7 @@ async function run15(argv, io) {
|
|
|
75480
76462
|
return usage4(io, `unexpected argument: ${rest.at(-1)}`);
|
|
75481
76463
|
const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
|
|
75482
76464
|
if (!target.ok)
|
|
75483
|
-
return
|
|
76465
|
+
return failed2(io, target);
|
|
75484
76466
|
const branch = target.value ?? currentBranch(io.cwd);
|
|
75485
76467
|
if (!branch)
|
|
75486
76468
|
return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
|
|
@@ -75490,7 +76472,7 @@ async function run15(argv, io) {
|
|
|
75490
76472
|
}
|
|
75491
76473
|
const target = targetBranch(io, rows, values.branch, pr);
|
|
75492
76474
|
if (!target.ok)
|
|
75493
|
-
return
|
|
76475
|
+
return failed2(io, target);
|
|
75494
76476
|
const acting = actorFor(io, values);
|
|
75495
76477
|
if (acting instanceof Error)
|
|
75496
76478
|
return usage4(io, acting.message);
|
|
@@ -75529,7 +76511,7 @@ async function run15(argv, io) {
|
|
|
75529
76511
|
..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
|
|
75530
76512
|
})), (row) => `recorded verdict ${row.id}: ${row.result} [${row.kind}] for ${row.branch} @ ${(row.head ?? "").slice(0, 12)} as ${actor.session ?? "no session"}${row.self ? ` (self${row.selfReason === "no_session" ? ": WORKIT_SESSION_ID unset" : ""}; never accepted)` : ""}`);
|
|
75531
76513
|
}
|
|
75532
|
-
var
|
|
76514
|
+
var USAGE9 = "workit ledger decision|ruling|verdict|list|check ... (workit help ledger for the grammar)", OPTIONS, SESSION_SAFE2, ROLE_SAFE, actorFor = (io, values) => {
|
|
75533
76515
|
const actor = actorFromEnv(io.env);
|
|
75534
76516
|
if (values.session !== undefined && values.as !== undefined)
|
|
75535
76517
|
return new Error("pass --session or --as, not both");
|
|
@@ -75553,7 +76535,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
75553
76535
|
return;
|
|
75554
76536
|
const parsed = Number(value);
|
|
75555
76537
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
|
|
75556
|
-
},
|
|
76538
|
+
}, failed2 = (io, result) => emit(io, fail(result.code, result.error, result.unblock ? { unblock: result.unblock } : {})), fromResult = (io, result, human) => result.ok ? emit(io, ok(result.value), human) : failed2(io, result), usage4 = (io, error) => emit(io, fail("invalid_input", error, { unblock: USAGE9 })), verdictLines = (check) => {
|
|
75557
76539
|
const lines = [
|
|
75558
76540
|
`${check.branch} @ ${(check.head ?? "no commit").slice(0, 12)}: current: ${check.current.basis}${typeof check.current.verdict?.result === "string" ? ` (${check.current.verdict.result})` : ""}; ${check.accepted.accepted ? "accepted" : `not accepted (${check.accepted.reasons.join(", ")})`}; review: ${check.review}`
|
|
75559
76541
|
];
|
|
@@ -90550,7 +91532,7 @@ var init_ast = __esm(() => {
|
|
|
90550
91532
|
|
|
90551
91533
|
// packages/workit-cli/src/test-audit/rules.ts
|
|
90552
91534
|
import { builtinModules } from "node:module";
|
|
90553
|
-
import
|
|
91535
|
+
import path44 from "node:path";
|
|
90554
91536
|
function testCallee(call) {
|
|
90555
91537
|
let callee = call.callee;
|
|
90556
91538
|
if (callee.type === "CallExpression")
|
|
@@ -91043,12 +92025,12 @@ function checkConstantsOnly(ctx, emit) {
|
|
|
91043
92025
|
}
|
|
91044
92026
|
function checkOverMocking(ctx, emit) {
|
|
91045
92027
|
const { program } = ctx.parsed;
|
|
91046
|
-
const unit =
|
|
92028
|
+
const unit = path44.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
|
|
91047
92029
|
const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
|
|
91048
92030
|
const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
|
|
91049
92031
|
for (const call of mocked) {
|
|
91050
92032
|
const spec = call.arguments[0].value;
|
|
91051
|
-
const base =
|
|
92033
|
+
const base = path44.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
91052
92034
|
if (base === unit)
|
|
91053
92035
|
emit({
|
|
91054
92036
|
rule: "over-mocking",
|
|
@@ -91436,11 +92418,11 @@ var init_rules = __esm(() => {
|
|
|
91436
92418
|
});
|
|
91437
92419
|
|
|
91438
92420
|
// packages/workit-cli/src/test-audit/audit.ts
|
|
91439
|
-
import { spawnSync as
|
|
92421
|
+
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
91440
92422
|
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
|
|
91441
|
-
import
|
|
92423
|
+
import path45 from "node:path";
|
|
91442
92424
|
function git6(cwd, args) {
|
|
91443
|
-
const run =
|
|
92425
|
+
const run = spawnSync21("git", ["-c", "core.quotepath=off", ...args], {
|
|
91444
92426
|
cwd,
|
|
91445
92427
|
encoding: "utf8",
|
|
91446
92428
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -91451,7 +92433,7 @@ function walkTests(dir, out) {
|
|
|
91451
92433
|
for (const entry of readdirSync8(dir, { withFileTypes: true })) {
|
|
91452
92434
|
if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
|
|
91453
92435
|
continue;
|
|
91454
|
-
const full =
|
|
92436
|
+
const full = path45.join(dir, entry.name);
|
|
91455
92437
|
if (entry.isDirectory()) {
|
|
91456
92438
|
if (!SKIP_DIRS.has(entry.name))
|
|
91457
92439
|
walkTests(full, out);
|
|
@@ -91463,7 +92445,7 @@ function expandPaths(cwd, inputs) {
|
|
|
91463
92445
|
const files = [];
|
|
91464
92446
|
const missing = [];
|
|
91465
92447
|
for (const input of inputs.length > 0 ? inputs : ["."]) {
|
|
91466
|
-
const full =
|
|
92448
|
+
const full = path45.resolve(cwd, input);
|
|
91467
92449
|
if (!existsSync18(full))
|
|
91468
92450
|
missing.push(input);
|
|
91469
92451
|
else if (statSync7(full).isDirectory())
|
|
@@ -91500,11 +92482,11 @@ function changedLines(cwd, base) {
|
|
|
91500
92482
|
for (let offset = 0;offset < count; offset += 1)
|
|
91501
92483
|
lines.add(start + offset);
|
|
91502
92484
|
}
|
|
91503
|
-
changed.set(
|
|
92485
|
+
changed.set(path45.join(root, file), lines);
|
|
91504
92486
|
}
|
|
91505
92487
|
const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
91506
92488
|
for (const file of untracked.stdout.split("\x00").filter(Boolean))
|
|
91507
|
-
changed.set(
|
|
92489
|
+
changed.set(path45.join(root, file), "all");
|
|
91508
92490
|
return changed;
|
|
91509
92491
|
}
|
|
91510
92492
|
function auditFiles(files, display) {
|
|
@@ -91566,7 +92548,7 @@ function auditFiles(files, display) {
|
|
|
91566
92548
|
durationMs: Math.round(performance.now() - started)
|
|
91567
92549
|
};
|
|
91568
92550
|
}
|
|
91569
|
-
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(
|
|
92551
|
+
var TEST_FILE, CODE_FILE, SKIP_DIRS, toPosix = (file) => file.split(path45.sep).join("/"), SEVERITY_ORDER, ruleIds;
|
|
91570
92552
|
var init_audit = __esm(() => {
|
|
91571
92553
|
init_ast();
|
|
91572
92554
|
init_rules();
|
|
@@ -91596,7 +92578,7 @@ import {
|
|
|
91596
92578
|
copyFileSync as copyFileSync6
|
|
91597
92579
|
} from "node:fs";
|
|
91598
92580
|
import os14 from "node:os";
|
|
91599
|
-
import
|
|
92581
|
+
import path46 from "node:path";
|
|
91600
92582
|
function mutantsFor(source, file, lines) {
|
|
91601
92583
|
const parsed = parseSource(source, file);
|
|
91602
92584
|
const out = [];
|
|
@@ -91669,7 +92651,7 @@ function relatedTests(source, testFiles) {
|
|
|
91669
92651
|
for (const match of text.matchAll(SPECIFIER)) {
|
|
91670
92652
|
const spec = match[1];
|
|
91671
92653
|
if (spec.startsWith(".")) {
|
|
91672
|
-
if (stripExt(
|
|
92654
|
+
if (stripExt(path46.posix.join(path46.posix.dirname(file), spec)) === target)
|
|
91673
92655
|
return true;
|
|
91674
92656
|
} else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
|
|
91675
92657
|
return true;
|
|
@@ -91689,12 +92671,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91689
92671
|
for (const name of names) {
|
|
91690
92672
|
if (!name.startsWith(TEMP_PREFIX))
|
|
91691
92673
|
continue;
|
|
91692
|
-
const dir =
|
|
92674
|
+
const dir = path46.join(tmp, name);
|
|
91693
92675
|
try {
|
|
91694
92676
|
const stat = lstatSync7(dir);
|
|
91695
92677
|
if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
|
|
91696
92678
|
continue;
|
|
91697
|
-
const pid = Number(readFileSync20(
|
|
92679
|
+
const pid = Number(readFileSync20(path46.join(dir, PID_FILE), "utf8"));
|
|
91698
92680
|
if (Number.isInteger(pid) && pid > 0 && alive(pid))
|
|
91699
92681
|
continue;
|
|
91700
92682
|
} catch (error) {
|
|
@@ -91711,8 +92693,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91711
92693
|
function linkNodeModules(origin, target, repo, copy) {
|
|
91712
92694
|
mkdirSync9(target, { recursive: true });
|
|
91713
92695
|
for (const entry of readdirSync9(origin, { withFileTypes: true })) {
|
|
91714
|
-
const from =
|
|
91715
|
-
const to =
|
|
92696
|
+
const from = path46.join(origin, entry.name);
|
|
92697
|
+
const to = path46.join(target, entry.name);
|
|
91716
92698
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
91717
92699
|
linkNodeModules(from, to, repo, copy);
|
|
91718
92700
|
continue;
|
|
@@ -91728,9 +92710,9 @@ function linkNodeModules(origin, target, repo, copy) {
|
|
|
91728
92710
|
} catch {
|
|
91729
92711
|
continue;
|
|
91730
92712
|
}
|
|
91731
|
-
const relative =
|
|
91732
|
-
if (!relative.startsWith("..") && !
|
|
91733
|
-
symlinkSync(
|
|
92713
|
+
const relative = path46.relative(repo, real);
|
|
92714
|
+
if (!relative.startsWith("..") && !path46.isAbsolute(relative) && !relative.split(path46.sep).includes("node_modules")) {
|
|
92715
|
+
symlinkSync(path46.join(copy, relative), to, "junction");
|
|
91734
92716
|
continue;
|
|
91735
92717
|
}
|
|
91736
92718
|
}
|
|
@@ -91746,7 +92728,7 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91746
92728
|
}
|
|
91747
92729
|
const realRepo = realpathSync9(repo);
|
|
91748
92730
|
if (real && inside2(realRepo, real)) {
|
|
91749
|
-
symlinkSync(
|
|
92731
|
+
symlinkSync(path46.relative(path46.dirname(to), path46.join(tree, path46.relative(realRepo, real))), to);
|
|
91750
92732
|
return;
|
|
91751
92733
|
}
|
|
91752
92734
|
if (real && statSync8(real).isFile())
|
|
@@ -91755,10 +92737,10 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91755
92737
|
symlinkSync(readlinkSync3(from), to);
|
|
91756
92738
|
}
|
|
91757
92739
|
function writeInside(copy, file, content) {
|
|
91758
|
-
const target =
|
|
92740
|
+
const target = path46.join(copy, file);
|
|
91759
92741
|
try {
|
|
91760
92742
|
const stat = lstatSync7(target);
|
|
91761
|
-
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(
|
|
92743
|
+
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path46.dirname(target))))
|
|
91762
92744
|
return false;
|
|
91763
92745
|
} catch {
|
|
91764
92746
|
return false;
|
|
@@ -91769,9 +92751,9 @@ function writeInside(copy, file, content) {
|
|
|
91769
92751
|
return true;
|
|
91770
92752
|
}
|
|
91771
92753
|
function copyWorkingTree(repo) {
|
|
91772
|
-
const dir = mkdtempSync4(
|
|
91773
|
-
writeFileSync9(
|
|
91774
|
-
const tree =
|
|
92754
|
+
const dir = mkdtempSync4(path46.join(os14.tmpdir(), TEMP_PREFIX));
|
|
92755
|
+
writeFileSync9(path46.join(dir, PID_FILE), String(process.pid));
|
|
92756
|
+
const tree = path46.join(dir, "tree");
|
|
91775
92757
|
const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
|
|
91776
92758
|
if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
|
|
91777
92759
|
const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
|
|
@@ -91788,29 +92770,29 @@ function copyWorkingTree(repo) {
|
|
|
91788
92770
|
for (const relative of listed) {
|
|
91789
92771
|
if (!relative)
|
|
91790
92772
|
continue;
|
|
91791
|
-
const from =
|
|
92773
|
+
const from = path46.join(repo, relative);
|
|
91792
92774
|
let stat;
|
|
91793
92775
|
try {
|
|
91794
92776
|
stat = lstatSync7(from);
|
|
91795
92777
|
} catch {
|
|
91796
92778
|
continue;
|
|
91797
92779
|
}
|
|
91798
|
-
const to =
|
|
91799
|
-
mkdirSync9(
|
|
92780
|
+
const to = path46.join(tree, relative);
|
|
92781
|
+
mkdirSync9(path46.dirname(to), { recursive: true });
|
|
91800
92782
|
if (stat.isSymbolicLink())
|
|
91801
92783
|
copySymlink(repo, tree, from, to);
|
|
91802
92784
|
else if (stat.isFile())
|
|
91803
92785
|
copyFileSync6(from, to);
|
|
91804
|
-
let parent =
|
|
92786
|
+
let parent = path46.dirname(relative);
|
|
91805
92787
|
while (parent !== "." && !dirs.has(parent)) {
|
|
91806
92788
|
dirs.add(parent);
|
|
91807
|
-
parent =
|
|
92789
|
+
parent = path46.dirname(parent);
|
|
91808
92790
|
}
|
|
91809
92791
|
}
|
|
91810
92792
|
for (const parent of dirs) {
|
|
91811
|
-
const modules =
|
|
91812
|
-
if (existsSync19(modules) && !existsSync19(
|
|
91813
|
-
linkNodeModules(modules,
|
|
92793
|
+
const modules = path46.join(repo, parent, "node_modules");
|
|
92794
|
+
if (existsSync19(modules) && !existsSync19(path46.join(tree, parent, "node_modules")))
|
|
92795
|
+
linkNodeModules(modules, path46.join(tree, parent, "node_modules"), repo, tree);
|
|
91814
92796
|
}
|
|
91815
92797
|
return { dir, tree };
|
|
91816
92798
|
}
|
|
@@ -91849,14 +92831,14 @@ async function runMutation(options) {
|
|
|
91849
92831
|
const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
|
|
91850
92832
|
const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
|
|
91851
92833
|
try {
|
|
91852
|
-
return { file, text: readFileSync20(
|
|
92834
|
+
return { file, text: readFileSync20(path46.join(repo, file), "utf8") };
|
|
91853
92835
|
} catch {
|
|
91854
92836
|
return { file, text: "" };
|
|
91855
92837
|
}
|
|
91856
92838
|
});
|
|
91857
92839
|
const candidates = [];
|
|
91858
92840
|
for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
91859
|
-
const relative = toPosix(
|
|
92841
|
+
const relative = toPosix(path46.relative(repo, absolute));
|
|
91860
92842
|
if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
|
|
91861
92843
|
continue;
|
|
91862
92844
|
if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
|
|
@@ -91870,9 +92852,9 @@ async function runMutation(options) {
|
|
|
91870
92852
|
for (const mutant of mutants)
|
|
91871
92853
|
if (!related.has(mutant.file))
|
|
91872
92854
|
related.set(mutant.file, relatedTests(mutant.file, testFiles));
|
|
91873
|
-
const cwdRelative =
|
|
92855
|
+
const cwdRelative = path46.relative(repo, options.cwd);
|
|
91874
92856
|
const asArg = (file) => {
|
|
91875
|
-
const relative = toPosix(
|
|
92857
|
+
const relative = toPosix(path46.relative(cwdRelative, file));
|
|
91876
92858
|
return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
|
|
91877
92859
|
};
|
|
91878
92860
|
const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
|
|
@@ -91907,7 +92889,7 @@ async function runMutation(options) {
|
|
|
91907
92889
|
return finish2(options, 0, results, counts, started);
|
|
91908
92890
|
removeStaleCopies();
|
|
91909
92891
|
const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
|
|
91910
|
-
const copyCwd =
|
|
92892
|
+
const copyCwd = path46.join(copy, cwdRelative);
|
|
91911
92893
|
let current = null;
|
|
91912
92894
|
const restore = () => {
|
|
91913
92895
|
if (current)
|
|
@@ -91942,7 +92924,7 @@ async function runMutation(options) {
|
|
|
91942
92924
|
record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
|
|
91943
92925
|
continue;
|
|
91944
92926
|
}
|
|
91945
|
-
const original = readFileSync20(
|
|
92927
|
+
const original = readFileSync20(path46.join(copy, mutant.file), "utf8");
|
|
91946
92928
|
const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
|
|
91947
92929
|
current = { file: mutant.file, original };
|
|
91948
92930
|
if (!writeInside(copy, mutant.file, mutated)) {
|
|
@@ -91999,8 +92981,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
|
|
|
91999
92981
|
return error.code === "EPERM";
|
|
92000
92982
|
}
|
|
92001
92983
|
}, inside2 = (root, file) => {
|
|
92002
|
-
const relative =
|
|
92003
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
92984
|
+
const relative = path46.relative(root, file);
|
|
92985
|
+
return relative === "" || !relative.startsWith("..") && !path46.isAbsolute(relative);
|
|
92004
92986
|
}, active, killTree = (child) => {
|
|
92005
92987
|
try {
|
|
92006
92988
|
if (child.pid && process.platform !== "win32")
|
|
@@ -92046,10 +93028,10 @@ var init_mutate = __esm(() => {
|
|
|
92046
93028
|
// packages/workit-cli/src/verbs/test-audit.ts
|
|
92047
93029
|
var exports_test_audit = {};
|
|
92048
93030
|
__export(exports_test_audit, {
|
|
92049
|
-
run: () =>
|
|
93031
|
+
run: () => run17
|
|
92050
93032
|
});
|
|
92051
93033
|
import { existsSync as existsSync20 } from "node:fs";
|
|
92052
|
-
import
|
|
93034
|
+
import path47 from "node:path";
|
|
92053
93035
|
function parse7(argv) {
|
|
92054
93036
|
const options = {
|
|
92055
93037
|
paths: [],
|
|
@@ -92143,14 +93125,14 @@ function human(data) {
|
|
|
92143
93125
|
}
|
|
92144
93126
|
return lines;
|
|
92145
93127
|
}
|
|
92146
|
-
async function
|
|
93128
|
+
async function run17(argv, io) {
|
|
92147
93129
|
const options = parse7(argv);
|
|
92148
93130
|
if (typeof options === "string")
|
|
92149
93131
|
return emit(io, fail("invalid_input", `${options}
|
|
92150
|
-
usage: ${
|
|
93132
|
+
usage: ${USAGE10}`));
|
|
92151
93133
|
const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
|
|
92152
|
-
const repo = top.ok ?
|
|
92153
|
-
const display = (file) => toPosix(
|
|
93134
|
+
const repo = top.ok ? path47.resolve(top.stdout.trim()) : null;
|
|
93135
|
+
const display = (file) => toPosix(path47.relative(io.cwd, file) || file);
|
|
92154
93136
|
let base = null;
|
|
92155
93137
|
let changed = null;
|
|
92156
93138
|
if (options.diff || options.mutate) {
|
|
@@ -92233,7 +93215,7 @@ usage: ${USAGE9}`));
|
|
|
92233
93215
|
}
|
|
92234
93216
|
return emit(io, ok(data), human);
|
|
92235
93217
|
}
|
|
92236
|
-
var
|
|
93218
|
+
var USAGE10 = "workit test-audit [paths…] [--diff [base]] [--rule <ids>] [--min-severity <level>] [--fail-on <level>] [--mutate [--test-cmd <cmd>] [--max-mutants <n>] [--budget <s>] [--timeout <s>]] [--json]", LEVELS, VALUE_FLAGS2, quote = (arg) => /^[\w./@:=+-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
92237
93219
|
var init_test_audit = __esm(() => {
|
|
92238
93220
|
init_audit();
|
|
92239
93221
|
init_mutate();
|
|
@@ -92358,14 +93340,14 @@ var init_youtrack_writes = __esm(() => {
|
|
|
92358
93340
|
// packages/workit-cli/src/verbs/youtrack.ts
|
|
92359
93341
|
var exports_youtrack = {};
|
|
92360
93342
|
__export(exports_youtrack, {
|
|
92361
|
-
run: () =>
|
|
93343
|
+
run: () => run18
|
|
92362
93344
|
});
|
|
92363
93345
|
import { readFileSync as readFileSync21 } from "node:fs";
|
|
92364
|
-
import
|
|
92365
|
-
async function
|
|
93346
|
+
import path48 from "node:path";
|
|
93347
|
+
async function run18(argv, io) {
|
|
92366
93348
|
const [sub, ...rest] = argv;
|
|
92367
93349
|
if (sub !== "note" && sub !== "time" && sub !== "meeting")
|
|
92368
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93350
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
|
|
92369
93351
|
const flags = parseFlags(rest, {
|
|
92370
93352
|
markdown: "value",
|
|
92371
93353
|
file: "value",
|
|
@@ -92374,18 +93356,18 @@ async function run17(argv, io) {
|
|
|
92374
93356
|
date: "value"
|
|
92375
93357
|
});
|
|
92376
93358
|
if (typeof flags === "string")
|
|
92377
|
-
return usage3(io, flags,
|
|
93359
|
+
return usage3(io, flags, USAGE11);
|
|
92378
93360
|
const [issueId, extra] = flags.positionals;
|
|
92379
93361
|
if (!issueId || !ISSUE_RE2.test(issueId))
|
|
92380
|
-
return usage3(io, "an issue id like ABC-123 is required",
|
|
93362
|
+
return usage3(io, "an issue id like ABC-123 is required", USAGE11);
|
|
92381
93363
|
if (extra !== undefined)
|
|
92382
|
-
return usage3(io, `unexpected argument ${extra}`,
|
|
93364
|
+
return usage3(io, `unexpected argument ${extra}`, USAGE11);
|
|
92383
93365
|
const minutes = positiveInt(flags.values.minutes, "--minutes");
|
|
92384
93366
|
if (typeof minutes === "string")
|
|
92385
|
-
return usage3(io, minutes,
|
|
93367
|
+
return usage3(io, minutes, USAGE11);
|
|
92386
93368
|
const date = youTrackWorkDateMs(flags.values.date ?? "auto");
|
|
92387
93369
|
if ("error" in date)
|
|
92388
|
-
return usage3(io, date.error,
|
|
93370
|
+
return usage3(io, date.error, USAGE11);
|
|
92389
93371
|
const dateMs = date.data.dateMs;
|
|
92390
93372
|
const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
|
|
92391
93373
|
const root = io.cwd;
|
|
@@ -92403,24 +93385,24 @@ async function run17(argv, io) {
|
|
|
92403
93385
|
};
|
|
92404
93386
|
if (sub === "note") {
|
|
92405
93387
|
if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
|
|
92406
|
-
return usage3(io, "pass exactly one of --markdown or --file",
|
|
93388
|
+
return usage3(io, "pass exactly one of --markdown or --file", USAGE11);
|
|
92407
93389
|
let markdown = flags.values.markdown ?? "";
|
|
92408
93390
|
if (flags.values.file)
|
|
92409
93391
|
try {
|
|
92410
|
-
markdown = readFileSync21(
|
|
93392
|
+
markdown = readFileSync21(path48.resolve(io.cwd, flags.values.file), "utf8");
|
|
92411
93393
|
} catch (error) {
|
|
92412
|
-
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`,
|
|
93394
|
+
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE11);
|
|
92413
93395
|
}
|
|
92414
93396
|
if (!markdown.trim())
|
|
92415
|
-
return usage3(io, "the note is empty",
|
|
93397
|
+
return usage3(io, "the note is empty", USAGE11);
|
|
92416
93398
|
const missing = step("comment", "comments", { operation: "note", issueId, markdown }, markdown, async (text) => commentResult(await postComment(issueId, text, root))) ?? (minutes ? step("time", "workItems", { operation: "note.time", issueId, markdown, minutes, ...markedDay }, "workit update", (text) => logTimeUpdate({ issueId, minutes, text, dateMs, workspace_root: root })) : null);
|
|
92417
93399
|
if (missing)
|
|
92418
93400
|
return emit(io, fail("unavailable", missing));
|
|
92419
93401
|
} else {
|
|
92420
93402
|
if (!minutes)
|
|
92421
|
-
return usage3(io, `${sub} needs --minutes <n>`,
|
|
93403
|
+
return usage3(io, `${sub} needs --minutes <n>`, USAGE11);
|
|
92422
93404
|
if (sub === "meeting" && !flags.values.text)
|
|
92423
|
-
return usage3(io, "meeting needs --text <what the meeting was>",
|
|
93405
|
+
return usage3(io, "meeting needs --text <what the meeting was>", USAGE11);
|
|
92424
93406
|
const text = flags.values.text ?? "";
|
|
92425
93407
|
const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
|
|
92426
93408
|
if (missing)
|
|
@@ -92449,7 +93431,7 @@ async function run17(argv, io) {
|
|
|
92449
93431
|
...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
|
|
92450
93432
|
]);
|
|
92451
93433
|
}
|
|
92452
|
-
var
|
|
93434
|
+
var USAGE11 = "workit youtrack note <ISSUE> (--markdown <t> | --file <p>) [--minutes <n>] [--date auto|YYYY-MM-DD] | youtrack time <ISSUE> --minutes <n> [--text <t>] [--date …] | youtrack meeting <ISSUE> --minutes <n> --text <t> [--date …]", commentResult = (value) => {
|
|
92453
93435
|
try {
|
|
92454
93436
|
return { ok: true, data: unwrap2(value) };
|
|
92455
93437
|
} catch (error) {
|
|
@@ -92473,14 +93455,14 @@ var init_youtrack2 = __esm(() => {
|
|
|
92473
93455
|
// packages/workit-cli/src/verbs/changelog.ts
|
|
92474
93456
|
var exports_changelog = {};
|
|
92475
93457
|
__export(exports_changelog, {
|
|
92476
|
-
run: () =>
|
|
93458
|
+
run: () => run19
|
|
92477
93459
|
});
|
|
92478
93460
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
92479
|
-
import
|
|
92480
|
-
async function
|
|
93461
|
+
import path49 from "node:path";
|
|
93462
|
+
async function run19(argv, io) {
|
|
92481
93463
|
const [sub, ...rest] = argv;
|
|
92482
93464
|
if (sub !== "apply")
|
|
92483
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93465
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
|
|
92484
93466
|
const flags = parseFlags(rest, {
|
|
92485
93467
|
entries: "value",
|
|
92486
93468
|
path: "value",
|
|
@@ -92488,18 +93470,18 @@ async function run18(argv, io) {
|
|
|
92488
93470
|
preview: "boolean"
|
|
92489
93471
|
});
|
|
92490
93472
|
if (typeof flags === "string")
|
|
92491
|
-
return usage3(io, flags,
|
|
93473
|
+
return usage3(io, flags, USAGE12);
|
|
92492
93474
|
if (flags.positionals.length)
|
|
92493
|
-
return usage3(io, `unexpected argument ${flags.positionals[0]}`,
|
|
93475
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE12);
|
|
92494
93476
|
const normalizeOnly = flags.booleans.has("normalize-only");
|
|
92495
93477
|
if (!normalizeOnly && !flags.values.entries)
|
|
92496
|
-
return usage3(io, "pass --entries <JSON> or --normalize-only",
|
|
93478
|
+
return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE12);
|
|
92497
93479
|
let entries;
|
|
92498
93480
|
if (flags.values.entries)
|
|
92499
93481
|
try {
|
|
92500
93482
|
entries = readEntries(flags.values.entries, io.cwd);
|
|
92501
93483
|
} catch (error) {
|
|
92502
|
-
return usage3(io, `--entries is not valid JSON: ${String(error)}`,
|
|
93484
|
+
return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE12);
|
|
92503
93485
|
}
|
|
92504
93486
|
const input = {
|
|
92505
93487
|
entries,
|
|
@@ -92521,8 +93503,8 @@ async function run18(argv, io) {
|
|
|
92521
93503
|
`changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
|
|
92522
93504
|
]);
|
|
92523
93505
|
}
|
|
92524
|
-
var
|
|
92525
|
-
const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(
|
|
93506
|
+
var USAGE12 = "workit changelog apply (--entries <JSON|@file|-> | --normalize-only) [--path CHANGELOG.md] [--preview]", readEntries = (raw, cwd) => {
|
|
93507
|
+
const text = raw === "-" ? readFileSync22(0, "utf8") : raw.startsWith("@") ? readFileSync22(path49.resolve(cwd, raw.slice(1)), "utf8") : raw;
|
|
92526
93508
|
return JSON.parse(text);
|
|
92527
93509
|
};
|
|
92528
93510
|
var init_changelog2 = __esm(() => {
|
|
@@ -92534,7 +93516,7 @@ var init_changelog2 = __esm(() => {
|
|
|
92534
93516
|
// packages/workit-cli/src/verbs/handoff.ts
|
|
92535
93517
|
var exports_handoff = {};
|
|
92536
93518
|
__export(exports_handoff, {
|
|
92537
|
-
run: () =>
|
|
93519
|
+
run: () => run20
|
|
92538
93520
|
});
|
|
92539
93521
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
92540
93522
|
function human2(brief) {
|
|
@@ -92582,11 +93564,11 @@ async function resumeBrief(argv, io) {
|
|
|
92582
93564
|
strict: true
|
|
92583
93565
|
}));
|
|
92584
93566
|
} catch (error) {
|
|
92585
|
-
return emit(io, fail("invalid_input", error.message, { unblock:
|
|
93567
|
+
return emit(io, fail("invalid_input", error.message, { unblock: USAGE13 }));
|
|
92586
93568
|
}
|
|
92587
93569
|
const last = values.last === undefined ? 10 : Number(values.last);
|
|
92588
93570
|
if (!Number.isInteger(last) || last < 1)
|
|
92589
|
-
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock:
|
|
93571
|
+
return emit(io, fail("invalid_input", "--last must be a positive integer", { unblock: USAGE13 }));
|
|
92590
93572
|
const built = buildHandoff(io.cwd, {
|
|
92591
93573
|
last,
|
|
92592
93574
|
note: values.note ?? null,
|
|
@@ -92604,13 +93586,13 @@ async function resumeBrief(argv, io) {
|
|
|
92604
93586
|
}
|
|
92605
93587
|
return emit(io, ok({ ...data, recorded }), human2);
|
|
92606
93588
|
}
|
|
92607
|
-
async function
|
|
93589
|
+
async function run20(argv, io) {
|
|
92608
93590
|
if (!argv.includes("--task"))
|
|
92609
93591
|
return resumeBrief(argv, io);
|
|
92610
93592
|
await Promise.resolve().then(() => init_task());
|
|
92611
93593
|
return runTaskCommand(["handoff", ...argv]);
|
|
92612
93594
|
}
|
|
92613
|
-
var
|
|
93595
|
+
var USAGE13 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
92614
93596
|
var init_handoff = __esm(() => {
|
|
92615
93597
|
init_ledger();
|
|
92616
93598
|
init_output();
|
|
@@ -92619,7 +93601,7 @@ var init_handoff = __esm(() => {
|
|
|
92619
93601
|
// packages/workit-cli/src/main.ts
|
|
92620
93602
|
init_package();
|
|
92621
93603
|
init_output();
|
|
92622
|
-
import
|
|
93604
|
+
import path50 from "node:path";
|
|
92623
93605
|
|
|
92624
93606
|
// packages/workit-cli/src/verbs/registry.ts
|
|
92625
93607
|
var TASK_FAMILY_NAMES = [
|
|
@@ -92731,6 +93713,13 @@ var VERBS = [
|
|
|
92731
93713
|
summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
|
|
92732
93714
|
load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
|
|
92733
93715
|
},
|
|
93716
|
+
{
|
|
93717
|
+
name: "fanout",
|
|
93718
|
+
group: "delivery",
|
|
93719
|
+
usage: "workit fanout plan <plan.json> [--name <n>] [--trunk <b> | --track <t>] | fanout check [<slice>…] [--name <n>] [--base <ref>] [--json]",
|
|
93720
|
+
summary: "Register parallel slices (complete briefs, disjoint scopes, shared-file owners), then gate fan-in: out-of-scope edits, trunk and sibling merge conflicts, landing order",
|
|
93721
|
+
load: () => Promise.resolve().then(() => (init_fanout2(), exports_fanout))
|
|
93722
|
+
},
|
|
92734
93723
|
{
|
|
92735
93724
|
name: "ledger",
|
|
92736
93725
|
group: "delivery",
|
|
@@ -92849,7 +93838,7 @@ var defaultIo = (json) => ({
|
|
|
92849
93838
|
});
|
|
92850
93839
|
async function main(argv, overrides = {}, options = {}) {
|
|
92851
93840
|
const parsed = parseGlobals(argv);
|
|
92852
|
-
const cwd =
|
|
93841
|
+
const cwd = path50.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
92853
93842
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
92854
93843
|
if (parsed.error)
|
|
92855
93844
|
return emit(io, fail("invalid_input", parsed.error));
|