@brainervirus/workit-claude-code 7.1.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-hook.js +38 -38
- package/dist/workit.js +1219 -129
- package/package.json +3 -3
- package/skills/fanout/SKILL.md +29 -20
- package/skills/fanout/references/brief.md +45 -0
- package/skills/implement/SKILL.md +3 -2
- package/skills/ship/SKILL.md +31 -13
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"
|
|
@@ -15473,7 +15473,7 @@ var init_workspaces = __esm(() => {
|
|
|
15473
15473
|
account: nonBlank.optional()
|
|
15474
15474
|
}).passthrough().optional(),
|
|
15475
15475
|
autoApprove: union([boolean2(), array(nonBlank)]).optional(),
|
|
15476
|
-
defaultEndpoint:
|
|
15476
|
+
defaultEndpoint: unknown().optional(),
|
|
15477
15477
|
verification: _enum(["self", "independent"]).optional(),
|
|
15478
15478
|
autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
|
|
15479
15479
|
kind,
|
|
@@ -16457,6 +16457,46 @@ var init_safe_write = () => {};
|
|
|
16457
16457
|
import { copyFileSync as copyFileSync3, existsSync as existsSync6 } from "node:fs";
|
|
16458
16458
|
import os3 from "node:os";
|
|
16459
16459
|
import path18 from "node:path";
|
|
16460
|
+
function endpointOf(entry) {
|
|
16461
|
+
const raw = entry?.defaultEndpoint;
|
|
16462
|
+
if (raw === undefined)
|
|
16463
|
+
return { raw, endpoint: DEFAULT_ENDPOINT };
|
|
16464
|
+
if (isDefaultEndpoint(raw))
|
|
16465
|
+
return { raw, endpoint: raw };
|
|
16466
|
+
return {
|
|
16467
|
+
raw,
|
|
16468
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
16469
|
+
issue: `defaultEndpoint ${JSON.stringify(raw)} is not one of ${DEFAULT_ENDPOINTS.join(", ")}; using ${DEFAULT_ENDPOINT}`
|
|
16470
|
+
};
|
|
16471
|
+
}
|
|
16472
|
+
function effectiveEndpoint(endpoint, context) {
|
|
16473
|
+
if (endpoint === "commit")
|
|
16474
|
+
return { endpoint };
|
|
16475
|
+
const missing = (kind) => `${kind} grant missing; ${grantHint(context.workspace, kind)}`;
|
|
16476
|
+
if (context.grants.push === false)
|
|
16477
|
+
return { endpoint: "commit", reason: missing("push") };
|
|
16478
|
+
if (context.grants.pr === false)
|
|
16479
|
+
return { endpoint: "commit", reason: missing("pr") };
|
|
16480
|
+
if (context.configured.length > 0 && !context.accountConfigured)
|
|
16481
|
+
return {
|
|
16482
|
+
endpoint: "commit",
|
|
16483
|
+
reason: `vcs.account missing; set vcs.account for workspace "${context.workspace ?? "?"}" in ~/.config/workit/workspaces.json`
|
|
16484
|
+
};
|
|
16485
|
+
if (endpoint === "merged" && context.grants.merge === false)
|
|
16486
|
+
return { endpoint: "green", reason: missing("merge") };
|
|
16487
|
+
return { endpoint };
|
|
16488
|
+
}
|
|
16489
|
+
function endpointView(entry, context) {
|
|
16490
|
+
const { raw, endpoint, issue } = endpointOf(entry);
|
|
16491
|
+
const effective = effectiveEndpoint(endpoint, context);
|
|
16492
|
+
return {
|
|
16493
|
+
defaultEndpoint: endpoint,
|
|
16494
|
+
configuredEndpoint: raw ?? null,
|
|
16495
|
+
effectiveEndpoint: effective.endpoint,
|
|
16496
|
+
...effective.reason ? { endpointReason: effective.reason } : {},
|
|
16497
|
+
...issue ? { endpointIssue: issue } : {}
|
|
16498
|
+
};
|
|
16499
|
+
}
|
|
16460
16500
|
function configuredGrants(entry) {
|
|
16461
16501
|
const record = entry ?? {};
|
|
16462
16502
|
const raw = record.autonomy;
|
|
@@ -16482,18 +16522,28 @@ function resolveAutonomy(cwd) {
|
|
|
16482
16522
|
source: "default",
|
|
16483
16523
|
accountConfigured: false,
|
|
16484
16524
|
defaultEndpoint: DEFAULT_ENDPOINT,
|
|
16525
|
+
configuredEndpoint: null,
|
|
16526
|
+
effectiveEndpoint: DEFAULT_ENDPOINT,
|
|
16485
16527
|
verification: DEFAULT_VERIFICATION,
|
|
16486
16528
|
note: override
|
|
16487
16529
|
};
|
|
16488
16530
|
const workspace = resolveWorkspaceFrom(cwd, grantsDir(), process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined);
|
|
16489
16531
|
const { grants, source } = configuredGrants(workspace);
|
|
16532
|
+
const full = { ...DEFAULT_GRANTS, ...grants };
|
|
16533
|
+
const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
|
|
16534
|
+
const accountConfigured = Boolean(workspace?.vcs?.account);
|
|
16490
16535
|
return {
|
|
16491
16536
|
workspace: workspace?.name ?? null,
|
|
16492
|
-
grants:
|
|
16493
|
-
configured
|
|
16537
|
+
grants: full,
|
|
16538
|
+
configured,
|
|
16494
16539
|
source,
|
|
16495
|
-
accountConfigured
|
|
16496
|
-
|
|
16540
|
+
accountConfigured,
|
|
16541
|
+
...endpointView(workspace, {
|
|
16542
|
+
workspace: workspace?.name ?? null,
|
|
16543
|
+
grants: full,
|
|
16544
|
+
configured,
|
|
16545
|
+
accountConfigured
|
|
16546
|
+
}),
|
|
16497
16547
|
verification: verificationOf(workspace)
|
|
16498
16548
|
};
|
|
16499
16549
|
}
|
|
@@ -16599,14 +16649,21 @@ function writeGrants(workspaceName, changes, dir = grantsDir(), endpoint, verifi
|
|
|
16599
16649
|
error: `failed to write ${current.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
16600
16650
|
};
|
|
16601
16651
|
}
|
|
16652
|
+
const full = { ...DEFAULT_GRANTS, ...grants };
|
|
16653
|
+
const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
|
|
16602
16654
|
return {
|
|
16603
16655
|
ok: true,
|
|
16604
16656
|
path: current.path,
|
|
16605
16657
|
backup,
|
|
16606
16658
|
workspace: workspaceName,
|
|
16607
|
-
grants:
|
|
16608
|
-
configured
|
|
16609
|
-
|
|
16659
|
+
grants: full,
|
|
16660
|
+
configured,
|
|
16661
|
+
...endpointView(entry, {
|
|
16662
|
+
workspace: workspaceName,
|
|
16663
|
+
grants: full,
|
|
16664
|
+
configured,
|
|
16665
|
+
accountConfigured: Boolean(accountOf(entry))
|
|
16666
|
+
}),
|
|
16610
16667
|
verification: verificationOf(entry)
|
|
16611
16668
|
};
|
|
16612
16669
|
}
|
|
@@ -16618,12 +16675,19 @@ function listGrants(dir = grantsDir()) {
|
|
|
16618
16675
|
path: current.path,
|
|
16619
16676
|
workspaces: current.entries.map((entry) => {
|
|
16620
16677
|
const { grants } = configuredGrants(entry);
|
|
16678
|
+
const full = { ...DEFAULT_GRANTS, ...grants };
|
|
16679
|
+
const configured = GRANT_KINDS.filter((kind) => grants[kind] !== undefined);
|
|
16621
16680
|
return {
|
|
16622
16681
|
name: entry.name,
|
|
16623
16682
|
glob: entry.glob,
|
|
16624
|
-
grants:
|
|
16625
|
-
configured
|
|
16626
|
-
|
|
16683
|
+
grants: full,
|
|
16684
|
+
configured,
|
|
16685
|
+
...endpointView(entry, {
|
|
16686
|
+
workspace: entry.name,
|
|
16687
|
+
grants: full,
|
|
16688
|
+
configured,
|
|
16689
|
+
accountConfigured: Boolean(entry.vcs?.account)
|
|
16690
|
+
}),
|
|
16627
16691
|
verification: verificationOf(entry)
|
|
16628
16692
|
};
|
|
16629
16693
|
})
|
|
@@ -16638,7 +16702,7 @@ var accountHome = () => {
|
|
|
16638
16702
|
}, grantsHome, grantsDir = () => path18.join(grantsHome.resolve(), ".config", "workit"), grantsOverride = () => {
|
|
16639
16703
|
const configured = path18.resolve(resolveConfigDir());
|
|
16640
16704
|
return configured === path18.resolve(grantsDir()) ? null : `the config directory is redirected to ${configured} (WORKFLOW_TOOLKIT_CONFIG, WORKFLOW_TOOLKIT_CONFIG_DIR or XDG_CONFIG_HOME); grants are read only from ${path18.join(grantsDir(), "workspaces.json")}, so the D4 defaults apply`;
|
|
16641
|
-
}, GRANT_KINDS, DEFAULT_GRANTS, DEFAULT_ENDPOINT = "commit",
|
|
16705
|
+
}, GRANT_KINDS, DEFAULT_GRANTS, DEFAULT_ENDPOINTS, DEFAULT_ENDPOINT = "commit", isDefaultEndpoint = (value) => typeof value === "string" && DEFAULT_ENDPOINTS.includes(value), raisesEndpoint = (current, next) => DEFAULT_ENDPOINTS.indexOf(next) > DEFAULT_ENDPOINTS.indexOf(current), DEFAULT_VERIFICATION = "self", verificationOf = (entry) => entry?.verification === "independent" ? "independent" : "self", isGrantKind = (value) => typeof value === "string" && GRANT_KINDS.includes(value), isGrantValue = (value) => value === true || value === false || value === "verified", normalized = (kind, value) => value === "verified" && kind !== "merge" ? true : value, fromAutoApprove = (value) => {
|
|
16642
16706
|
if (value === true)
|
|
16643
16707
|
return { push: true, pr: true, merge: "verified" };
|
|
16644
16708
|
if (!Array.isArray(value))
|
|
@@ -16650,7 +16714,7 @@ var accountHome = () => {
|
|
|
16650
16714
|
else if (item === "merge")
|
|
16651
16715
|
grants.merge = "verified";
|
|
16652
16716
|
return Object.keys(grants).length > 0 ? grants : null;
|
|
16653
|
-
}, suggested = (kind) => kind === "merge" ? "verified" : "true", grantHint = (workspace, kind) => `ask the user to run, in their own terminal: workit grant set ${workspace ?? "<workspace>"} ${kind}=${suggested(kind)}`, raises = (kind, current, next) => {
|
|
16717
|
+
}, accountOf = (entry) => entry?.vcs?.account, suggested = (kind) => kind === "merge" ? "verified" : "true", grantHint = (workspace, kind) => `ask the user to run, in their own terminal: workit grant set ${workspace ?? "<workspace>"} ${kind}=${suggested(kind)}`, raises = (kind, current, next) => {
|
|
16654
16718
|
const rank = (value) => value === true ? 2 : value === "verified" ? 1 : 0;
|
|
16655
16719
|
return rank(normalized(kind, next)) > rank(normalized(kind, current));
|
|
16656
16720
|
};
|
|
@@ -16667,6 +16731,7 @@ var init_autonomy = __esm(() => {
|
|
|
16667
16731
|
release: false,
|
|
16668
16732
|
rerun: true
|
|
16669
16733
|
});
|
|
16734
|
+
DEFAULT_ENDPOINTS = ["commit", "pr", "green", "merged"];
|
|
16670
16735
|
});
|
|
16671
16736
|
|
|
16672
16737
|
// packages/workit-core/src/core/task-engine.ts
|
|
@@ -70508,7 +70573,7 @@ function show(argv, io) {
|
|
|
70508
70573
|
return emit(io, fail("not_found", `no workspace named "${named[0]}" in ${listed.path}`));
|
|
70509
70574
|
return emit(io, ok({ path: listed.path, defaults: DEFAULT_GRANTS, workspaces: rows }), () => rows.flatMap((entry) => [
|
|
70510
70575
|
`${entry.name} (${entry.glob})`,
|
|
70511
|
-
...describe2(entry.grants, entry.configured, entry
|
|
70576
|
+
...describe2(entry.grants, entry.configured, entry, entry.verification)
|
|
70512
70577
|
]));
|
|
70513
70578
|
}
|
|
70514
70579
|
let autonomy;
|
|
@@ -70522,7 +70587,7 @@ function show(argv, io) {
|
|
|
70522
70587
|
return emit(io, ok(shown), (data) => [
|
|
70523
70588
|
...data.note ? [`note: ${data.note}`] : [],
|
|
70524
70589
|
data.workspace ? `workspace ${data.workspace} (${data.source === "default" ? "D4 defaults" : `from ${data.source}`})` : "no workspace matches this checkout: D4 defaults apply",
|
|
70525
|
-
...describe2(data.grants, data.configured, data
|
|
70590
|
+
...describe2(data.grants, data.configured, data, data.verification),
|
|
70526
70591
|
...describeReleaseTracks(tracks).map((line) => ` ${line}`),
|
|
70527
70592
|
"raising a grant needs the user: workit grant set <workspace> <kind>=<value>"
|
|
70528
70593
|
]);
|
|
@@ -70545,10 +70610,10 @@ async function change(verb, argv, io, deps) {
|
|
|
70545
70610
|
if (kind === "defaultEndpoint") {
|
|
70546
70611
|
if (verb === "unset")
|
|
70547
70612
|
endpoint = null;
|
|
70548
|
-
else if (raw
|
|
70613
|
+
else if (isDefaultEndpoint(raw))
|
|
70549
70614
|
endpoint = raw;
|
|
70550
70615
|
else
|
|
70551
|
-
return usage2(io, `${spec}: defaultEndpoint must be
|
|
70616
|
+
return usage2(io, `${spec}: defaultEndpoint must be ${DEFAULT_ENDPOINTS.join(", ")}`);
|
|
70552
70617
|
continue;
|
|
70553
70618
|
}
|
|
70554
70619
|
if (kind === "verification") {
|
|
@@ -70572,8 +70637,8 @@ async function change(verb, argv, io, deps) {
|
|
|
70572
70637
|
changes.push({ kind, value });
|
|
70573
70638
|
}
|
|
70574
70639
|
const raised = changes.filter(({ kind, value }) => raises(kind, entry.grants[kind], value ?? DEFAULT_GRANTS[kind])).map(({ kind, value }) => ({ kind, value: String(value ?? DEFAULT_GRANTS[kind]) }));
|
|
70575
|
-
if (endpoint
|
|
70576
|
-
raised.push({ kind: "defaultEndpoint", value:
|
|
70640
|
+
if (endpoint && raisesEndpoint(entry.defaultEndpoint, endpoint))
|
|
70641
|
+
raised.push({ kind: "defaultEndpoint", value: endpoint });
|
|
70577
70642
|
if (verification !== undefined && verification !== "independent" && entry.verification === "independent")
|
|
70578
70643
|
raised.push({ kind: "verification", value: "self" });
|
|
70579
70644
|
if (raised.length > 0) {
|
|
@@ -70592,7 +70657,7 @@ async function change(verb, argv, io, deps) {
|
|
|
70592
70657
|
return emit(io, fail(written.code, written.error));
|
|
70593
70658
|
return emit(io, ok(written), (data) => [
|
|
70594
70659
|
`${data.workspace}: grants updated in ${data.path}${data.backup ? ` (previous copy: ${data.backup})` : ""}`,
|
|
70595
|
-
...describe2(data.grants, data.configured, data
|
|
70660
|
+
...describe2(data.grants, data.configured, data, data.verification)
|
|
70596
70661
|
]);
|
|
70597
70662
|
}
|
|
70598
70663
|
async function runGrant(argv, io, deps = defaultDeps) {
|
|
@@ -70610,9 +70675,10 @@ async function runGrant(argv, io, deps = defaultDeps) {
|
|
|
70610
70675
|
}
|
|
70611
70676
|
return usage2(io, `unknown subcommand "${sub}"`);
|
|
70612
70677
|
}
|
|
70613
|
-
var USAGE2 = "workit grant show [<workspace>] [--all] | grant set <workspace> <kind>=<true|false|verified>… [defaultEndpoint=commit|pr] [verification=self|independent] | grant unset <workspace> <kind>… (kinds: push, pr, merge, release, rerun, defaultEndpoint, verification)", AGENT_ENV, agentMarker = (env) => AGENT_ENV.find((name) => env[name]) ?? Object.keys(env).find((name) => name.startsWith("CODEX_") && env[name]), defaultDeps, parseValue = (raw) => raw === "true" || raw === "yes" || raw === "on" ? true : raw === "false" || raw === "no" || raw === "off" ? false : raw === "verified" ? "verified" : null, describe2 = (grants, configured, endpoint, verification) => [
|
|
70678
|
+
var USAGE2 = "workit grant show [<workspace>] [--all] | grant set <workspace> <kind>=<true|false|verified>… [defaultEndpoint=commit|pr|green|merged] [verification=self|independent] | grant unset <workspace> <kind>… (kinds: push, pr, merge, release, rerun, defaultEndpoint, verification)", AGENT_ENV, agentMarker = (env) => AGENT_ENV.find((name) => env[name]) ?? Object.keys(env).find((name) => name.startsWith("CODEX_") && env[name]), defaultDeps, parseValue = (raw) => raw === "true" || raw === "yes" || raw === "on" ? true : raw === "false" || raw === "no" || raw === "off" ? false : raw === "verified" ? "verified" : null, STOPS_AT, endpointLabel = (view) => view.effectiveEndpoint === view.defaultEndpoint ? view.defaultEndpoint : `${view.defaultEndpoint} (effective: ${view.effectiveEndpoint}${view.endpointReason ? `, ${view.endpointReason}` : ""})`, describe2 = (grants, configured, endpoint, verification) => [
|
|
70614
70679
|
...GRANT_KINDS.map((kind) => ` ${kind.padEnd(8)}${String(grants[kind]).padEnd(10)}${configured.includes(kind) ? "configured" : "default"}${kind === "release" ? " (no consumer yet: reserved, not enforced)" : ""}`),
|
|
70615
|
-
` default endpoint: ${endpoint} (an unnamed request stops at ${endpoint
|
|
70680
|
+
` default endpoint: ${endpointLabel(endpoint)} (an unnamed request stops at ${STOPS_AT[endpoint.effectiveEndpoint]})`,
|
|
70681
|
+
...endpoint.endpointIssue ? [` note: ${endpoint.endpointIssue}`] : [],
|
|
70616
70682
|
` verification: ${verification} (${verification === "independent" ? "a normal-risk behavior change needs a non-author verdict" : "a normal-risk behavior change needs a passing check and the author's --self verdict, shown as self-reviewed"})`
|
|
70617
70683
|
], usage2 = (io, message) => emit(io, fail("invalid_input", `${message}
|
|
70618
70684
|
usage: ${USAGE2}`)), run8 = (argv, io) => runGrant(argv, io);
|
|
@@ -70633,6 +70699,12 @@ var init_grant = __esm(() => {
|
|
|
70633
70699
|
interactive: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
|
|
70634
70700
|
ask: (question) => askLine(question)
|
|
70635
70701
|
};
|
|
70702
|
+
STOPS_AT = {
|
|
70703
|
+
commit: "a local commit",
|
|
70704
|
+
pr: "an opened PR",
|
|
70705
|
+
green: "a merge-ready PR: CI green, threads resolved, verified; never merged",
|
|
70706
|
+
merged: "a merged PR (workit pr merge)"
|
|
70707
|
+
};
|
|
70636
70708
|
});
|
|
70637
70709
|
|
|
70638
70710
|
// packages/workit-cli/src/verbs/check.ts
|
|
@@ -71494,6 +71566,32 @@ function blockersOf(input) {
|
|
|
71494
71566
|
}
|
|
71495
71567
|
return out;
|
|
71496
71568
|
}
|
|
71569
|
+
function babysitAction(next, reason, draft) {
|
|
71570
|
+
switch (next) {
|
|
71571
|
+
case "MERGED":
|
|
71572
|
+
return "merged";
|
|
71573
|
+
case "CLOSED":
|
|
71574
|
+
return null;
|
|
71575
|
+
case "RESOLVE_CONFLICTS":
|
|
71576
|
+
case "REBASE":
|
|
71577
|
+
return "update-branch";
|
|
71578
|
+
case "RESOLVE_THREADS":
|
|
71579
|
+
case "ADDRESS_REVIEW":
|
|
71580
|
+
return "address-threads";
|
|
71581
|
+
case "FIX_CI":
|
|
71582
|
+
return "fix-ci";
|
|
71583
|
+
case "WAITING_CI":
|
|
71584
|
+
return "wait";
|
|
71585
|
+
case "IN_MERGE_QUEUE":
|
|
71586
|
+
return "wait-forge";
|
|
71587
|
+
default:
|
|
71588
|
+
if (draft)
|
|
71589
|
+
return "mark-ready";
|
|
71590
|
+
if (next === "NOT_MERGEABLE" && reason === "mergeability_unknown")
|
|
71591
|
+
return "wait-forge";
|
|
71592
|
+
return "ready";
|
|
71593
|
+
}
|
|
71594
|
+
}
|
|
71497
71595
|
function computeBehindBase(cwd, remotes, baseSha, headSha_, options = {}) {
|
|
71498
71596
|
const empty = { behind: null, ahead: null, upToDate: null };
|
|
71499
71597
|
if (!baseSha)
|
|
@@ -71618,7 +71716,8 @@ function buildStatusDoc(cwd, resolved, status, options) {
|
|
|
71618
71716
|
blockers: status.state === "open" ? blockers.map((blocker) => blocker.reason) : [],
|
|
71619
71717
|
identity: options.identity ?? null,
|
|
71620
71718
|
truncated: status.truncated,
|
|
71621
|
-
next: blockers[0]?.next ?? "READY"
|
|
71719
|
+
next: blockers[0]?.next ?? "READY",
|
|
71720
|
+
babysit: babysitAction(blockers[0]?.next ?? "READY", blockers[0]?.reason ?? null, status.draft || blockers.some((blocker) => blocker.next === "MARK_READY"))
|
|
71622
71721
|
});
|
|
71623
71722
|
}
|
|
71624
71723
|
function prStatusReport(cwd, resolved, options = {}) {
|
|
@@ -72918,6 +73017,20 @@ function parseDuration2(value) {
|
|
|
72918
73017
|
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000;
|
|
72919
73018
|
return Math.round(Number(match[1]) * factor);
|
|
72920
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
|
+
}
|
|
72921
73034
|
function connect(io, branch) {
|
|
72922
73035
|
const resolved = resolveForge(io.cwd, {
|
|
72923
73036
|
branch,
|
|
@@ -72977,12 +73090,15 @@ function renderStatus(doc) {
|
|
|
72977
73090
|
lines.push(`blockers: ${doc.blockers.join(", ")}`);
|
|
72978
73091
|
if (doc.identity?.note)
|
|
72979
73092
|
lines.push(`identity: ${doc.identity.note}`);
|
|
73093
|
+
if (doc.babysit)
|
|
73094
|
+
lines.push(`babysit: ${doc.babysit}`);
|
|
72980
73095
|
lines.push(`next: ${doc.next}`);
|
|
72981
73096
|
return lines;
|
|
72982
73097
|
}
|
|
72983
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) : "?";
|
|
72984
73099
|
var init_forge_common = __esm(() => {
|
|
72985
73100
|
init_resolve();
|
|
73101
|
+
init_vcs_config();
|
|
72986
73102
|
init_output();
|
|
72987
73103
|
forgeDeps = {
|
|
72988
73104
|
runner: undefined,
|
|
@@ -75106,7 +75222,7 @@ async function plan(argv, io) {
|
|
|
75106
75222
|
return usage3(io, "pass --trunk or --track, not both (--track picks the trunk)", PLAN_USAGE);
|
|
75107
75223
|
let trunk = flags.values.trunk ?? existing?.trunk ?? null;
|
|
75108
75224
|
if (trunk === null || track !== null) {
|
|
75109
|
-
const derived =
|
|
75225
|
+
const derived = releaseTrunk(io, track, flags.positionals[0] ?? null);
|
|
75110
75226
|
if ("error" in derived)
|
|
75111
75227
|
return emit(io, fail("blocked", derived.error, {
|
|
75112
75228
|
unblock: "workit stack plan --track <name> … # or --trunk <branch>"
|
|
@@ -75274,17 +75390,7 @@ async function run14(argv, io) {
|
|
|
75274
75390
|
return land(rest, io);
|
|
75275
75391
|
return usage3(io, sub && !sub.startsWith("-") ? `unknown stack subcommand "${sub}"` : "missing stack subcommand", USAGE7);
|
|
75276
75392
|
}
|
|
75277
|
-
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 ?? {} })),
|
|
75278
|
-
const resolved = vcsConfig("resolve", io.cwd, {
|
|
75279
|
-
track,
|
|
75280
|
-
...branch ? { branch } : {}
|
|
75281
|
-
});
|
|
75282
|
-
if (resolved.ok === false)
|
|
75283
|
-
return track !== null || String(resolved.configPath ?? "").endsWith("workspaces.json") ? { error: String(resolved.error) } : { trunk: null };
|
|
75284
|
-
if (resolved.releaseTrack?.blocking)
|
|
75285
|
-
return { error: String(resolved.releaseTrack.blocking) };
|
|
75286
|
-
return { trunk: String(resolved.defaultTargetBranch ?? "") || null };
|
|
75287
|
-
}, 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) => [
|
|
75288
75394
|
`${data.dryRun ? "would sync" : "synced"} stack ${data.name}${data.merged.length ? `; merged: ${data.merged.map((item) => item.branch).join(", ")}` : ""}`,
|
|
75289
75395
|
...data.steps.map((step) => [
|
|
75290
75396
|
` ${step.branch} on ${step.parent}:`,
|
|
@@ -75296,16 +75402,993 @@ var PLAN_USAGE = "workit stack plan [--name <n>] [--trunk <b> | --track <t>] [<b
|
|
|
75296
75402
|
var init_stack2 = __esm(() => {
|
|
75297
75403
|
init_ledger();
|
|
75298
75404
|
init_stack();
|
|
75299
|
-
init_vcs_config();
|
|
75300
75405
|
init_output();
|
|
75301
75406
|
init_forge_common();
|
|
75302
75407
|
METHODS3 = new Set(["squash", "merge", "rebase"]);
|
|
75303
75408
|
});
|
|
75304
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
|
+
|
|
75305
76388
|
// packages/workit-cli/src/verbs/ledger.ts
|
|
75306
76389
|
var exports_ledger = {};
|
|
75307
76390
|
__export(exports_ledger, {
|
|
75308
|
-
run: () =>
|
|
76391
|
+
run: () => run16
|
|
75309
76392
|
});
|
|
75310
76393
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
75311
76394
|
import { parseArgs } from "node:util";
|
|
@@ -75329,7 +76412,7 @@ function targetBranch(io, rows, branch, pr) {
|
|
|
75329
76412
|
}
|
|
75330
76413
|
return { ok: true, value: branch ?? null };
|
|
75331
76414
|
}
|
|
75332
|
-
async function
|
|
76415
|
+
async function run16(argv, io) {
|
|
75333
76416
|
let parsed;
|
|
75334
76417
|
try {
|
|
75335
76418
|
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
@@ -75357,7 +76440,7 @@ async function run15(argv, io) {
|
|
|
75357
76440
|
return usage4(io, `unknown ledger subcommand "${sub}"`);
|
|
75358
76441
|
const ledger = readLedger(io.cwd);
|
|
75359
76442
|
if (!ledger.ok)
|
|
75360
|
-
return
|
|
76443
|
+
return failed2(io, ledger);
|
|
75361
76444
|
const { rows } = ledger.value;
|
|
75362
76445
|
if (sub === "list" || sub === "show") {
|
|
75363
76446
|
if (rest.length)
|
|
@@ -75379,7 +76462,7 @@ async function run15(argv, io) {
|
|
|
75379
76462
|
return usage4(io, `unexpected argument: ${rest.at(-1)}`);
|
|
75380
76463
|
const target = targetBranch(io, rows, rest[0] ?? values.branch, pr);
|
|
75381
76464
|
if (!target.ok)
|
|
75382
|
-
return
|
|
76465
|
+
return failed2(io, target);
|
|
75383
76466
|
const branch = target.value ?? currentBranch(io.cwd);
|
|
75384
76467
|
if (!branch)
|
|
75385
76468
|
return emit(io, fail("invalid_input", "HEAD is detached; name the branch", {
|
|
@@ -75389,7 +76472,7 @@ async function run15(argv, io) {
|
|
|
75389
76472
|
}
|
|
75390
76473
|
const target = targetBranch(io, rows, values.branch, pr);
|
|
75391
76474
|
if (!target.ok)
|
|
75392
|
-
return
|
|
76475
|
+
return failed2(io, target);
|
|
75393
76476
|
const acting = actorFor(io, values);
|
|
75394
76477
|
if (acting instanceof Error)
|
|
75395
76478
|
return usage4(io, acting.message);
|
|
@@ -75428,7 +76511,7 @@ async function run15(argv, io) {
|
|
|
75428
76511
|
..."derivedFrom" in acting ? { derivedFrom: acting.derivedFrom } : {}
|
|
75429
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)` : ""}`);
|
|
75430
76513
|
}
|
|
75431
|
-
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) => {
|
|
75432
76515
|
const actor = actorFromEnv(io.env);
|
|
75433
76516
|
if (values.session !== undefined && values.as !== undefined)
|
|
75434
76517
|
return new Error("pass --session or --as, not both");
|
|
@@ -75452,7 +76535,7 @@ var USAGE8 = "workit ledger decision|ruling|verdict|list|check ... (workit help
|
|
|
75452
76535
|
return;
|
|
75453
76536
|
const parsed = Number(value);
|
|
75454
76537
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : new Error(`${flag} must be a positive integer`);
|
|
75455
|
-
},
|
|
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) => {
|
|
75456
76539
|
const lines = [
|
|
75457
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}`
|
|
75458
76541
|
];
|
|
@@ -90449,7 +91532,7 @@ var init_ast = __esm(() => {
|
|
|
90449
91532
|
|
|
90450
91533
|
// packages/workit-cli/src/test-audit/rules.ts
|
|
90451
91534
|
import { builtinModules } from "node:module";
|
|
90452
|
-
import
|
|
91535
|
+
import path44 from "node:path";
|
|
90453
91536
|
function testCallee(call) {
|
|
90454
91537
|
let callee = call.callee;
|
|
90455
91538
|
if (callee.type === "CallExpression")
|
|
@@ -90942,12 +92025,12 @@ function checkConstantsOnly(ctx, emit) {
|
|
|
90942
92025
|
}
|
|
90943
92026
|
function checkOverMocking(ctx, emit) {
|
|
90944
92027
|
const { program } = ctx.parsed;
|
|
90945
|
-
const unit =
|
|
92028
|
+
const unit = path44.basename(ctx.file).replace(/\.(?:[cm]?[jt]sx?)$/, "").replace(/\.(?:test|spec)$/, "");
|
|
90946
92029
|
const mocked = find2(program, (node) => node.type === "CallExpression" && MODULE_MOCK.test(calleeName(node.callee) ?? "") && node.arguments[0]?.type === "StringLiteral");
|
|
90947
92030
|
const internal = mocked.filter((call) => /^(?:\.|@\/|~\/)/.test(call.arguments[0].value));
|
|
90948
92031
|
for (const call of mocked) {
|
|
90949
92032
|
const spec = call.arguments[0].value;
|
|
90950
|
-
const base =
|
|
92033
|
+
const base = path44.basename(spec.replace(/\/index(?:\.[cm]?[jt]sx?)?$/, "")).replace(/\.(?:[cm]?[jt]sx?)$/, "");
|
|
90951
92034
|
if (base === unit)
|
|
90952
92035
|
emit({
|
|
90953
92036
|
rule: "over-mocking",
|
|
@@ -91335,11 +92418,11 @@ var init_rules = __esm(() => {
|
|
|
91335
92418
|
});
|
|
91336
92419
|
|
|
91337
92420
|
// packages/workit-cli/src/test-audit/audit.ts
|
|
91338
|
-
import { spawnSync as
|
|
92421
|
+
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
91339
92422
|
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync19, statSync as statSync7 } from "node:fs";
|
|
91340
|
-
import
|
|
92423
|
+
import path45 from "node:path";
|
|
91341
92424
|
function git6(cwd, args) {
|
|
91342
|
-
const run =
|
|
92425
|
+
const run = spawnSync21("git", ["-c", "core.quotepath=off", ...args], {
|
|
91343
92426
|
cwd,
|
|
91344
92427
|
encoding: "utf8",
|
|
91345
92428
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -91350,7 +92433,7 @@ function walkTests(dir, out) {
|
|
|
91350
92433
|
for (const entry of readdirSync8(dir, { withFileTypes: true })) {
|
|
91351
92434
|
if (entry.name.startsWith(".") && entry.name !== "." && entry.isDirectory())
|
|
91352
92435
|
continue;
|
|
91353
|
-
const full =
|
|
92436
|
+
const full = path45.join(dir, entry.name);
|
|
91354
92437
|
if (entry.isDirectory()) {
|
|
91355
92438
|
if (!SKIP_DIRS.has(entry.name))
|
|
91356
92439
|
walkTests(full, out);
|
|
@@ -91362,7 +92445,7 @@ function expandPaths(cwd, inputs) {
|
|
|
91362
92445
|
const files = [];
|
|
91363
92446
|
const missing = [];
|
|
91364
92447
|
for (const input of inputs.length > 0 ? inputs : ["."]) {
|
|
91365
|
-
const full =
|
|
92448
|
+
const full = path45.resolve(cwd, input);
|
|
91366
92449
|
if (!existsSync18(full))
|
|
91367
92450
|
missing.push(input);
|
|
91368
92451
|
else if (statSync7(full).isDirectory())
|
|
@@ -91399,11 +92482,11 @@ function changedLines(cwd, base) {
|
|
|
91399
92482
|
for (let offset = 0;offset < count; offset += 1)
|
|
91400
92483
|
lines.add(start + offset);
|
|
91401
92484
|
}
|
|
91402
|
-
changed.set(
|
|
92485
|
+
changed.set(path45.join(root, file), lines);
|
|
91403
92486
|
}
|
|
91404
92487
|
const untracked = git6(root, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
91405
92488
|
for (const file of untracked.stdout.split("\x00").filter(Boolean))
|
|
91406
|
-
changed.set(
|
|
92489
|
+
changed.set(path45.join(root, file), "all");
|
|
91407
92490
|
return changed;
|
|
91408
92491
|
}
|
|
91409
92492
|
function auditFiles(files, display) {
|
|
@@ -91465,7 +92548,7 @@ function auditFiles(files, display) {
|
|
|
91465
92548
|
durationMs: Math.round(performance.now() - started)
|
|
91466
92549
|
};
|
|
91467
92550
|
}
|
|
91468
|
-
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;
|
|
91469
92552
|
var init_audit = __esm(() => {
|
|
91470
92553
|
init_ast();
|
|
91471
92554
|
init_rules();
|
|
@@ -91495,7 +92578,7 @@ import {
|
|
|
91495
92578
|
copyFileSync as copyFileSync6
|
|
91496
92579
|
} from "node:fs";
|
|
91497
92580
|
import os14 from "node:os";
|
|
91498
|
-
import
|
|
92581
|
+
import path46 from "node:path";
|
|
91499
92582
|
function mutantsFor(source, file, lines) {
|
|
91500
92583
|
const parsed = parseSource(source, file);
|
|
91501
92584
|
const out = [];
|
|
@@ -91568,7 +92651,7 @@ function relatedTests(source, testFiles) {
|
|
|
91568
92651
|
for (const match of text.matchAll(SPECIFIER)) {
|
|
91569
92652
|
const spec = match[1];
|
|
91570
92653
|
if (spec.startsWith(".")) {
|
|
91571
|
-
if (stripExt(
|
|
92654
|
+
if (stripExt(path46.posix.join(path46.posix.dirname(file), spec)) === target)
|
|
91572
92655
|
return true;
|
|
91573
92656
|
} else if (spec.includes("/") && stripExt(spec).split("/").slice(-2).join("/") === tail)
|
|
91574
92657
|
return true;
|
|
@@ -91588,12 +92671,12 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91588
92671
|
for (const name of names) {
|
|
91589
92672
|
if (!name.startsWith(TEMP_PREFIX))
|
|
91590
92673
|
continue;
|
|
91591
|
-
const dir =
|
|
92674
|
+
const dir = path46.join(tmp, name);
|
|
91592
92675
|
try {
|
|
91593
92676
|
const stat = lstatSync7(dir);
|
|
91594
92677
|
if (!stat.isDirectory() || uid !== undefined && stat.uid !== uid)
|
|
91595
92678
|
continue;
|
|
91596
|
-
const pid = Number(readFileSync20(
|
|
92679
|
+
const pid = Number(readFileSync20(path46.join(dir, PID_FILE), "utf8"));
|
|
91597
92680
|
if (Number.isInteger(pid) && pid > 0 && alive(pid))
|
|
91598
92681
|
continue;
|
|
91599
92682
|
} catch (error) {
|
|
@@ -91610,8 +92693,8 @@ function removeStaleCopies(tmp = os14.tmpdir()) {
|
|
|
91610
92693
|
function linkNodeModules(origin, target, repo, copy) {
|
|
91611
92694
|
mkdirSync9(target, { recursive: true });
|
|
91612
92695
|
for (const entry of readdirSync9(origin, { withFileTypes: true })) {
|
|
91613
|
-
const from =
|
|
91614
|
-
const to =
|
|
92696
|
+
const from = path46.join(origin, entry.name);
|
|
92697
|
+
const to = path46.join(target, entry.name);
|
|
91615
92698
|
if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
91616
92699
|
linkNodeModules(from, to, repo, copy);
|
|
91617
92700
|
continue;
|
|
@@ -91627,9 +92710,9 @@ function linkNodeModules(origin, target, repo, copy) {
|
|
|
91627
92710
|
} catch {
|
|
91628
92711
|
continue;
|
|
91629
92712
|
}
|
|
91630
|
-
const relative =
|
|
91631
|
-
if (!relative.startsWith("..") && !
|
|
91632
|
-
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");
|
|
91633
92716
|
continue;
|
|
91634
92717
|
}
|
|
91635
92718
|
}
|
|
@@ -91645,7 +92728,7 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91645
92728
|
}
|
|
91646
92729
|
const realRepo = realpathSync9(repo);
|
|
91647
92730
|
if (real && inside2(realRepo, real)) {
|
|
91648
|
-
symlinkSync(
|
|
92731
|
+
symlinkSync(path46.relative(path46.dirname(to), path46.join(tree, path46.relative(realRepo, real))), to);
|
|
91649
92732
|
return;
|
|
91650
92733
|
}
|
|
91651
92734
|
if (real && statSync8(real).isFile())
|
|
@@ -91654,10 +92737,10 @@ function copySymlink(repo, tree, from, to) {
|
|
|
91654
92737
|
symlinkSync(readlinkSync3(from), to);
|
|
91655
92738
|
}
|
|
91656
92739
|
function writeInside(copy, file, content) {
|
|
91657
|
-
const target =
|
|
92740
|
+
const target = path46.join(copy, file);
|
|
91658
92741
|
try {
|
|
91659
92742
|
const stat = lstatSync7(target);
|
|
91660
|
-
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(
|
|
92743
|
+
if (!stat.isFile() || !inside2(realpathSync9(copy), realpathSync9(path46.dirname(target))))
|
|
91661
92744
|
return false;
|
|
91662
92745
|
} catch {
|
|
91663
92746
|
return false;
|
|
@@ -91668,9 +92751,9 @@ function writeInside(copy, file, content) {
|
|
|
91668
92751
|
return true;
|
|
91669
92752
|
}
|
|
91670
92753
|
function copyWorkingTree(repo) {
|
|
91671
|
-
const dir = mkdtempSync4(
|
|
91672
|
-
writeFileSync9(
|
|
91673
|
-
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");
|
|
91674
92757
|
const head = git6(repo, ["rev-parse", "--verify", "--quiet", "HEAD"]).stdout.trim();
|
|
91675
92758
|
if (head && git6(dir, ["clone", "-q", "--shared", "--no-checkout", repo, tree]).ok) {
|
|
91676
92759
|
const branch = git6(repo, ["symbolic-ref", "--quiet", "--short", "HEAD"]).stdout.trim();
|
|
@@ -91687,29 +92770,29 @@ function copyWorkingTree(repo) {
|
|
|
91687
92770
|
for (const relative of listed) {
|
|
91688
92771
|
if (!relative)
|
|
91689
92772
|
continue;
|
|
91690
|
-
const from =
|
|
92773
|
+
const from = path46.join(repo, relative);
|
|
91691
92774
|
let stat;
|
|
91692
92775
|
try {
|
|
91693
92776
|
stat = lstatSync7(from);
|
|
91694
92777
|
} catch {
|
|
91695
92778
|
continue;
|
|
91696
92779
|
}
|
|
91697
|
-
const to =
|
|
91698
|
-
mkdirSync9(
|
|
92780
|
+
const to = path46.join(tree, relative);
|
|
92781
|
+
mkdirSync9(path46.dirname(to), { recursive: true });
|
|
91699
92782
|
if (stat.isSymbolicLink())
|
|
91700
92783
|
copySymlink(repo, tree, from, to);
|
|
91701
92784
|
else if (stat.isFile())
|
|
91702
92785
|
copyFileSync6(from, to);
|
|
91703
|
-
let parent =
|
|
92786
|
+
let parent = path46.dirname(relative);
|
|
91704
92787
|
while (parent !== "." && !dirs.has(parent)) {
|
|
91705
92788
|
dirs.add(parent);
|
|
91706
|
-
parent =
|
|
92789
|
+
parent = path46.dirname(parent);
|
|
91707
92790
|
}
|
|
91708
92791
|
}
|
|
91709
92792
|
for (const parent of dirs) {
|
|
91710
|
-
const modules =
|
|
91711
|
-
if (existsSync19(modules) && !existsSync19(
|
|
91712
|
-
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);
|
|
91713
92796
|
}
|
|
91714
92797
|
return { dir, tree };
|
|
91715
92798
|
}
|
|
@@ -91748,14 +92831,14 @@ async function runMutation(options) {
|
|
|
91748
92831
|
const tracked = git6(repo, ["ls-files", "-co", "--exclude-standard", "-z"]).stdout.split("\x00").filter(Boolean);
|
|
91749
92832
|
const testFiles = tracked.filter((file) => TEST_FILE.test(file)).map((file) => {
|
|
91750
92833
|
try {
|
|
91751
|
-
return { file, text: readFileSync20(
|
|
92834
|
+
return { file, text: readFileSync20(path46.join(repo, file), "utf8") };
|
|
91752
92835
|
} catch {
|
|
91753
92836
|
return { file, text: "" };
|
|
91754
92837
|
}
|
|
91755
92838
|
});
|
|
91756
92839
|
const candidates = [];
|
|
91757
92840
|
for (const [absolute, lines] of [...options.changed].toSorted(([a], [b]) => a.localeCompare(b))) {
|
|
91758
|
-
const relative = toPosix(
|
|
92841
|
+
const relative = toPosix(path46.relative(repo, absolute));
|
|
91759
92842
|
if (!CODE_FILE.test(relative) || TEST_FILE.test(relative) || relative.endsWith(".d.ts"))
|
|
91760
92843
|
continue;
|
|
91761
92844
|
if (NOT_SOURCE.test(relative) || !existsSync19(absolute))
|
|
@@ -91769,9 +92852,9 @@ async function runMutation(options) {
|
|
|
91769
92852
|
for (const mutant of mutants)
|
|
91770
92853
|
if (!related.has(mutant.file))
|
|
91771
92854
|
related.set(mutant.file, relatedTests(mutant.file, testFiles));
|
|
91772
|
-
const cwdRelative =
|
|
92855
|
+
const cwdRelative = path46.relative(repo, options.cwd);
|
|
91773
92856
|
const asArg = (file) => {
|
|
91774
|
-
const relative = toPosix(
|
|
92857
|
+
const relative = toPosix(path46.relative(cwdRelative, file));
|
|
91775
92858
|
return shellQuote(relative.startsWith(".") ? relative : `./${relative}`);
|
|
91776
92859
|
};
|
|
91777
92860
|
const commandFor = (tests) => options.command.replaceAll("{files}", () => tests.map(asArg).join(" "));
|
|
@@ -91806,7 +92889,7 @@ async function runMutation(options) {
|
|
|
91806
92889
|
return finish2(options, 0, results, counts, started);
|
|
91807
92890
|
removeStaleCopies();
|
|
91808
92891
|
const { dir: copyDir, tree: copy } = copyWorkingTree(repo);
|
|
91809
|
-
const copyCwd =
|
|
92892
|
+
const copyCwd = path46.join(copy, cwdRelative);
|
|
91810
92893
|
let current = null;
|
|
91811
92894
|
const restore = () => {
|
|
91812
92895
|
if (current)
|
|
@@ -91841,7 +92924,7 @@ async function runMutation(options) {
|
|
|
91841
92924
|
record({ ...mutant, status: "skipped", durationMs: 0, tests, reason: "budget" });
|
|
91842
92925
|
continue;
|
|
91843
92926
|
}
|
|
91844
|
-
const original = readFileSync20(
|
|
92927
|
+
const original = readFileSync20(path46.join(copy, mutant.file), "utf8");
|
|
91845
92928
|
const mutated = original.slice(0, mutant.start) + mutant.replacement + original.slice(mutant.end);
|
|
91846
92929
|
current = { file: mutant.file, original };
|
|
91847
92930
|
if (!writeInside(copy, mutant.file, mutated)) {
|
|
@@ -91898,8 +92981,8 @@ var NOT_SOURCE, FLIPS, isStringy = (node) => node.type === "StringLiteral" || no
|
|
|
91898
92981
|
return error.code === "EPERM";
|
|
91899
92982
|
}
|
|
91900
92983
|
}, inside2 = (root, file) => {
|
|
91901
|
-
const relative =
|
|
91902
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
92984
|
+
const relative = path46.relative(root, file);
|
|
92985
|
+
return relative === "" || !relative.startsWith("..") && !path46.isAbsolute(relative);
|
|
91903
92986
|
}, active, killTree = (child) => {
|
|
91904
92987
|
try {
|
|
91905
92988
|
if (child.pid && process.platform !== "win32")
|
|
@@ -91945,10 +93028,10 @@ var init_mutate = __esm(() => {
|
|
|
91945
93028
|
// packages/workit-cli/src/verbs/test-audit.ts
|
|
91946
93029
|
var exports_test_audit = {};
|
|
91947
93030
|
__export(exports_test_audit, {
|
|
91948
|
-
run: () =>
|
|
93031
|
+
run: () => run17
|
|
91949
93032
|
});
|
|
91950
93033
|
import { existsSync as existsSync20 } from "node:fs";
|
|
91951
|
-
import
|
|
93034
|
+
import path47 from "node:path";
|
|
91952
93035
|
function parse7(argv) {
|
|
91953
93036
|
const options = {
|
|
91954
93037
|
paths: [],
|
|
@@ -92042,14 +93125,14 @@ function human(data) {
|
|
|
92042
93125
|
}
|
|
92043
93126
|
return lines;
|
|
92044
93127
|
}
|
|
92045
|
-
async function
|
|
93128
|
+
async function run17(argv, io) {
|
|
92046
93129
|
const options = parse7(argv);
|
|
92047
93130
|
if (typeof options === "string")
|
|
92048
93131
|
return emit(io, fail("invalid_input", `${options}
|
|
92049
|
-
usage: ${
|
|
93132
|
+
usage: ${USAGE10}`));
|
|
92050
93133
|
const top = git6(io.cwd, ["rev-parse", "--show-toplevel"]);
|
|
92051
|
-
const repo = top.ok ?
|
|
92052
|
-
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);
|
|
92053
93136
|
let base = null;
|
|
92054
93137
|
let changed = null;
|
|
92055
93138
|
if (options.diff || options.mutate) {
|
|
@@ -92132,7 +93215,7 @@ usage: ${USAGE9}`));
|
|
|
92132
93215
|
}
|
|
92133
93216
|
return emit(io, ok(data), human);
|
|
92134
93217
|
}
|
|
92135
|
-
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, "'\\''")}'`;
|
|
92136
93219
|
var init_test_audit = __esm(() => {
|
|
92137
93220
|
init_audit();
|
|
92138
93221
|
init_mutate();
|
|
@@ -92257,14 +93340,14 @@ var init_youtrack_writes = __esm(() => {
|
|
|
92257
93340
|
// packages/workit-cli/src/verbs/youtrack.ts
|
|
92258
93341
|
var exports_youtrack = {};
|
|
92259
93342
|
__export(exports_youtrack, {
|
|
92260
|
-
run: () =>
|
|
93343
|
+
run: () => run18
|
|
92261
93344
|
});
|
|
92262
93345
|
import { readFileSync as readFileSync21 } from "node:fs";
|
|
92263
|
-
import
|
|
92264
|
-
async function
|
|
93346
|
+
import path48 from "node:path";
|
|
93347
|
+
async function run18(argv, io) {
|
|
92265
93348
|
const [sub, ...rest] = argv;
|
|
92266
93349
|
if (sub !== "note" && sub !== "time" && sub !== "meeting")
|
|
92267
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93350
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE11);
|
|
92268
93351
|
const flags = parseFlags(rest, {
|
|
92269
93352
|
markdown: "value",
|
|
92270
93353
|
file: "value",
|
|
@@ -92273,18 +93356,18 @@ async function run17(argv, io) {
|
|
|
92273
93356
|
date: "value"
|
|
92274
93357
|
});
|
|
92275
93358
|
if (typeof flags === "string")
|
|
92276
|
-
return usage3(io, flags,
|
|
93359
|
+
return usage3(io, flags, USAGE11);
|
|
92277
93360
|
const [issueId, extra] = flags.positionals;
|
|
92278
93361
|
if (!issueId || !ISSUE_RE2.test(issueId))
|
|
92279
|
-
return usage3(io, "an issue id like ABC-123 is required",
|
|
93362
|
+
return usage3(io, "an issue id like ABC-123 is required", USAGE11);
|
|
92280
93363
|
if (extra !== undefined)
|
|
92281
|
-
return usage3(io, `unexpected argument ${extra}`,
|
|
93364
|
+
return usage3(io, `unexpected argument ${extra}`, USAGE11);
|
|
92282
93365
|
const minutes = positiveInt(flags.values.minutes, "--minutes");
|
|
92283
93366
|
if (typeof minutes === "string")
|
|
92284
|
-
return usage3(io, minutes,
|
|
93367
|
+
return usage3(io, minutes, USAGE11);
|
|
92285
93368
|
const date = youTrackWorkDateMs(flags.values.date ?? "auto");
|
|
92286
93369
|
if ("error" in date)
|
|
92287
|
-
return usage3(io, date.error,
|
|
93370
|
+
return usage3(io, date.error, USAGE11);
|
|
92288
93371
|
const dateMs = date.data.dateMs;
|
|
92289
93372
|
const markedDay = flags.values.date !== undefined && flags.values.date !== "auto" ? { dateMs } : {};
|
|
92290
93373
|
const root = io.cwd;
|
|
@@ -92302,24 +93385,24 @@ async function run17(argv, io) {
|
|
|
92302
93385
|
};
|
|
92303
93386
|
if (sub === "note") {
|
|
92304
93387
|
if (Boolean(flags.values.markdown) === Boolean(flags.values.file))
|
|
92305
|
-
return usage3(io, "pass exactly one of --markdown or --file",
|
|
93388
|
+
return usage3(io, "pass exactly one of --markdown or --file", USAGE11);
|
|
92306
93389
|
let markdown = flags.values.markdown ?? "";
|
|
92307
93390
|
if (flags.values.file)
|
|
92308
93391
|
try {
|
|
92309
|
-
markdown = readFileSync21(
|
|
93392
|
+
markdown = readFileSync21(path48.resolve(io.cwd, flags.values.file), "utf8");
|
|
92310
93393
|
} catch (error) {
|
|
92311
|
-
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`,
|
|
93394
|
+
return usage3(io, `cannot read ${flags.values.file}: ${String(error)}`, USAGE11);
|
|
92312
93395
|
}
|
|
92313
93396
|
if (!markdown.trim())
|
|
92314
|
-
return usage3(io, "the note is empty",
|
|
93397
|
+
return usage3(io, "the note is empty", USAGE11);
|
|
92315
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);
|
|
92316
93399
|
if (missing)
|
|
92317
93400
|
return emit(io, fail("unavailable", missing));
|
|
92318
93401
|
} else {
|
|
92319
93402
|
if (!minutes)
|
|
92320
|
-
return usage3(io, `${sub} needs --minutes <n>`,
|
|
93403
|
+
return usage3(io, `${sub} needs --minutes <n>`, USAGE11);
|
|
92321
93404
|
if (sub === "meeting" && !flags.values.text)
|
|
92322
|
-
return usage3(io, "meeting needs --text <what the meeting was>",
|
|
93405
|
+
return usage3(io, "meeting needs --text <what the meeting was>", USAGE11);
|
|
92323
93406
|
const text = flags.values.text ?? "";
|
|
92324
93407
|
const missing = step(sub, "workItems", { operation: sub, issueId, minutes, text, ...markedDay }, text, (marked) => logTimeUpdate({ issueId, minutes, text: marked, dateMs, workspace_root: root }));
|
|
92325
93408
|
if (missing)
|
|
@@ -92348,7 +93431,7 @@ async function run17(argv, io) {
|
|
|
92348
93431
|
...data.steps.flatMap((entry) => entry.note ? [` note (${entry.step}): ${entry.note}`] : [])
|
|
92349
93432
|
]);
|
|
92350
93433
|
}
|
|
92351
|
-
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) => {
|
|
92352
93435
|
try {
|
|
92353
93436
|
return { ok: true, data: unwrap2(value) };
|
|
92354
93437
|
} catch (error) {
|
|
@@ -92372,14 +93455,14 @@ var init_youtrack2 = __esm(() => {
|
|
|
92372
93455
|
// packages/workit-cli/src/verbs/changelog.ts
|
|
92373
93456
|
var exports_changelog = {};
|
|
92374
93457
|
__export(exports_changelog, {
|
|
92375
|
-
run: () =>
|
|
93458
|
+
run: () => run19
|
|
92376
93459
|
});
|
|
92377
93460
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
92378
|
-
import
|
|
92379
|
-
async function
|
|
93461
|
+
import path49 from "node:path";
|
|
93462
|
+
async function run19(argv, io) {
|
|
92380
93463
|
const [sub, ...rest] = argv;
|
|
92381
93464
|
if (sub !== "apply")
|
|
92382
|
-
return usage3(io, `unknown subcommand "${sub ?? ""}"`,
|
|
93465
|
+
return usage3(io, `unknown subcommand "${sub ?? ""}"`, USAGE12);
|
|
92383
93466
|
const flags = parseFlags(rest, {
|
|
92384
93467
|
entries: "value",
|
|
92385
93468
|
path: "value",
|
|
@@ -92387,18 +93470,18 @@ async function run18(argv, io) {
|
|
|
92387
93470
|
preview: "boolean"
|
|
92388
93471
|
});
|
|
92389
93472
|
if (typeof flags === "string")
|
|
92390
|
-
return usage3(io, flags,
|
|
93473
|
+
return usage3(io, flags, USAGE12);
|
|
92391
93474
|
if (flags.positionals.length)
|
|
92392
|
-
return usage3(io, `unexpected argument ${flags.positionals[0]}`,
|
|
93475
|
+
return usage3(io, `unexpected argument ${flags.positionals[0]}`, USAGE12);
|
|
92393
93476
|
const normalizeOnly = flags.booleans.has("normalize-only");
|
|
92394
93477
|
if (!normalizeOnly && !flags.values.entries)
|
|
92395
|
-
return usage3(io, "pass --entries <JSON> or --normalize-only",
|
|
93478
|
+
return usage3(io, "pass --entries <JSON> or --normalize-only", USAGE12);
|
|
92396
93479
|
let entries;
|
|
92397
93480
|
if (flags.values.entries)
|
|
92398
93481
|
try {
|
|
92399
93482
|
entries = readEntries(flags.values.entries, io.cwd);
|
|
92400
93483
|
} catch (error) {
|
|
92401
|
-
return usage3(io, `--entries is not valid JSON: ${String(error)}`,
|
|
93484
|
+
return usage3(io, `--entries is not valid JSON: ${String(error)}`, USAGE12);
|
|
92402
93485
|
}
|
|
92403
93486
|
const input = {
|
|
92404
93487
|
entries,
|
|
@@ -92420,8 +93503,8 @@ async function run18(argv, io) {
|
|
|
92420
93503
|
`changelog updated: ${String(applied.path ?? "CHANGELOG.md")}`
|
|
92421
93504
|
]);
|
|
92422
93505
|
}
|
|
92423
|
-
var
|
|
92424
|
-
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;
|
|
92425
93508
|
return JSON.parse(text);
|
|
92426
93509
|
};
|
|
92427
93510
|
var init_changelog2 = __esm(() => {
|
|
@@ -92433,7 +93516,7 @@ var init_changelog2 = __esm(() => {
|
|
|
92433
93516
|
// packages/workit-cli/src/verbs/handoff.ts
|
|
92434
93517
|
var exports_handoff = {};
|
|
92435
93518
|
__export(exports_handoff, {
|
|
92436
|
-
run: () =>
|
|
93519
|
+
run: () => run20
|
|
92437
93520
|
});
|
|
92438
93521
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
92439
93522
|
function human2(brief) {
|
|
@@ -92481,11 +93564,11 @@ async function resumeBrief(argv, io) {
|
|
|
92481
93564
|
strict: true
|
|
92482
93565
|
}));
|
|
92483
93566
|
} catch (error) {
|
|
92484
|
-
return emit(io, fail("invalid_input", error.message, { unblock:
|
|
93567
|
+
return emit(io, fail("invalid_input", error.message, { unblock: USAGE13 }));
|
|
92485
93568
|
}
|
|
92486
93569
|
const last = values.last === undefined ? 10 : Number(values.last);
|
|
92487
93570
|
if (!Number.isInteger(last) || last < 1)
|
|
92488
|
-
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 }));
|
|
92489
93572
|
const built = buildHandoff(io.cwd, {
|
|
92490
93573
|
last,
|
|
92491
93574
|
note: values.note ?? null,
|
|
@@ -92503,13 +93586,13 @@ async function resumeBrief(argv, io) {
|
|
|
92503
93586
|
}
|
|
92504
93587
|
return emit(io, ok({ ...data, recorded }), human2);
|
|
92505
93588
|
}
|
|
92506
|
-
async function
|
|
93589
|
+
async function run20(argv, io) {
|
|
92507
93590
|
if (!argv.includes("--task"))
|
|
92508
93591
|
return resumeBrief(argv, io);
|
|
92509
93592
|
await Promise.resolve().then(() => init_task());
|
|
92510
93593
|
return runTaskCommand(["handoff", ...argv]);
|
|
92511
93594
|
}
|
|
92512
|
-
var
|
|
93595
|
+
var USAGE13 = "workit handoff [--note <text>] [--next <text>] [--record] [--last <n>] [--json]", short5 = (sha) => sha ? sha.slice(0, 12) : "no commit";
|
|
92513
93596
|
var init_handoff = __esm(() => {
|
|
92514
93597
|
init_ledger();
|
|
92515
93598
|
init_output();
|
|
@@ -92518,7 +93601,7 @@ var init_handoff = __esm(() => {
|
|
|
92518
93601
|
// packages/workit-cli/src/main.ts
|
|
92519
93602
|
init_package();
|
|
92520
93603
|
init_output();
|
|
92521
|
-
import
|
|
93604
|
+
import path50 from "node:path";
|
|
92522
93605
|
|
|
92523
93606
|
// packages/workit-cli/src/verbs/registry.ts
|
|
92524
93607
|
var TASK_FAMILY_NAMES = [
|
|
@@ -92630,6 +93713,13 @@ var VERBS = [
|
|
|
92630
93713
|
summary: "Plan, restack (lease push + retarget) and land a base-branch stack of PRs, root first",
|
|
92631
93714
|
load: () => Promise.resolve().then(() => (init_stack2(), exports_stack))
|
|
92632
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
|
+
},
|
|
92633
93723
|
{
|
|
92634
93724
|
name: "ledger",
|
|
92635
93725
|
group: "delivery",
|
|
@@ -92748,7 +93838,7 @@ var defaultIo = (json) => ({
|
|
|
92748
93838
|
});
|
|
92749
93839
|
async function main(argv, overrides = {}, options = {}) {
|
|
92750
93840
|
const parsed = parseGlobals(argv);
|
|
92751
|
-
const cwd =
|
|
93841
|
+
const cwd = path50.resolve(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
|
|
92752
93842
|
const io = { ...defaultIo(parsed.json), ...overrides, json: parsed.json, cwd };
|
|
92753
93843
|
if (parsed.error)
|
|
92754
93844
|
return emit(io, fail("invalid_input", parsed.error));
|