@brainervirus/workit-cursor 8.12.3 → 8.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +1855 -1661
- package/dist/mcp-server.js +592 -369
- package/dist/workit-hook.js +1241 -1037
- package/package.json +4 -4
- package/skills/workit-fanout/references/orchestration.md +4 -3
- package/skills/workit-review/SKILL.md +1 -1
- package/skills/workit-ship/SKILL.md +20 -20
- package/skills/workit-ship/references/release.md +26 -0
|
@@ -8638,1557 +8638,1697 @@ var init_rev = __esm(() => {
|
|
|
8638
8638
|
MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
8639
8639
|
});
|
|
8640
8640
|
|
|
8641
|
-
// packages/workit-core/src/
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
}
|
|
8646
|
-
return { error_name: "unknown", error: String(err) };
|
|
8647
|
-
};
|
|
8648
|
-
var init_boundary = __esm(() => {
|
|
8649
|
-
EVENT = {
|
|
8650
|
-
initialization: "initialization",
|
|
8651
|
-
provenance: "provenance",
|
|
8652
|
-
assets: "assets",
|
|
8653
|
-
configurationSource: "configuration_source",
|
|
8654
|
-
hooks: "hooks",
|
|
8655
|
-
mcpConnection: "mcp_connection",
|
|
8656
|
-
migration: "migration",
|
|
8657
|
-
installSteps: "install_steps",
|
|
8658
|
-
uncaughtFailure: "uncaught_failure",
|
|
8659
|
-
toolsFailed: "tools_failed",
|
|
8660
|
-
doctor: "doctor"
|
|
8661
|
-
};
|
|
8662
|
-
});
|
|
8663
|
-
|
|
8664
|
-
// packages/workit-core/src/core/config.ts
|
|
8665
|
-
import {
|
|
8666
|
-
copyFileSync,
|
|
8667
|
-
cpSync,
|
|
8668
|
-
existsSync,
|
|
8669
|
-
mkdirSync,
|
|
8670
|
-
readFileSync,
|
|
8671
|
-
readdirSync,
|
|
8672
|
-
writeFileSync
|
|
8673
|
-
} from "node:fs";
|
|
8674
|
-
import os2 from "node:os";
|
|
8641
|
+
// packages/workit-core/src/store/paths.ts
|
|
8642
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8643
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
8644
|
+
import fs2 from "node:fs";
|
|
8675
8645
|
import path2 from "node:path";
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
}
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8646
|
+
function fastGit(root) {
|
|
8647
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
8648
|
+
return null;
|
|
8649
|
+
let dir = real(root);
|
|
8650
|
+
let device = null;
|
|
8651
|
+
for (;; ) {
|
|
8652
|
+
let stat;
|
|
8653
|
+
try {
|
|
8654
|
+
stat = fs2.statSync(dir);
|
|
8655
|
+
} catch {
|
|
8656
|
+
return null;
|
|
8657
|
+
}
|
|
8658
|
+
if (device !== null && stat.dev !== device)
|
|
8659
|
+
return { kind: "none" };
|
|
8660
|
+
device = stat.dev;
|
|
8661
|
+
const dotGit = path2.join(dir, ".git");
|
|
8662
|
+
let entry = null;
|
|
8663
|
+
try {
|
|
8664
|
+
entry = fs2.statSync(dotGit);
|
|
8665
|
+
} catch {}
|
|
8666
|
+
let gitDir = null;
|
|
8667
|
+
let top = null;
|
|
8668
|
+
if (entry) {
|
|
8669
|
+
top = dir;
|
|
8670
|
+
if (entry.isDirectory())
|
|
8671
|
+
gitDir = dotGit;
|
|
8672
|
+
else if (entry.isFile()) {
|
|
8673
|
+
let text = "";
|
|
8674
|
+
try {
|
|
8675
|
+
text = fs2.readFileSync(dotGit, "utf8");
|
|
8676
|
+
} catch {
|
|
8677
|
+
return null;
|
|
8678
|
+
}
|
|
8679
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
8680
|
+
if (!match)
|
|
8681
|
+
return null;
|
|
8682
|
+
gitDir = path2.resolve(dir, match[1].trim());
|
|
8683
|
+
} else
|
|
8684
|
+
return null;
|
|
8685
|
+
if (!fs2.existsSync(path2.join(gitDir, "HEAD")))
|
|
8686
|
+
return null;
|
|
8687
|
+
} else if (isGitDir(dir))
|
|
8688
|
+
gitDir = dir;
|
|
8689
|
+
if (gitDir) {
|
|
8690
|
+
let common = gitDir;
|
|
8691
|
+
try {
|
|
8692
|
+
common = path2.resolve(gitDir, fs2.readFileSync(path2.join(gitDir, "commondir"), "utf8").trim());
|
|
8693
|
+
} catch {}
|
|
8694
|
+
if (!fs2.existsSync(path2.join(common, "objects")))
|
|
8695
|
+
return null;
|
|
8696
|
+
return {
|
|
8697
|
+
kind: "repo",
|
|
8698
|
+
common: real(common),
|
|
8699
|
+
gitDir: real(gitDir),
|
|
8700
|
+
top: top === null ? null : real(top),
|
|
8701
|
+
reftable: reftableRefs(common)
|
|
8702
|
+
};
|
|
8703
|
+
}
|
|
8704
|
+
const parent = path2.dirname(dir);
|
|
8705
|
+
if (parent === dir)
|
|
8706
|
+
return { kind: "none" };
|
|
8707
|
+
dir = parent;
|
|
8696
8708
|
}
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8709
|
+
}
|
|
8710
|
+
function resolveStore(root) {
|
|
8711
|
+
root = path2.resolve(root);
|
|
8712
|
+
let directory = false;
|
|
8713
|
+
try {
|
|
8714
|
+
directory = fs2.statSync(root).isDirectory();
|
|
8715
|
+
} catch {}
|
|
8716
|
+
if (!directory)
|
|
8717
|
+
return { dir: path2.join(root, ".workit"), shared: false, top: null };
|
|
8718
|
+
const fast = fastGit(root);
|
|
8719
|
+
if (fast?.kind === "none")
|
|
8720
|
+
return { dir: path2.join(root, ".workit"), shared: false, top: null };
|
|
8721
|
+
if (fast?.kind === "repo")
|
|
8722
|
+
return {
|
|
8723
|
+
dir: path2.join(fast.common, "workit"),
|
|
8724
|
+
shared: true,
|
|
8725
|
+
top: fast.top,
|
|
8726
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
8727
|
+
};
|
|
8728
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
8729
|
+
const common = run.stdout.trim();
|
|
8730
|
+
if (run.ok && common) {
|
|
8731
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
8732
|
+
return {
|
|
8733
|
+
dir: path2.join(real(path2.resolve(root, common)), "workit"),
|
|
8734
|
+
shared: true,
|
|
8735
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
8736
|
+
};
|
|
8700
8737
|
}
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8738
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
8739
|
+
return { dir: path2.join(root, ".workit"), shared: false, top: null };
|
|
8740
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
8741
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
8742
|
+
}
|
|
8743
|
+
function resolveTaskKey(root, location) {
|
|
8744
|
+
if (!location.shared)
|
|
8745
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
8746
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
8747
|
+
if (location.git && !location.git.reftable) {
|
|
8748
|
+
let head = "";
|
|
8709
8749
|
try {
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8750
|
+
head = fs2.readFileSync(path2.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
8751
|
+
} catch {}
|
|
8752
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
8753
|
+
if (ref && ref[1] !== ".invalid")
|
|
8754
|
+
return branchKey(ref[1]);
|
|
8755
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
8756
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
8757
|
+
if (rebasing)
|
|
8758
|
+
return branchKey(rebasing);
|
|
8759
|
+
return {
|
|
8760
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
8761
|
+
kind: "detached",
|
|
8762
|
+
branch: null
|
|
8763
|
+
};
|
|
8718
8764
|
}
|
|
8719
8765
|
}
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8766
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
8767
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
8768
|
+
if (name && name !== ".invalid")
|
|
8769
|
+
return branchKey(name);
|
|
8770
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
8771
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
8772
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
8773
|
+
if (rebasing)
|
|
8774
|
+
return branchKey(rebasing);
|
|
8775
|
+
}
|
|
8776
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
8777
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
8778
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
8779
|
+
}
|
|
8780
|
+
function checkoutRootOf(root, location) {
|
|
8781
|
+
const resolved = path2.resolve(root);
|
|
8782
|
+
let canonical = resolved;
|
|
8724
8783
|
try {
|
|
8725
|
-
|
|
8784
|
+
canonical = fs2.realpathSync(resolved);
|
|
8726
8785
|
} catch {
|
|
8727
|
-
return
|
|
8786
|
+
return resolved;
|
|
8728
8787
|
}
|
|
8729
|
-
|
|
8730
|
-
if (
|
|
8731
|
-
return
|
|
8732
|
-
|
|
8788
|
+
const found = location ?? resolveStore(canonical);
|
|
8789
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
8790
|
+
return canonical;
|
|
8791
|
+
if (real(canonical) === found.top)
|
|
8792
|
+
return canonical;
|
|
8733
8793
|
try {
|
|
8734
|
-
|
|
8794
|
+
return fs2.realpathSync(found.top);
|
|
8735
8795
|
} catch {
|
|
8736
|
-
return
|
|
8737
|
-
}
|
|
8738
|
-
if (!isConfigObject(parsed)) {
|
|
8739
|
-
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
8796
|
+
return canonical;
|
|
8740
8797
|
}
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
const
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
commitPolicy: {
|
|
8753
|
-
preset: commitPreset,
|
|
8754
|
-
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
8798
|
+
}
|
|
8799
|
+
function branchRenames(location) {
|
|
8800
|
+
if (!location.shared)
|
|
8801
|
+
return [];
|
|
8802
|
+
const files = [];
|
|
8803
|
+
const walk = (dir) => {
|
|
8804
|
+
let entries;
|
|
8805
|
+
try {
|
|
8806
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
8807
|
+
} catch {
|
|
8808
|
+
return;
|
|
8755
8809
|
}
|
|
8810
|
+
for (const entry of entries)
|
|
8811
|
+
if (entry.isDirectory())
|
|
8812
|
+
walk(path2.join(dir, entry.name));
|
|
8813
|
+
else
|
|
8814
|
+
files.push(path2.join(dir, entry.name));
|
|
8756
8815
|
};
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
const
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
}
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
const allowed = merged.allowed.map(branchGlobPattern);
|
|
8778
|
-
return {
|
|
8779
|
-
preset,
|
|
8780
|
-
allowed,
|
|
8781
|
-
protected: new Set([...merged.protected, ...detected ? PRESETS.gitflow.protected : []].map((p) => p.toLowerCase())),
|
|
8782
|
-
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
8783
|
-
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop",
|
|
8784
|
-
detected: detected !== null
|
|
8785
|
-
};
|
|
8786
|
-
}, COMMIT_PRESETS, resolveCommitPolicy = (config, workspace) => {
|
|
8787
|
-
const wp = workspace?.commitPolicy ?? {};
|
|
8788
|
-
if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
|
|
8789
|
-
return {
|
|
8790
|
-
preset: wp.preset,
|
|
8791
|
-
...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
|
|
8792
|
-
};
|
|
8816
|
+
walk(path2.join(path2.dirname(location.dir), "logs", "refs", "heads"));
|
|
8817
|
+
if (location.git)
|
|
8818
|
+
files.push(path2.join(location.git.gitDir, "logs", "HEAD"));
|
|
8819
|
+
const seen = new Set;
|
|
8820
|
+
const renames = [];
|
|
8821
|
+
for (const file of files) {
|
|
8822
|
+
let text = "";
|
|
8823
|
+
try {
|
|
8824
|
+
text = fs2.readFileSync(file, "utf8");
|
|
8825
|
+
} catch {
|
|
8826
|
+
continue;
|
|
8827
|
+
}
|
|
8828
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
8829
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
8830
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
8831
|
+
if (seen.has(id))
|
|
8832
|
+
continue;
|
|
8833
|
+
seen.add(id);
|
|
8834
|
+
renames.push(rename);
|
|
8835
|
+
}
|
|
8793
8836
|
}
|
|
8794
|
-
return
|
|
8795
|
-
}
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8837
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
8838
|
+
}
|
|
8839
|
+
function followRenames(key, boundAt, renames) {
|
|
8840
|
+
let current = key;
|
|
8841
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
8842
|
+
for (const rename of renames)
|
|
8843
|
+
if (rename.from === current && rename.at >= since) {
|
|
8844
|
+
current = rename.to;
|
|
8845
|
+
since = rename.at;
|
|
8846
|
+
}
|
|
8847
|
+
return current;
|
|
8848
|
+
}
|
|
8849
|
+
var NOT_A_REPO, gitRun = (cwd, args) => {
|
|
8850
|
+
const run = spawnSync2("git", args, {
|
|
8851
|
+
cwd,
|
|
8852
|
+
encoding: "utf8",
|
|
8853
|
+
timeout: 20000,
|
|
8854
|
+
killSignal: "SIGKILL",
|
|
8855
|
+
windowsHide: true,
|
|
8856
|
+
env: {
|
|
8857
|
+
...process.env,
|
|
8858
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
8859
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
8860
|
+
LC_ALL: "C",
|
|
8861
|
+
LANGUAGE: "C"
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
const error = run.error;
|
|
8865
|
+
return {
|
|
8866
|
+
ok: run.status === 0,
|
|
8867
|
+
stdout: run.stdout ?? "",
|
|
8868
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
8869
|
+
missing: error?.code === "ENOENT"
|
|
8817
8870
|
};
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
"freeform",
|
|
8824
|
-
"custom",
|
|
8825
|
-
"auto"
|
|
8826
|
-
];
|
|
8827
|
-
});
|
|
8828
|
-
|
|
8829
|
-
// packages/workit-core/src/core/branch-policy.ts
|
|
8830
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
8831
|
-
var PREFIXES, branchNames = (workspaceRoot) => {
|
|
8832
|
-
const r = spawnSync2("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"], { cwd: workspaceRoot, encoding: "utf8" });
|
|
8833
|
-
if (r.status !== 0)
|
|
8834
|
-
return new Set;
|
|
8835
|
-
return new Set((r.stdout ?? "").split(`
|
|
8836
|
-
`).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
|
|
8837
|
-
}, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
|
|
8838
|
-
const names = branchNames(workspaceRoot);
|
|
8839
|
-
const develop = names.has("develop");
|
|
8840
|
-
const main = names.has("main");
|
|
8841
|
-
const master = names.has("master");
|
|
8842
|
-
const root = main ? "main" : master ? "master" : null;
|
|
8843
|
-
if (develop) {
|
|
8844
|
-
return {
|
|
8845
|
-
preset: "gitflow",
|
|
8846
|
-
developBranch: "develop",
|
|
8847
|
-
integration: "merge",
|
|
8848
|
-
protected: root ? [root, "develop"] : ["develop"],
|
|
8849
|
-
allowed: [...PRESETS.gitflow.allowed],
|
|
8850
|
-
prefixes: PREFIXES
|
|
8851
|
-
};
|
|
8871
|
+
}, shortHash = (value) => createHash3("sha256").update(value).digest("hex").slice(0, 12), real = (file) => {
|
|
8872
|
+
try {
|
|
8873
|
+
return fs2.realpathSync.native(file);
|
|
8874
|
+
} catch {
|
|
8875
|
+
return file;
|
|
8852
8876
|
}
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
protected: [root],
|
|
8859
|
-
allowed: ["*"],
|
|
8860
|
-
prefixes: PREFIXES
|
|
8861
|
-
};
|
|
8877
|
+
}, DISCOVERY_ENV, isGitDir = (dir) => {
|
|
8878
|
+
try {
|
|
8879
|
+
return fs2.statSync(path2.join(dir, "HEAD")).isFile() && fs2.statSync(path2.join(dir, "objects")).isDirectory() && fs2.statSync(path2.join(dir, "refs")).isDirectory();
|
|
8880
|
+
} catch {
|
|
8881
|
+
return false;
|
|
8862
8882
|
}
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
protected: [root],
|
|
8869
|
-
allowed: ["*"],
|
|
8870
|
-
prefixes: PREFIXES
|
|
8871
|
-
};
|
|
8883
|
+
}, reftableRefs = (common) => {
|
|
8884
|
+
try {
|
|
8885
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs2.readFileSync(path2.join(common, "config"), "utf8"));
|
|
8886
|
+
} catch {
|
|
8887
|
+
return false;
|
|
8872
8888
|
}
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
}
|
|
8882
|
-
|
|
8883
|
-
|
|
8889
|
+
}, rebasingBranch = (gitDir) => {
|
|
8890
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
8891
|
+
try {
|
|
8892
|
+
const ref = fs2.readFileSync(path2.join(gitDir, state, "head-name"), "utf8").trim();
|
|
8893
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
8894
|
+
if (match)
|
|
8895
|
+
return match[1];
|
|
8896
|
+
} catch {}
|
|
8897
|
+
}
|
|
8898
|
+
return null;
|
|
8899
|
+
}, checkoutSlug = (root) => {
|
|
8900
|
+
let canonical = real(root);
|
|
8901
|
+
if (process.platform === "win32")
|
|
8902
|
+
canonical = canonical.toLowerCase();
|
|
8903
|
+
const base = path2.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
8904
|
+
return `${base}-${shortHash(canonical)}`;
|
|
8884
8905
|
};
|
|
8885
|
-
var
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8906
|
+
var init_paths = __esm(() => {
|
|
8907
|
+
NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
8908
|
+
DISCOVERY_ENV = [
|
|
8909
|
+
"GIT_DIR",
|
|
8910
|
+
"GIT_COMMON_DIR",
|
|
8911
|
+
"GIT_WORK_TREE",
|
|
8912
|
+
"GIT_CEILING_DIRECTORIES",
|
|
8913
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
8914
|
+
];
|
|
8893
8915
|
});
|
|
8894
8916
|
|
|
8895
|
-
// packages/workit-core/src/core/
|
|
8917
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
8896
8918
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
8897
|
-
import
|
|
8898
|
-
import {
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
|
|
8906
|
-
return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
|
|
8907
|
-
}
|
|
8908
|
-
var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
|
|
8909
|
-
if (!isConfigObject(parsed)) {
|
|
8910
|
-
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
8919
|
+
import fs3 from "node:fs";
|
|
8920
|
+
import { hostname } from "node:os";
|
|
8921
|
+
var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, IN_PROCESS_LOCK_TIMEOUT_MS = 250, defaultLockTimeoutMs, defaultLockTimeout = () => defaultLockTimeoutMs, hostAwareLockTimeout = (cliMs) => defaultLockTimeoutMs > IN_PROCESS_LOCK_TIMEOUT_MS ? cliMs : IN_PROCESS_LOCK_TIMEOUT_MS, parseMetadataLock = (raw) => {
|
|
8922
|
+
let value;
|
|
8923
|
+
try {
|
|
8924
|
+
value = JSON.parse(raw);
|
|
8925
|
+
} catch {
|
|
8926
|
+
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
8911
8927
|
}
|
|
8912
|
-
const
|
|
8913
|
-
if (!
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
path: file,
|
|
8922
|
-
entries: [],
|
|
8923
|
-
error: `${file} has invalid workspace configuration: ${detail}`
|
|
8924
|
-
};
|
|
8928
|
+
const parsed = metadataLockSchema.safeParse(value);
|
|
8929
|
+
if (!parsed.success)
|
|
8930
|
+
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
8931
|
+
return parsed.data;
|
|
8932
|
+
}, parseMetadataLockOrNull = (raw) => {
|
|
8933
|
+
try {
|
|
8934
|
+
return parseMetadataLock(raw);
|
|
8935
|
+
} catch {
|
|
8936
|
+
return null;
|
|
8925
8937
|
}
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
};
|
|
8938
|
+
}, parseProcStatStart = (stat) => {
|
|
8939
|
+
const close = stat.lastIndexOf(")");
|
|
8940
|
+
if (close < 0)
|
|
8941
|
+
return null;
|
|
8942
|
+
return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
|
|
8943
|
+
}, processStartOf = (pid) => {
|
|
8944
|
+
if (process.platform === "linux") {
|
|
8945
|
+
try {
|
|
8946
|
+
return parseProcStatStart(fs3.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
8947
|
+
} catch {
|
|
8948
|
+
return null;
|
|
8935
8949
|
}
|
|
8936
8950
|
}
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
} catch (error) {
|
|
8949
|
-
if (error.code === "ENOENT")
|
|
8950
|
-
return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
|
|
8951
|
-
return {
|
|
8952
|
-
status: "malformed",
|
|
8953
|
-
path: file,
|
|
8954
|
-
entries: [],
|
|
8955
|
-
error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
8956
|
-
};
|
|
8951
|
+
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
8952
|
+
try {
|
|
8953
|
+
const run = spawnSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
8954
|
+
encoding: "utf8",
|
|
8955
|
+
timeout: 1000
|
|
8956
|
+
});
|
|
8957
|
+
const value = run.status === 0 ? run.stdout.trim() : "";
|
|
8958
|
+
return value || null;
|
|
8959
|
+
} catch {
|
|
8960
|
+
return null;
|
|
8961
|
+
}
|
|
8957
8962
|
}
|
|
8958
|
-
|
|
8963
|
+
return null;
|
|
8964
|
+
}, readTrimmed = (file) => {
|
|
8959
8965
|
try {
|
|
8960
|
-
|
|
8966
|
+
return fs3.readFileSync(file, "utf8").trim() || null;
|
|
8961
8967
|
} catch {
|
|
8962
|
-
return
|
|
8963
|
-
status: "malformed",
|
|
8964
|
-
path: file,
|
|
8965
|
-
entries: [],
|
|
8966
|
-
revision: workspacesRevision(raw),
|
|
8967
|
-
error: `${file} is not valid JSON`
|
|
8968
|
-
};
|
|
8968
|
+
return null;
|
|
8969
8969
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
out += ".*";
|
|
8989
|
-
}
|
|
8990
|
-
} else {
|
|
8991
|
-
out += ".*";
|
|
8992
|
-
}
|
|
8993
|
-
i++;
|
|
8994
|
-
}
|
|
8995
|
-
} else {
|
|
8996
|
-
out += "[^/]*";
|
|
8997
|
-
}
|
|
8998
|
-
} else {
|
|
8999
|
-
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
9000
|
-
}
|
|
8970
|
+
}, cachedLockHost = null, localLockHost = () => {
|
|
8971
|
+
if (cachedLockHost !== null)
|
|
8972
|
+
return cachedLockHost;
|
|
8973
|
+
let pidns = null;
|
|
8974
|
+
try {
|
|
8975
|
+
pidns = /\[(\d+)\]/.exec(fs3.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
8976
|
+
} catch {}
|
|
8977
|
+
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
8978
|
+
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
8979
|
+
return cachedLockHost;
|
|
8980
|
+
}, pidAlive = (pid) => {
|
|
8981
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
8982
|
+
return false;
|
|
8983
|
+
try {
|
|
8984
|
+
process.kill(pid, 0);
|
|
8985
|
+
return true;
|
|
8986
|
+
} catch (error) {
|
|
8987
|
+
return error.code === "EPERM";
|
|
9001
8988
|
}
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
const
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
8989
|
+
}, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
|
|
8990
|
+
const lock = metadataLockSchema.safeParse(payload);
|
|
8991
|
+
if (!lock.success)
|
|
8992
|
+
return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
|
|
8993
|
+
const { pid, processStart, host } = lock.data;
|
|
8994
|
+
const [lockName, lockSpace] = host.split("#");
|
|
8995
|
+
const [localName, localSpace] = localHost.split("#");
|
|
8996
|
+
if (lockSpace && localSpace && lockName === localName) {
|
|
8997
|
+
const [lockNs, lockBoot] = lockSpace.split(":");
|
|
8998
|
+
const [localNs, localBoot] = localSpace.split(":");
|
|
8999
|
+
if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
|
|
9000
|
+
return { state: "stale", reason: "lock was taken before this machine rebooted" };
|
|
9013
9001
|
}
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9002
|
+
if (host !== localHost)
|
|
9003
|
+
return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
|
|
9004
|
+
if (!pidAlive(pid))
|
|
9005
|
+
return { state: "stale", reason: `pid ${pid} is not running` };
|
|
9006
|
+
const currentStart = processStartOf(pid);
|
|
9007
|
+
if (processStart !== null && currentStart !== null && processStart !== currentStart)
|
|
9008
|
+
return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
|
|
9009
|
+
if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
|
|
9018
9010
|
return {
|
|
9019
|
-
|
|
9020
|
-
|
|
9011
|
+
state: "stale",
|
|
9012
|
+
reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
|
|
9021
9013
|
};
|
|
9022
|
-
}
|
|
9023
|
-
|
|
9024
|
-
}, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
|
|
9025
|
-
const cached = mainCheckouts.get(cwd);
|
|
9026
|
-
if (cached !== undefined)
|
|
9027
|
-
return cached;
|
|
9028
|
-
const env = { ...process.env };
|
|
9029
|
-
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
|
|
9030
|
-
delete env[name];
|
|
9031
|
-
const git = (args) => {
|
|
9032
|
-
const run = spawnSync3("git", args, {
|
|
9033
|
-
cwd,
|
|
9034
|
-
env,
|
|
9035
|
-
encoding: "utf8",
|
|
9036
|
-
timeout: 5000,
|
|
9037
|
-
windowsHide: true
|
|
9038
|
-
});
|
|
9039
|
-
return run.status === 0 ? run.stdout : null;
|
|
9040
|
-
};
|
|
9041
|
-
let main = null;
|
|
9042
|
-
const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
|
|
9043
|
-
const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
|
|
9044
|
-
if (gitDir && common && path3.resolve(gitDir) !== path3.resolve(common)) {
|
|
9045
|
-
const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
|
|
9046
|
-
main = listed?.trim() || (path3.basename(common) === ".git" ? path3.dirname(common) : common);
|
|
9047
|
-
}
|
|
9048
|
-
mainCheckouts.set(cwd, main);
|
|
9049
|
-
return main;
|
|
9050
|
-
}, matchingEntries = (cwd, entries) => {
|
|
9051
|
-
const match = (where) => {
|
|
9052
|
-
const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
|
|
9053
|
-
return entries.filter((entry) => {
|
|
9054
|
-
const glob = entry.glob.replaceAll("\\", "/");
|
|
9055
|
-
const canonical = canonicalGlob(glob);
|
|
9056
|
-
return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
|
|
9057
|
-
});
|
|
9058
|
-
};
|
|
9059
|
-
const direct = match(cwd);
|
|
9060
|
-
if (direct.length)
|
|
9061
|
-
return direct;
|
|
9062
|
-
const main = mainCheckoutOf(cwd);
|
|
9063
|
-
return main ? match(main) : direct;
|
|
9064
|
-
}, realpathOf = (p) => {
|
|
9065
|
-
const real = realpathSync.native ?? realpathSync;
|
|
9014
|
+
return { state: "live", reason: `held by running pid ${pid}` };
|
|
9015
|
+
}, ageOf = (file, nowMs) => {
|
|
9066
9016
|
try {
|
|
9067
|
-
return
|
|
9017
|
+
return nowMs - fs3.lstatSync(file).mtimeMs;
|
|
9068
9018
|
} catch {
|
|
9069
|
-
return
|
|
9070
|
-
}
|
|
9071
|
-
}, canonicalGlob = (glob) => {
|
|
9072
|
-
const m = /[*?[\]{]/.exec(glob);
|
|
9073
|
-
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
9074
|
-
const rest = m ? glob.slice(m.index) : "";
|
|
9075
|
-
if (!prefix)
|
|
9076
|
-
return glob;
|
|
9077
|
-
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
9078
|
-
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
9079
|
-
}, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
|
|
9080
|
-
if (workspaceName !== undefined) {
|
|
9081
|
-
const named = matches.filter((entry) => entry.name === workspaceName);
|
|
9082
|
-
if (named.length === 1)
|
|
9083
|
-
return named[0];
|
|
9084
|
-
if (named.length > 1)
|
|
9085
|
-
throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
|
|
9086
|
-
if (matches.length > 0) {
|
|
9087
|
-
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
|
|
9088
|
-
}
|
|
9089
|
-
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
9090
|
-
}
|
|
9091
|
-
if (matches.length < 2)
|
|
9092
|
-
return matches[0];
|
|
9093
|
-
const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
|
|
9094
|
-
const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
|
|
9095
|
-
if (mostSpecific.length > 1) {
|
|
9096
|
-
throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
|
|
9097
|
-
}
|
|
9098
|
-
return mostSpecific[0];
|
|
9099
|
-
}, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
|
|
9100
|
-
const matches = matchingEntries(cwd, entries);
|
|
9101
|
-
return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
|
|
9102
|
-
}, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
|
|
9103
|
-
const result = readWorkspacesResult(dir);
|
|
9104
|
-
if (result.status === "malformed" || result.status === "invalid")
|
|
9105
|
-
throw new WorkspaceConfigError(result.error);
|
|
9106
|
-
return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
|
|
9107
|
-
}, runtimeWorkspacePolicySchema = (kind) => {
|
|
9108
|
-
const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
|
|
9109
|
-
const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
|
|
9110
|
-
provider: _enum(["gitlab", "github"]),
|
|
9111
|
-
defaultTargetBranch: nonBlank.optional(),
|
|
9112
|
-
account: nonBlank.optional()
|
|
9113
|
-
}).passthrough();
|
|
9114
|
-
const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
|
|
9115
|
-
const entrySchema = object({
|
|
9116
|
-
name: nonBlank,
|
|
9117
|
-
glob: nonBlank,
|
|
9118
|
-
[key]: policySchema.optional(),
|
|
9119
|
-
defaultProfile: nonBlank.optional(),
|
|
9120
|
-
profiles: record(nonBlank, profileSchema).optional()
|
|
9121
|
-
}).passthrough();
|
|
9122
|
-
return entrySchema;
|
|
9123
|
-
}, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
|
|
9124
|
-
const file = path3.join(dir, "workspaces.json");
|
|
9125
|
-
let raw;
|
|
9126
|
-
try {
|
|
9127
|
-
raw = readFileSync2(file, "utf8");
|
|
9128
|
-
} catch (error) {
|
|
9129
|
-
if (error.code === "ENOENT")
|
|
9130
|
-
return null;
|
|
9131
|
-
throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
9019
|
+
return null;
|
|
9132
9020
|
}
|
|
9133
|
-
|
|
9021
|
+
}, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
9022
|
+
const guard = `${lockPath}.reclaim`;
|
|
9023
|
+
const age = ageOf(guard, nowMs);
|
|
9024
|
+
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
9025
|
+
return false;
|
|
9134
9026
|
try {
|
|
9135
|
-
|
|
9027
|
+
fs3.rmdirSync(guard);
|
|
9028
|
+
return true;
|
|
9136
9029
|
} catch {
|
|
9137
|
-
|
|
9138
|
-
}
|
|
9139
|
-
const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
|
|
9140
|
-
if (!validated.success) {
|
|
9141
|
-
const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
|
|
9142
|
-
throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
|
|
9143
|
-
}
|
|
9144
|
-
const entries = validated.data.workspaces;
|
|
9145
|
-
for (const entry of entries ?? []) {
|
|
9146
|
-
const glob = validateWorkspaceGlob(entry.glob);
|
|
9147
|
-
if (!glob.ok)
|
|
9148
|
-
throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
|
|
9030
|
+
return false;
|
|
9149
9031
|
}
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
}
|
|
9162
|
-
|
|
9032
|
+
};
|
|
9033
|
+
var init_store_lock = __esm(() => {
|
|
9034
|
+
init_zod();
|
|
9035
|
+
init_task_contract();
|
|
9036
|
+
init_paths();
|
|
9037
|
+
metadataLockSchema = object({
|
|
9038
|
+
pid: number2().int().nonnegative().safe(),
|
|
9039
|
+
processStart: string2().nullable(),
|
|
9040
|
+
host: string2().min(1),
|
|
9041
|
+
nonce: string2().min(1),
|
|
9042
|
+
externalAction: literal(true).optional()
|
|
9043
|
+
}).strict();
|
|
9044
|
+
FOREIGN_LOCK_TTL_MS = 10 * 60000;
|
|
9045
|
+
defaultLockTimeoutMs = IN_PROCESS_LOCK_TIMEOUT_MS;
|
|
9046
|
+
TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
|
|
9047
|
+
});
|
|
9048
|
+
|
|
9049
|
+
// packages/workit-core/src/core/boundary.ts
|
|
9050
|
+
var EVENT, errorDetail = (err) => {
|
|
9051
|
+
if (err instanceof Error) {
|
|
9052
|
+
return { error_name: err.name, error: err.message };
|
|
9163
9053
|
}
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9054
|
+
return { error_name: "unknown", error: String(err) };
|
|
9055
|
+
};
|
|
9056
|
+
var init_boundary = __esm(() => {
|
|
9057
|
+
EVENT = {
|
|
9058
|
+
initialization: "initialization",
|
|
9059
|
+
provenance: "provenance",
|
|
9060
|
+
assets: "assets",
|
|
9061
|
+
configurationSource: "configuration_source",
|
|
9062
|
+
hooks: "hooks",
|
|
9063
|
+
mcpConnection: "mcp_connection",
|
|
9064
|
+
migration: "migration",
|
|
9065
|
+
installSteps: "install_steps",
|
|
9066
|
+
uncaughtFailure: "uncaught_failure",
|
|
9067
|
+
toolsFailed: "tools_failed",
|
|
9068
|
+
doctor: "doctor"
|
|
9069
|
+
};
|
|
9070
|
+
});
|
|
9071
|
+
|
|
9072
|
+
// packages/workit-core/src/core/config.ts
|
|
9073
|
+
import {
|
|
9074
|
+
copyFileSync,
|
|
9075
|
+
cpSync,
|
|
9076
|
+
existsSync,
|
|
9077
|
+
mkdirSync,
|
|
9078
|
+
readFileSync,
|
|
9079
|
+
readdirSync,
|
|
9080
|
+
writeFileSync
|
|
9081
|
+
} from "node:fs";
|
|
9082
|
+
import os2 from "node:os";
|
|
9083
|
+
import path3 from "node:path";
|
|
9084
|
+
var diagnosticLogger, PRESETS, mergePreset = (preset, input = {}, current = {
|
|
9085
|
+
branchPolicy: { preset, allowed: [], protected: [] }
|
|
9086
|
+
}) => {
|
|
9087
|
+
const defs = PRESETS[preset];
|
|
9169
9088
|
return {
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9089
|
+
preset,
|
|
9090
|
+
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
9091
|
+
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
9173
9092
|
};
|
|
9174
|
-
},
|
|
9175
|
-
|
|
9176
|
-
|
|
9093
|
+
}, resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path3.join(process.env.XDG_CONFIG_HOME || path3.join(os2.homedir(), ".config"), "workit"), migratedDir = null, migrationFailed = false, ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
9094
|
+
if (migratedDir === dir)
|
|
9095
|
+
return dir;
|
|
9096
|
+
if (process.env.WORKFLOW_TOOLKIT_CONFIG || process.env.WORKFLOW_TOOLKIT_CONFIG_DIR) {
|
|
9097
|
+
migratedDir = dir;
|
|
9098
|
+
return dir;
|
|
9099
|
+
}
|
|
9100
|
+
const legacy = path3.join(process.env.XDG_CONFIG_HOME || path3.join(os2.homedir(), ".config"), "workflow-toolkit");
|
|
9101
|
+
if (!existsSync(legacy)) {
|
|
9102
|
+
migratedDir = dir;
|
|
9103
|
+
return dir;
|
|
9104
|
+
}
|
|
9105
|
+
if (!migrationFailed && existsSync(dir)) {
|
|
9106
|
+
migratedDir = dir;
|
|
9107
|
+
return dir;
|
|
9108
|
+
}
|
|
9109
|
+
migrationFailed = false;
|
|
9110
|
+
mkdirSync(dir, { recursive: true });
|
|
9111
|
+
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
9112
|
+
for (const entry of readdirSync(legacy, { withFileTypes: true })) {
|
|
9113
|
+
const src = path3.join(legacy, entry.name);
|
|
9114
|
+
const dest = path3.join(dir, entry.name);
|
|
9115
|
+
if (existsSync(dest))
|
|
9116
|
+
continue;
|
|
9117
|
+
try {
|
|
9118
|
+
if (entry.isDirectory())
|
|
9119
|
+
cpSync(src, dest, { recursive: true });
|
|
9120
|
+
else if (entry.isFile())
|
|
9121
|
+
copyFileSync(src, dest);
|
|
9122
|
+
} catch (err) {
|
|
9123
|
+
migrationFailed = true;
|
|
9124
|
+
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
9125
|
+
console.warn(`[workit] config migration: a file could not be copied to ${dir}; it will be retried on the next run`);
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
if (!migrationFailed)
|
|
9129
|
+
migratedDir = dir;
|
|
9130
|
+
return dir;
|
|
9131
|
+
}, configDir = () => ensureConfigDir(), LOCALE_RE, DEFAULTS, unchosenBranchPolicy, readSafe = (p) => {
|
|
9132
|
+
try {
|
|
9133
|
+
return readFileSync(p, "utf8");
|
|
9134
|
+
} catch {
|
|
9177
9135
|
return null;
|
|
9178
|
-
const { file, selected } = matched;
|
|
9179
|
-
const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
|
|
9180
|
-
if (!policy.success) {
|
|
9181
|
-
const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
|
|
9182
|
-
throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
|
|
9183
9136
|
}
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
const
|
|
9197
|
-
const
|
|
9137
|
+
}, isConfigObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value), parseConfigResult = (raw, file) => {
|
|
9138
|
+
if (raw === null)
|
|
9139
|
+
return { status: "missing", path: file };
|
|
9140
|
+
let parsed;
|
|
9141
|
+
try {
|
|
9142
|
+
parsed = JSON.parse(raw);
|
|
9143
|
+
} catch {
|
|
9144
|
+
return { status: "malformed", path: file, error: `${file} is not valid JSON` };
|
|
9145
|
+
}
|
|
9146
|
+
if (!isConfigObject(parsed)) {
|
|
9147
|
+
return { status: "malformed", path: file, error: `${file} is not a JSON object` };
|
|
9148
|
+
}
|
|
9149
|
+
const input = parsed;
|
|
9150
|
+
const locale = LOCALE_RE.test(String(input.locale ?? "")) ? input.locale : DEFAULTS.locale;
|
|
9151
|
+
const preset = Object.hasOwn(PRESETS, input.branchPolicy?.preset) ? input.branchPolicy?.preset : "gitflow";
|
|
9152
|
+
const commitPreset = COMMIT_PRESETS.includes(String(input.commitPolicy?.preset ?? "")) ? input.commitPolicy?.preset : "conventional";
|
|
9153
|
+
const config = {
|
|
9154
|
+
locale,
|
|
9155
|
+
localeOptions: Array.isArray(input.localeOptions) ? input.localeOptions : DEFAULTS.localeOptions,
|
|
9156
|
+
branchPolicy: mergePreset(preset, {
|
|
9157
|
+
allowed: Array.isArray(input.branchPolicy?.allowed) ? input.branchPolicy.allowed : undefined,
|
|
9158
|
+
protectedNames: Array.isArray(input.branchPolicy?.protected) ? input.branchPolicy.protected : undefined
|
|
9159
|
+
}, DEFAULTS),
|
|
9160
|
+
commitPolicy: {
|
|
9161
|
+
preset: commitPreset,
|
|
9162
|
+
...typeof input.commitPolicy?.pattern === "string" ? { pattern: input.commitPolicy.pattern } : {}
|
|
9163
|
+
}
|
|
9164
|
+
};
|
|
9165
|
+
if (!Object.hasOwn(PRESETS, input.branchPolicy?.preset))
|
|
9166
|
+
unchosenBranchPolicy.add(config);
|
|
9167
|
+
return { status: "valid", path: file, config };
|
|
9168
|
+
}, readConfigTyped = (dir) => {
|
|
9169
|
+
const file = path3.join(dir ?? configDir(), "config.json");
|
|
9170
|
+
return parseConfigResult(readSafe(file), file);
|
|
9171
|
+
}, readConfig = () => {
|
|
9172
|
+
const result = readConfigTyped();
|
|
9173
|
+
if (result.status === "malformed")
|
|
9174
|
+
throw new Error(result.error);
|
|
9175
|
+
return result.config ?? DEFAULTS;
|
|
9176
|
+
}, vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path3.join(configDir(), "vcs.json"), branchGlobPattern = (glob) => new RegExp(`^${glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"), resolveBranchPolicy = (config, workspace, repoPreset) => {
|
|
9177
|
+
const wp = workspace?.branchPolicy ?? {};
|
|
9178
|
+
const workspacePreset = Object.hasOwn(PRESETS, wp.preset);
|
|
9179
|
+
const detected = !workspacePreset && unchosenBranchPolicy.has(config) ? repoPreset?.() ?? null : null;
|
|
9180
|
+
const preset = workspacePreset ? wp.preset : detected ?? config.branchPolicy?.preset ?? "gitflow";
|
|
9181
|
+
const merged = mergePreset(preset, {
|
|
9182
|
+
allowed: wp.allowed,
|
|
9183
|
+
protectedNames: wp.protected
|
|
9184
|
+
}, config);
|
|
9185
|
+
const allowed = merged.allowed.map(branchGlobPattern);
|
|
9198
9186
|
return {
|
|
9199
|
-
|
|
9200
|
-
|
|
9187
|
+
preset,
|
|
9188
|
+
allowed,
|
|
9189
|
+
protected: new Set([...merged.protected, ...detected ? PRESETS.gitflow.protected : []].map((p) => p.toLowerCase())),
|
|
9190
|
+
integration: wp.integration === "merge" ? "merge" : "pr",
|
|
9191
|
+
defaultTargetBranch: preset === "github-flow" ? "main" : preset === "trunk-based" ? "master" : "develop",
|
|
9192
|
+
detected: detected !== null
|
|
9201
9193
|
};
|
|
9194
|
+
}, COMMIT_PRESETS, resolveCommitPolicy = (config, workspace) => {
|
|
9195
|
+
const wp = workspace?.commitPolicy ?? {};
|
|
9196
|
+
if (COMMIT_PRESETS.includes(String(wp.preset ?? ""))) {
|
|
9197
|
+
return {
|
|
9198
|
+
preset: wp.preset,
|
|
9199
|
+
...typeof wp.pattern === "string" ? { pattern: wp.pattern } : {}
|
|
9200
|
+
};
|
|
9201
|
+
}
|
|
9202
|
+
return config.commitPolicy ?? { preset: "conventional" };
|
|
9202
9203
|
};
|
|
9203
|
-
var
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
prefixes: object({
|
|
9214
|
-
feature: nonBlank,
|
|
9215
|
-
bugfix: nonBlank,
|
|
9216
|
-
release: nonBlank,
|
|
9217
|
-
hotfix: nonBlank
|
|
9218
|
-
}).passthrough().optional(),
|
|
9219
|
-
allowed: array(nonBlank).optional(),
|
|
9220
|
-
protected: array(nonBlank).optional(),
|
|
9221
|
-
integration: _enum(["pr", "merge"]).optional()
|
|
9222
|
-
}).strict();
|
|
9223
|
-
workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
|
|
9224
|
-
workspaceProfileSchema = object({
|
|
9225
|
-
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9226
|
-
commitPolicy: workspaceCommitPolicySchema.optional()
|
|
9227
|
-
}).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
|
|
9228
|
-
message: "a profile must define branchPolicy or commitPolicy"
|
|
9229
|
-
});
|
|
9230
|
-
versionSourceSchema = discriminatedUnion("kind", [
|
|
9231
|
-
object({
|
|
9232
|
-
kind: literal("package-json"),
|
|
9233
|
-
path: nonBlank,
|
|
9234
|
-
field: nonBlank
|
|
9235
|
-
}).strict(),
|
|
9236
|
-
object({ kind: literal("git-tag") }).strict(),
|
|
9237
|
-
object({ kind: literal("manual") }).strict()
|
|
9238
|
-
]);
|
|
9239
|
-
releaseTrackSchema = object({
|
|
9240
|
-
strategy: branchPreset.optional(),
|
|
9241
|
-
productionBranch: nonBlank,
|
|
9242
|
-
integrationBranch: nonBlank,
|
|
9243
|
-
naming: object({
|
|
9244
|
-
feature: nonBlank.optional(),
|
|
9245
|
-
release: nonBlank.optional(),
|
|
9246
|
-
hotfix: nonBlank.optional()
|
|
9247
|
-
}).passthrough().optional(),
|
|
9248
|
-
baseBranch: nonBlank.optional(),
|
|
9249
|
-
mergeBackBranches: array(nonBlank).optional(),
|
|
9250
|
-
pullRequestTarget: nonBlank.optional(),
|
|
9251
|
-
tagNamespace: string2().optional(),
|
|
9252
|
-
versionSource: versionSourceSchema.optional(),
|
|
9253
|
-
requiredChecks: array(nonBlank).optional(),
|
|
9254
|
-
critical: array(nonBlank).optional()
|
|
9255
|
-
}).passthrough().superRefine((track, context) => {
|
|
9256
|
-
for (const [field, values] of [
|
|
9257
|
-
["mergeBackBranches", track.mergeBackBranches ?? []],
|
|
9258
|
-
["requiredChecks", track.requiredChecks ?? []]
|
|
9259
|
-
]) {
|
|
9260
|
-
if (new Set(values).size !== values.length) {
|
|
9261
|
-
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
9262
|
-
}
|
|
9263
|
-
}
|
|
9264
|
-
if (track.versionSource?.kind === "package-json" && (path3.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
|
|
9265
|
-
context.addIssue({
|
|
9266
|
-
code: "custom",
|
|
9267
|
-
path: ["versionSource", "path"],
|
|
9268
|
-
message: "must stay inside the repository"
|
|
9269
|
-
});
|
|
9270
|
-
}
|
|
9271
|
-
});
|
|
9272
|
-
workspaceConfigSchema = object({
|
|
9273
|
-
name: nonBlank,
|
|
9274
|
-
glob: nonBlank,
|
|
9275
|
-
vcs: object({
|
|
9276
|
-
provider: _enum(["gitlab", "github"]),
|
|
9277
|
-
defaultTargetBranch: nonBlank.optional(),
|
|
9278
|
-
tokenFile: nonBlank.optional(),
|
|
9279
|
-
account: nonBlank.optional()
|
|
9280
|
-
}).passthrough().optional(),
|
|
9281
|
-
autoApprove: union([boolean2(), array(nonBlank)]).optional(),
|
|
9282
|
-
defaultEndpoint: unknown().optional(),
|
|
9283
|
-
verification: _enum(["self", "independent"]).optional(),
|
|
9284
|
-
trailers: boolean2().optional(),
|
|
9285
|
-
autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
|
|
9286
|
-
kind,
|
|
9287
|
-
union([boolean2(), literal("verified")]).optional()
|
|
9288
|
-
]))).passthrough().optional(),
|
|
9289
|
-
youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
|
|
9290
|
-
issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
|
|
9291
|
-
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9292
|
-
commitPolicy: workspaceCommitPolicySchema.optional(),
|
|
9293
|
-
defaultProfile: nonBlank.optional(),
|
|
9294
|
-
profiles: record(nonBlank, workspaceProfileSchema).optional(),
|
|
9295
|
-
releaseTracks: record(nonBlank, releaseTrackSchema).optional()
|
|
9296
|
-
}).passthrough().superRefine((workspace, context) => {
|
|
9297
|
-
if (workspace.issues && workspace.vcs?.provider !== "github") {
|
|
9298
|
-
context.addIssue({
|
|
9299
|
-
code: "custom",
|
|
9300
|
-
path: ["issues"],
|
|
9301
|
-
message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
|
|
9302
|
-
});
|
|
9303
|
-
}
|
|
9304
|
-
if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
|
|
9305
|
-
context.addIssue({
|
|
9306
|
-
code: "custom",
|
|
9307
|
-
path: ["defaultProfile"],
|
|
9308
|
-
message: `does not name a configured profile`
|
|
9309
|
-
});
|
|
9310
|
-
}
|
|
9311
|
-
});
|
|
9312
|
-
workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
|
|
9313
|
-
const names = new Set;
|
|
9314
|
-
for (const [index, workspace] of (file.workspaces ?? []).entries()) {
|
|
9315
|
-
if (names.has(workspace.name)) {
|
|
9316
|
-
context.addIssue({
|
|
9317
|
-
code: "custom",
|
|
9318
|
-
path: ["workspaces", index, "name"],
|
|
9319
|
-
message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
|
|
9320
|
-
});
|
|
9321
|
-
}
|
|
9322
|
-
names.add(workspace.name);
|
|
9323
|
-
}
|
|
9324
|
-
});
|
|
9325
|
-
UNSUPPORTED_GLOB = /[?[\]{}]/;
|
|
9326
|
-
UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
|
|
9327
|
-
WorkspaceConfigError = class WorkspaceConfigError extends Error {
|
|
9328
|
-
name = "WorkspaceConfigError";
|
|
9204
|
+
var init_config = __esm(() => {
|
|
9205
|
+
init_boundary();
|
|
9206
|
+
PRESETS = {
|
|
9207
|
+
gitflow: {
|
|
9208
|
+
allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
|
|
9209
|
+
protected: ["main", "develop", "master", "prod", "production"]
|
|
9210
|
+
},
|
|
9211
|
+
"github-flow": { allowed: ["*"], protected: ["main"] },
|
|
9212
|
+
"trunk-based": { allowed: ["*"], protected: ["main"] },
|
|
9213
|
+
custom: { allowed: [], protected: [] }
|
|
9329
9214
|
};
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9215
|
+
LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
|
|
9216
|
+
DEFAULTS = {
|
|
9217
|
+
locale: "en",
|
|
9218
|
+
localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
|
|
9219
|
+
branchPolicy: {
|
|
9220
|
+
preset: "gitflow",
|
|
9221
|
+
allowed: [...PRESETS.gitflow.allowed],
|
|
9222
|
+
protected: [...PRESETS.gitflow.protected]
|
|
9223
|
+
},
|
|
9224
|
+
commitPolicy: { preset: "conventional" }
|
|
9225
|
+
};
|
|
9226
|
+
unchosenBranchPolicy = new WeakSet([DEFAULTS]);
|
|
9227
|
+
COMMIT_PRESETS = [
|
|
9228
|
+
"conventional",
|
|
9229
|
+
"gitmoji",
|
|
9230
|
+
"ticket-prefix",
|
|
9231
|
+
"freeform",
|
|
9232
|
+
"custom",
|
|
9233
|
+
"auto"
|
|
9234
|
+
];
|
|
9334
9235
|
});
|
|
9335
9236
|
|
|
9336
|
-
// packages/workit-core/src/core/
|
|
9337
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
9237
|
+
// packages/workit-core/src/core/branch-policy.ts
|
|
9338
9238
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
9339
|
-
|
|
9340
|
-
const
|
|
9341
|
-
if (
|
|
9342
|
-
return
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
const
|
|
9348
|
-
const
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
continue;
|
|
9360
|
-
}
|
|
9361
|
-
let critical = [];
|
|
9362
|
-
if (Array.isArray(entry.critical))
|
|
9363
|
-
critical = entry.critical.filter((item) => typeof item === "string");
|
|
9364
|
-
else if (entry.critical !== undefined)
|
|
9365
|
-
out.issues.push(`${label} critical: must be a list of field names; ignored`);
|
|
9366
|
-
const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
|
|
9367
|
-
const problem = (field, message) => {
|
|
9368
|
-
if (isCritical(field))
|
|
9369
|
-
fatal.push(`${label} ${field}: ${message}`);
|
|
9370
|
-
else
|
|
9371
|
-
out.issues.push(`${label} ${field}: ${message}`);
|
|
9239
|
+
var PREFIXES, branchNames = (workspaceRoot) => {
|
|
9240
|
+
const r = spawnSync4("git", ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"], { cwd: workspaceRoot, encoding: "utf8" });
|
|
9241
|
+
if (r.status !== 0)
|
|
9242
|
+
return new Set;
|
|
9243
|
+
return new Set((r.stdout ?? "").split(`
|
|
9244
|
+
`).filter(Boolean).map((ref) => ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref.slice("refs/remotes/origin/".length)));
|
|
9245
|
+
}, productionBranchOf = (names) => names.has("main") ? "main" : names.has("master") ? "master" : "main", detectBranchPolicy = (workspaceRoot) => {
|
|
9246
|
+
const names = branchNames(workspaceRoot);
|
|
9247
|
+
const develop = names.has("develop");
|
|
9248
|
+
const main = names.has("main");
|
|
9249
|
+
const master = names.has("master");
|
|
9250
|
+
const root = main ? "main" : master ? "master" : null;
|
|
9251
|
+
if (develop) {
|
|
9252
|
+
return {
|
|
9253
|
+
preset: "gitflow",
|
|
9254
|
+
developBranch: "develop",
|
|
9255
|
+
integration: "merge",
|
|
9256
|
+
protected: root ? [root, "develop"] : ["develop"],
|
|
9257
|
+
allowed: [...PRESETS.gitflow.allowed],
|
|
9258
|
+
prefixes: PREFIXES
|
|
9372
9259
|
};
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
const optionalBranch = (field) => {
|
|
9383
|
-
if (entry[field] === undefined)
|
|
9384
|
-
return null;
|
|
9385
|
-
const value = branchName(entry[field]);
|
|
9386
|
-
if (!value)
|
|
9387
|
-
problem(field, "not a branch name; using the integration branch");
|
|
9388
|
-
return value;
|
|
9260
|
+
}
|
|
9261
|
+
if (root === "main") {
|
|
9262
|
+
return {
|
|
9263
|
+
preset: "github-flow",
|
|
9264
|
+
developBranch: null,
|
|
9265
|
+
integration: "pr",
|
|
9266
|
+
protected: [root],
|
|
9267
|
+
allowed: ["*"],
|
|
9268
|
+
prefixes: PREFIXES
|
|
9389
9269
|
};
|
|
9390
|
-
const baseBranch = optionalBranch("baseBranch");
|
|
9391
|
-
const pullRequestTarget = optionalBranch("pullRequestTarget");
|
|
9392
|
-
let mergeBackBranches = [];
|
|
9393
|
-
if (entry.mergeBackBranches !== undefined) {
|
|
9394
|
-
const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
|
|
9395
|
-
if (list.some((item) => item === null))
|
|
9396
|
-
problem("mergeBackBranches", "must be a list of branch names; ignored");
|
|
9397
|
-
else
|
|
9398
|
-
mergeBackBranches = [...new Set(list)];
|
|
9399
|
-
}
|
|
9400
|
-
const naming = {};
|
|
9401
|
-
if (entry.naming !== undefined) {
|
|
9402
|
-
if (!isRecord(entry.naming))
|
|
9403
|
-
problem("naming", "not an object; ignored");
|
|
9404
|
-
else
|
|
9405
|
-
for (const kind of ["feature", "release", "hotfix"]) {
|
|
9406
|
-
const template = entry.naming[kind];
|
|
9407
|
-
if (template === undefined)
|
|
9408
|
-
continue;
|
|
9409
|
-
if (typeof template !== "string" || !template.trim())
|
|
9410
|
-
problem(`naming.${kind}`, "not a template string; ignored");
|
|
9411
|
-
else if (!literalOf(template))
|
|
9412
|
-
problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
|
|
9413
|
-
else
|
|
9414
|
-
naming[kind] = template.trim();
|
|
9415
|
-
}
|
|
9416
|
-
}
|
|
9417
|
-
if (!productionBranch || !integrationBranch) {
|
|
9418
|
-
out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
|
|
9419
|
-
continue;
|
|
9420
|
-
}
|
|
9421
|
-
out.tracks.push({
|
|
9422
|
-
name,
|
|
9423
|
-
productionBranch,
|
|
9424
|
-
integrationBranch,
|
|
9425
|
-
baseBranch: baseBranch ?? integrationBranch,
|
|
9426
|
-
pullRequestTarget: pullRequestTarget ?? integrationBranch,
|
|
9427
|
-
mergeBackBranches,
|
|
9428
|
-
naming
|
|
9429
|
-
});
|
|
9430
9270
|
}
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
if (tracks.length === 0) {
|
|
9441
|
-
if (requested)
|
|
9442
|
-
return {
|
|
9443
|
-
status: "invalid",
|
|
9444
|
-
error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
|
|
9445
|
-
issues
|
|
9446
|
-
};
|
|
9447
|
-
return { status: "none", issues };
|
|
9271
|
+
if (root === "master") {
|
|
9272
|
+
return {
|
|
9273
|
+
preset: "trunk-based",
|
|
9274
|
+
developBranch: null,
|
|
9275
|
+
integration: "pr",
|
|
9276
|
+
protected: [root],
|
|
9277
|
+
allowed: ["*"],
|
|
9278
|
+
prefixes: PREFIXES
|
|
9279
|
+
};
|
|
9448
9280
|
}
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9281
|
+
return {
|
|
9282
|
+
preset: "gitflow",
|
|
9283
|
+
developBranch: null,
|
|
9284
|
+
integration: "merge",
|
|
9285
|
+
protected: [],
|
|
9286
|
+
allowed: [],
|
|
9287
|
+
prefixes: PREFIXES
|
|
9288
|
+
};
|
|
9289
|
+
}, repoBranchPreset = (workspaceRoot) => {
|
|
9290
|
+
const detected = detectBranchPolicy(workspaceRoot);
|
|
9291
|
+
return detected.developBranch || detected.allowed.length ? detected.preset : null;
|
|
9292
|
+
}, releaseTagPrefixOf = (workspaceRoot, policyPrefix) => {
|
|
9293
|
+
if (typeof policyPrefix === "string")
|
|
9294
|
+
return policyPrefix.trim();
|
|
9295
|
+
const r = spawnSync4("git", ["config", "--get", "gitflow.prefix.versiontag"], {
|
|
9296
|
+
cwd: workspaceRoot,
|
|
9297
|
+
encoding: "utf8"
|
|
9298
|
+
});
|
|
9299
|
+
return r.status === 0 ? (r.stdout ?? "").trim() : "v";
|
|
9300
|
+
};
|
|
9301
|
+
var init_branch_policy = __esm(() => {
|
|
9302
|
+
init_config();
|
|
9303
|
+
PREFIXES = {
|
|
9304
|
+
feature: "feature/*",
|
|
9305
|
+
bugfix: "bugfix/*",
|
|
9306
|
+
release: "release/*",
|
|
9307
|
+
hotfix: "hotfix/*"
|
|
9308
|
+
};
|
|
9309
|
+
});
|
|
9310
|
+
|
|
9311
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
9312
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9313
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9314
|
+
import { readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
9315
|
+
import path4 from "node:path";
|
|
9316
|
+
function workspaceDefaultTarget(cwd, globalDefault) {
|
|
9317
|
+
const ws = resolveRuntimeWorkspaceVcs(cwd);
|
|
9318
|
+
const wp = ws?.branchPolicy ?? {};
|
|
9319
|
+
const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
|
|
9320
|
+
const selected = resolveRuntimeWorkspacePolicy(cwd, "branch", process.env.WORKFLOW_PROFILE?.trim() || undefined);
|
|
9321
|
+
const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
|
|
9322
|
+
return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
|
|
9323
|
+
}
|
|
9324
|
+
var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash4("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
|
|
9325
|
+
if (!isConfigObject(parsed)) {
|
|
9326
|
+
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
9327
|
+
}
|
|
9328
|
+
const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
|
|
9329
|
+
if (!validated.success) {
|
|
9330
|
+
const detail = validated.error.issues.map((issue) => {
|
|
9331
|
+
const field = String(issue.path[issue.path.length - 1] ?? "");
|
|
9332
|
+
const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
|
|
9333
|
+
return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
|
|
9334
|
+
}).join("; ");
|
|
9335
|
+
return {
|
|
9336
|
+
status: "invalid",
|
|
9337
|
+
path: file,
|
|
9338
|
+
entries: [],
|
|
9339
|
+
error: `${file} has invalid workspace configuration: ${detail}`
|
|
9340
|
+
};
|
|
9341
|
+
}
|
|
9342
|
+
for (const [index, workspace] of (validated.data.workspaces ?? []).entries()) {
|
|
9343
|
+
const glob = validateWorkspaceGlob(workspace.glob);
|
|
9344
|
+
if (!glob.ok) {
|
|
9463
9345
|
return {
|
|
9464
9346
|
status: "invalid",
|
|
9465
|
-
|
|
9466
|
-
|
|
9347
|
+
path: file,
|
|
9348
|
+
entries: [],
|
|
9349
|
+
error: `${file} workspaces.${index}.glob: ${glob.error}`
|
|
9467
9350
|
};
|
|
9468
|
-
|
|
9351
|
+
}
|
|
9469
9352
|
}
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
].includes(branch));
|
|
9476
|
-
const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
|
|
9477
|
-
name: IMPLICIT,
|
|
9478
|
-
productionBranch: input.defaultBranch,
|
|
9479
|
-
integrationBranch: input.defaultBranch,
|
|
9480
|
-
baseBranch: input.defaultBranch,
|
|
9481
|
-
pullRequestTarget: input.defaultBranch,
|
|
9482
|
-
mergeBackBranches: [],
|
|
9483
|
-
naming: {}
|
|
9484
|
-
} : null;
|
|
9485
|
-
const candidates = implicit ? [...tracks, implicit] : tracks;
|
|
9486
|
-
const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
|
|
9487
|
-
const fallback = (why) => {
|
|
9488
|
-
const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
|
|
9489
|
-
const track = byDefault.length === 1 ? byDefault[0] : null;
|
|
9490
|
-
const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
|
|
9491
|
-
const ambiguous = candidates.length >= 2 ? `can't tell which release line ${input.branch ?? "HEAD"} is on (${why}); pass --track <name> (${names}) or --base <branch>` : null;
|
|
9492
|
-
return done(track, "default", `${why}; using the workspace ${target}`, [
|
|
9493
|
-
`release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
|
|
9494
|
-
], ambiguous);
|
|
9353
|
+
return {
|
|
9354
|
+
status: "valid",
|
|
9355
|
+
path: file,
|
|
9356
|
+
entries: validated.data.workspaces ?? [],
|
|
9357
|
+
document: parsed
|
|
9495
9358
|
};
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9359
|
+
}, readWorkspacesResult = (dir = configDir()) => {
|
|
9360
|
+
const file = path4.join(dir, "workspaces.json");
|
|
9361
|
+
let raw;
|
|
9362
|
+
try {
|
|
9363
|
+
raw = readFileSync2(file, "utf8");
|
|
9364
|
+
} catch (error) {
|
|
9365
|
+
if (error.code === "ENOENT")
|
|
9366
|
+
return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
|
|
9367
|
+
return {
|
|
9368
|
+
status: "malformed",
|
|
9369
|
+
path: file,
|
|
9370
|
+
entries: [],
|
|
9371
|
+
error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
9372
|
+
};
|
|
9503
9373
|
}
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9374
|
+
let parsed;
|
|
9375
|
+
try {
|
|
9376
|
+
parsed = JSON.parse(raw);
|
|
9377
|
+
} catch {
|
|
9378
|
+
return {
|
|
9379
|
+
status: "malformed",
|
|
9380
|
+
path: file,
|
|
9381
|
+
entries: [],
|
|
9382
|
+
revision: workspacesRevision(raw),
|
|
9383
|
+
error: `${file} is not valid JSON`
|
|
9384
|
+
};
|
|
9510
9385
|
}
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9386
|
+
return { ...validateWorkspacesDocument(parsed, file), revision: workspacesRevision(raw) };
|
|
9387
|
+
}, globToRegExp = (glob) => {
|
|
9388
|
+
let out = "";
|
|
9389
|
+
for (let i = 0;i < glob.length; i++) {
|
|
9390
|
+
const c = glob[i];
|
|
9391
|
+
if (c === "*") {
|
|
9392
|
+
if (glob[i + 1] === "*") {
|
|
9393
|
+
if (glob[i + 2] === "/") {
|
|
9394
|
+
if (out === "")
|
|
9395
|
+
out += "/?";
|
|
9396
|
+
out += "(?:[^/]+/)*";
|
|
9397
|
+
i += 2;
|
|
9398
|
+
} else {
|
|
9399
|
+
if (i + 2 >= glob.length) {
|
|
9400
|
+
if (out.endsWith("/")) {
|
|
9401
|
+
out = out.slice(0, -1);
|
|
9402
|
+
out += "(?:/.*)?";
|
|
9403
|
+
} else {
|
|
9404
|
+
out += ".*";
|
|
9405
|
+
}
|
|
9406
|
+
} else {
|
|
9407
|
+
out += ".*";
|
|
9408
|
+
}
|
|
9409
|
+
i++;
|
|
9410
|
+
}
|
|
9411
|
+
} else {
|
|
9412
|
+
out += "[^/]*";
|
|
9413
|
+
}
|
|
9414
|
+
} else {
|
|
9415
|
+
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
9536
9416
|
}
|
|
9537
|
-
return null;
|
|
9538
|
-
};
|
|
9539
|
-
const recordedOwner = followRecorded(branch);
|
|
9540
|
-
if (recordedOwner)
|
|
9541
|
-
return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
|
|
9542
|
-
for (const source of ["reflog", "checkout"]) {
|
|
9543
|
-
const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
|
|
9544
|
-
if (!ref)
|
|
9545
|
-
continue;
|
|
9546
|
-
const owner = ownerOf(ref) ?? followRecorded(short(ref));
|
|
9547
|
-
if (owner)
|
|
9548
|
-
return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
|
|
9549
9417
|
}
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
if (older === newer)
|
|
9562
|
-
return false;
|
|
9563
|
-
const key = `${older}>${newer}`;
|
|
9564
|
-
if (!ancestry.has(key))
|
|
9565
|
-
ancestry.set(key, input.probe.isAncestor(older, newer));
|
|
9566
|
-
return ancestry.get(key);
|
|
9567
|
-
};
|
|
9568
|
-
const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
|
|
9569
|
-
const winningTracks = [...new Set(winners.map((fork) => fork.track))];
|
|
9570
|
-
if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
|
|
9571
|
-
const win = winners[0];
|
|
9572
|
-
return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
|
|
9418
|
+
return new RegExp(`^${out}$`);
|
|
9419
|
+
}, matchWorkspace = (glob, target) => globToRegExp(glob.replaceAll("\\", "/")).test(target.replaceAll("\\", "/")), UNSUPPORTED_GLOB, UNSUPPORTED_EXTGLOB, validateWorkspaceGlob = (glob) => {
|
|
9420
|
+
const trimmed = glob.trim();
|
|
9421
|
+
if (!trimmed)
|
|
9422
|
+
return { ok: false, error: "workspace pattern is required" };
|
|
9423
|
+
const m = UNSUPPORTED_GLOB.exec(trimmed);
|
|
9424
|
+
if (m) {
|
|
9425
|
+
return {
|
|
9426
|
+
ok: false,
|
|
9427
|
+
error: `unsupported glob character ${JSON.stringify(m[0])} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
|
|
9428
|
+
};
|
|
9573
9429
|
}
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
const tip = (branch) => {
|
|
9583
|
-
remote ??= pushRemoteName(cwd) ?? "origin";
|
|
9584
|
-
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
|
|
9585
|
-
};
|
|
9586
|
-
return {
|
|
9587
|
-
remotes: () => remotes ??= remoteNames(cwd),
|
|
9588
|
-
recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
|
|
9589
|
-
createdFrom: (branch) => {
|
|
9590
|
-
const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
|
|
9591
|
-
const oldest = log?.split(`
|
|
9592
|
-
`).findLast(Boolean) ?? "";
|
|
9593
|
-
const match = /^branch: Created from (\S+)$/u.exec(oldest);
|
|
9594
|
-
return match && match[1] !== "HEAD" ? match[1] : null;
|
|
9595
|
-
},
|
|
9596
|
-
checkedOutFrom: (branch) => {
|
|
9597
|
-
const own = reflog(cwd, `refs/heads/${branch}`);
|
|
9598
|
-
const created = own.at(-1);
|
|
9599
|
-
if (!created || created.subject !== "branch: Created from HEAD")
|
|
9600
|
-
return null;
|
|
9601
|
-
const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
|
|
9602
|
-
const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
|
|
9603
|
-
return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
|
|
9604
|
-
},
|
|
9605
|
-
mergeBase: (branch, trackBranch) => {
|
|
9606
|
-
const base = tip(trackBranch);
|
|
9607
|
-
return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
|
|
9608
|
-
},
|
|
9609
|
-
isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
|
|
9610
|
-
};
|
|
9611
|
-
}
|
|
9612
|
-
function workspaceReleaseTracks(cwd) {
|
|
9613
|
-
const raw = resolveRuntimeReleaseTracks(cwd);
|
|
9614
|
-
return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
|
|
9615
|
-
}
|
|
9616
|
-
function resolveTrackFor(cwd, options) {
|
|
9617
|
-
const read = workspaceReleaseTracks(cwd);
|
|
9618
|
-
if (read.error)
|
|
9619
|
-
return { status: "invalid", error: read.error, issues: read.issues };
|
|
9620
|
-
if (read.tracks.length === 0 && !options.track?.trim())
|
|
9621
|
-
return { status: "none", issues: read.issues };
|
|
9622
|
-
const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
|
|
9623
|
-
const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
|
|
9624
|
-
const input = {
|
|
9625
|
-
tracks: read.tracks,
|
|
9626
|
-
requested: options.track ?? null,
|
|
9627
|
-
envRequested,
|
|
9628
|
-
branch,
|
|
9629
|
-
defaultBranch: options.defaultBranch,
|
|
9630
|
-
issues: read.issues
|
|
9631
|
-
};
|
|
9632
|
-
let key = null;
|
|
9633
|
-
if (!options.probe) {
|
|
9634
|
-
const refs = git2(cwd, [
|
|
9635
|
-
"for-each-ref",
|
|
9636
|
-
"--format=%(refname) %(objectname)",
|
|
9637
|
-
"refs/heads",
|
|
9638
|
-
"refs/remotes"
|
|
9639
|
-
]) ?? "";
|
|
9640
|
-
const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
|
|
9641
|
-
const own = branch ? [
|
|
9642
|
-
git2(cwd, [
|
|
9643
|
-
"log",
|
|
9644
|
-
"-g",
|
|
9645
|
-
"-n",
|
|
9646
|
-
"1",
|
|
9647
|
-
"--date=unix",
|
|
9648
|
-
"--format=%H %gd %gs",
|
|
9649
|
-
`refs/heads/${branch}`,
|
|
9650
|
-
"--"
|
|
9651
|
-
]) ?? "",
|
|
9652
|
-
git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
|
|
9653
|
-
] : [];
|
|
9654
|
-
key = createHash4("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
|
|
9655
|
-
const hit = memo2.get(key);
|
|
9656
|
-
if (hit)
|
|
9657
|
-
return hit;
|
|
9658
|
-
}
|
|
9659
|
-
const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
|
|
9660
|
-
if (key) {
|
|
9661
|
-
if (memo2.size >= MEMO_LIMIT)
|
|
9662
|
-
memo2.delete(memo2.keys().next().value);
|
|
9663
|
-
memo2.set(key, resolution);
|
|
9430
|
+
const ext = UNSUPPORTED_EXTGLOB.exec(trimmed);
|
|
9431
|
+
if (ext) {
|
|
9432
|
+
const token = ext[0];
|
|
9433
|
+
const kind = token === "!" ? "negation (!)" : `extglob (${token})`;
|
|
9434
|
+
return {
|
|
9435
|
+
ok: false,
|
|
9436
|
+
error: `unsupported glob ${kind} in workspace pattern ${JSON.stringify(glob)} — the matcher supports * and ** only (e.g. /work/**)`
|
|
9437
|
+
};
|
|
9664
9438
|
}
|
|
9665
|
-
return
|
|
9666
|
-
}
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
}, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
|
|
9683
|
-
const run = spawnSync4("git", args, {
|
|
9684
|
-
cwd,
|
|
9685
|
-
encoding: "utf8",
|
|
9686
|
-
timeout: 20000,
|
|
9687
|
-
windowsHide: true,
|
|
9688
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
9689
|
-
});
|
|
9690
|
-
return run.status === 0 ? (run.stdout ?? "").trim() : null;
|
|
9691
|
-
}, recordedBaseKey = (branch) => `branch.${branch}.workitBase`, escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), reflog = (cwd, ref) => (git2(cwd, ["log", "-g", "--date=unix", "--format=%H%x09%gd%x09%gs", ref, "--"]) ?? "").split(`
|
|
9692
|
-
`).map((line) => {
|
|
9693
|
-
const [sha = "", selector = "", ...subject] = line.split("\t");
|
|
9694
|
-
return {
|
|
9695
|
-
sha,
|
|
9696
|
-
time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
|
|
9697
|
-
subject: subject.join("\t")
|
|
9698
|
-
};
|
|
9699
|
-
}).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
|
|
9700
|
-
var init_release_tracks = __esm(() => {
|
|
9701
|
-
init_rev();
|
|
9702
|
-
init_config();
|
|
9703
|
-
init_workspaces();
|
|
9704
|
-
ReleaseTrackError = class ReleaseTrackError extends Error {
|
|
9439
|
+
return { ok: true };
|
|
9440
|
+
}, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
|
|
9441
|
+
const cached = mainCheckouts.get(cwd);
|
|
9442
|
+
if (cached !== undefined)
|
|
9443
|
+
return cached;
|
|
9444
|
+
const env = { ...process.env };
|
|
9445
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
|
|
9446
|
+
delete env[name];
|
|
9447
|
+
const git = (args) => {
|
|
9448
|
+
const run = spawnSync5("git", args, {
|
|
9449
|
+
cwd,
|
|
9450
|
+
env,
|
|
9451
|
+
encoding: "utf8",
|
|
9452
|
+
timeout: 5000,
|
|
9453
|
+
windowsHide: true
|
|
9454
|
+
});
|
|
9455
|
+
return run.status === 0 ? run.stdout : null;
|
|
9705
9456
|
};
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
"
|
|
9711
|
-
"
|
|
9712
|
-
"mergeBackBranches",
|
|
9713
|
-
"pullRequestTarget",
|
|
9714
|
-
"tagNamespace",
|
|
9715
|
-
"versionSource",
|
|
9716
|
-
"requiredChecks",
|
|
9717
|
-
"critical"
|
|
9718
|
-
]);
|
|
9719
|
-
BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
|
|
9720
|
-
PLACEHOLDER = /\{[^}]*\}/gu;
|
|
9721
|
-
HAS_PLACEHOLDER = /\{[^}]*\}/u;
|
|
9722
|
-
memo2 = new Map;
|
|
9723
|
-
});
|
|
9724
|
-
|
|
9725
|
-
// packages/workit-core/src/core/vcs-config.ts
|
|
9726
|
-
import fs2 from "node:fs";
|
|
9727
|
-
import path4 from "node:path";
|
|
9728
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9729
|
-
function vcsConfig(mode, cwd, options = {}) {
|
|
9730
|
-
const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
|
|
9731
|
-
const wsVcs = ws?.vcs ?? {};
|
|
9732
|
-
const wsYt = ws?.youtrack ?? {};
|
|
9733
|
-
const wsIssues = ws?.issues ?? {};
|
|
9734
|
-
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
9735
|
-
const root = vcsCwd(cwd);
|
|
9736
|
-
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
9737
|
-
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
9738
|
-
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
9739
|
-
const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
|
|
9740
|
-
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
9741
|
-
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
9742
|
-
let issuesProvider = null;
|
|
9743
|
-
let linkOnPr = null;
|
|
9744
|
-
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
9745
|
-
issuesProvider = "github";
|
|
9746
|
-
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
9457
|
+
let main = null;
|
|
9458
|
+
const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
|
|
9459
|
+
const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
|
|
9460
|
+
if (gitDir && common && path4.resolve(gitDir) !== path4.resolve(common)) {
|
|
9461
|
+
const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
|
|
9462
|
+
main = listed?.trim() || (path4.basename(common) === ".git" ? path4.dirname(common) : common);
|
|
9747
9463
|
}
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9464
|
+
mainCheckouts.set(cwd, main);
|
|
9465
|
+
return main;
|
|
9466
|
+
}, matchingEntries = (cwd, entries) => {
|
|
9467
|
+
const match = (where) => {
|
|
9468
|
+
const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
|
|
9469
|
+
return entries.filter((entry) => {
|
|
9470
|
+
const glob = entry.glob.replaceAll("\\", "/");
|
|
9471
|
+
const canonical = canonicalGlob(glob);
|
|
9472
|
+
return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
|
|
9755
9473
|
});
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
releaseTrack,
|
|
9769
|
-
link_issues: linkIssues,
|
|
9770
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
9771
|
-
issues_provider: issuesProvider,
|
|
9772
|
-
link_on_pr: linkOnPr
|
|
9773
|
-
};
|
|
9774
|
-
}
|
|
9775
|
-
if (cfgStatus === "malformed") {
|
|
9776
|
-
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
9474
|
+
};
|
|
9475
|
+
const direct = match(cwd);
|
|
9476
|
+
if (direct.length)
|
|
9477
|
+
return direct;
|
|
9478
|
+
const main = mainCheckoutOf(cwd);
|
|
9479
|
+
return main ? match(main) : direct;
|
|
9480
|
+
}, realpathOf = (p) => {
|
|
9481
|
+
const real = realpathSync.native ?? realpathSync;
|
|
9482
|
+
try {
|
|
9483
|
+
return real(p);
|
|
9484
|
+
} catch {
|
|
9485
|
+
return p;
|
|
9777
9486
|
}
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9487
|
+
}, canonicalGlob = (glob) => {
|
|
9488
|
+
const m = /[*?[\]{]/.exec(glob);
|
|
9489
|
+
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
9490
|
+
const rest = m ? glob.slice(m.index) : "";
|
|
9491
|
+
if (!prefix)
|
|
9492
|
+
return glob;
|
|
9493
|
+
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
9494
|
+
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
9495
|
+
}, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
|
|
9496
|
+
if (workspaceName !== undefined) {
|
|
9497
|
+
const named = matches.filter((entry) => entry.name === workspaceName);
|
|
9498
|
+
if (named.length === 1)
|
|
9499
|
+
return named[0];
|
|
9500
|
+
if (named.length > 1)
|
|
9501
|
+
throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
|
|
9502
|
+
if (matches.length > 0) {
|
|
9503
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
|
|
9504
|
+
}
|
|
9505
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
9786
9506
|
}
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
pr: cfg.pr ?? {},
|
|
9794
|
-
workspace_name: ws?.name ?? null,
|
|
9795
|
-
link_issues: linkIssues,
|
|
9796
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
9797
|
-
issues_provider: issuesProvider,
|
|
9798
|
-
link_on_pr: linkOnPr
|
|
9799
|
-
};
|
|
9800
|
-
if (provider === "gitlab") {
|
|
9801
|
-
out.gitlab = {
|
|
9802
|
-
host: prov.host ?? "gitlab.com",
|
|
9803
|
-
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
9804
|
-
};
|
|
9805
|
-
} else if (provider === "github") {
|
|
9806
|
-
out.github = { host: prov.host ?? "github.com" };
|
|
9507
|
+
if (matches.length < 2)
|
|
9508
|
+
return matches[0];
|
|
9509
|
+
const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
|
|
9510
|
+
const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
|
|
9511
|
+
if (mostSpecific.length > 1) {
|
|
9512
|
+
throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
|
|
9807
9513
|
}
|
|
9808
|
-
return
|
|
9809
|
-
}
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9514
|
+
return mostSpecific[0];
|
|
9515
|
+
}, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
|
|
9516
|
+
const matches = matchingEntries(cwd, entries);
|
|
9517
|
+
return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
|
|
9518
|
+
}, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
|
|
9519
|
+
const result = readWorkspacesResult(dir);
|
|
9520
|
+
if (result.status === "malformed" || result.status === "invalid")
|
|
9521
|
+
throw new WorkspaceConfigError(result.error);
|
|
9522
|
+
return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
|
|
9523
|
+
}, runtimeWorkspacePolicySchema = (kind) => {
|
|
9524
|
+
const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
|
|
9525
|
+
const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
|
|
9526
|
+
provider: _enum(["gitlab", "github"]),
|
|
9527
|
+
defaultTargetBranch: nonBlank.optional(),
|
|
9528
|
+
account: nonBlank.optional()
|
|
9529
|
+
}).passthrough();
|
|
9530
|
+
const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
|
|
9531
|
+
const entrySchema = object({
|
|
9532
|
+
name: nonBlank,
|
|
9533
|
+
glob: nonBlank,
|
|
9534
|
+
[key]: policySchema.optional(),
|
|
9535
|
+
defaultProfile: nonBlank.optional(),
|
|
9536
|
+
profiles: record(nonBlank, profileSchema).optional()
|
|
9537
|
+
}).passthrough();
|
|
9538
|
+
return entrySchema;
|
|
9539
|
+
}, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
|
|
9540
|
+
const file = path4.join(dir, "workspaces.json");
|
|
9822
9541
|
let raw;
|
|
9823
9542
|
try {
|
|
9824
|
-
raw =
|
|
9825
|
-
} catch {
|
|
9826
|
-
|
|
9543
|
+
raw = readFileSync2(file, "utf8");
|
|
9544
|
+
} catch (error) {
|
|
9545
|
+
if (error.code === "ENOENT")
|
|
9546
|
+
return null;
|
|
9547
|
+
throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
9827
9548
|
}
|
|
9828
9549
|
let parsed;
|
|
9829
9550
|
try {
|
|
9830
9551
|
parsed = JSON.parse(raw);
|
|
9831
9552
|
} catch {
|
|
9832
|
-
|
|
9553
|
+
throw new WorkspaceConfigError(`${file} is not valid JSON`);
|
|
9833
9554
|
}
|
|
9834
|
-
|
|
9835
|
-
|
|
9555
|
+
const validated = runtimeWorkspaceIndexSchema.safeParse(parsed);
|
|
9556
|
+
if (!validated.success) {
|
|
9557
|
+
const detail = validated.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}`).join("; ");
|
|
9558
|
+
throw new WorkspaceConfigError(`${file} has invalid workspace matching data: ${detail}`);
|
|
9836
9559
|
}
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
warnings: resolution.warnings,
|
|
9843
|
-
track: resolution.track,
|
|
9844
|
-
tracks: resolution.tracks,
|
|
9845
|
-
blocking: resolution.blocking
|
|
9846
|
-
} : null;
|
|
9847
|
-
var init_vcs_config = __esm(() => {
|
|
9848
|
-
init_config();
|
|
9849
|
-
init_workspaces();
|
|
9850
|
-
init_release_tracks();
|
|
9851
|
-
});
|
|
9852
|
-
|
|
9853
|
-
// packages/workit-core/src/core/logger.ts
|
|
9854
|
-
import os3 from "node:os";
|
|
9855
|
-
import path5 from "node:path";
|
|
9856
|
-
var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
|
|
9857
|
-
const override = process.env.WORKFLOW_TOOLKIT_STATE;
|
|
9858
|
-
if (override)
|
|
9859
|
-
return override;
|
|
9860
|
-
const home = os3.homedir();
|
|
9861
|
-
if (process.env.XDG_STATE_HOME)
|
|
9862
|
-
return path5.join(process.env.XDG_STATE_HOME, "workit");
|
|
9863
|
-
switch (os3.platform()) {
|
|
9864
|
-
case "darwin":
|
|
9865
|
-
return path5.join(home, "Library", "Application Support", "workit");
|
|
9866
|
-
case "win32":
|
|
9867
|
-
return path5.join(process.env.LOCALAPPDATA ?? path5.join(home, "AppData", "Local"), "workit");
|
|
9868
|
-
default:
|
|
9869
|
-
return path5.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
9560
|
+
const entries = validated.data.workspaces;
|
|
9561
|
+
for (const entry of entries ?? []) {
|
|
9562
|
+
const glob = validateWorkspaceGlob(entry.glob);
|
|
9563
|
+
if (!glob.ok)
|
|
9564
|
+
throw new WorkspaceConfigError(`${file} workspace ${entry.name}: ${glob.error}`);
|
|
9870
9565
|
}
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
"
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9566
|
+
const matches = matchingEntries(cwd, entries ?? []);
|
|
9567
|
+
const workspaceName = process.env.WORKFLOW_WORKSPACE_NAME?.trim() || undefined;
|
|
9568
|
+
const selected = selectWorkspaceMatch(matches, cwd, workspaceName);
|
|
9569
|
+
return selected ? { file, selected, matches } : null;
|
|
9570
|
+
}, resolveRuntimeReleaseTracks = (cwd) => {
|
|
9571
|
+
const matched = matchRuntimeWorkspace(cwd);
|
|
9572
|
+
return matched ? { workspace: matched.selected.name, releaseTracks: matched.selected.releaseTracks } : null;
|
|
9573
|
+
}, workspaceTrailers = (cwd, dir) => {
|
|
9574
|
+
let matched;
|
|
9575
|
+
try {
|
|
9576
|
+
matched = matchRuntimeWorkspace(cwd, dir);
|
|
9577
|
+
} catch {
|
|
9578
|
+
return { enabled: true, workspace: null, from: null };
|
|
9579
|
+
}
|
|
9580
|
+
if (!matched)
|
|
9581
|
+
return { enabled: true, workspace: null, from: null };
|
|
9582
|
+
const { selected, matches } = matched;
|
|
9583
|
+
const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
|
|
9584
|
+
const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
|
|
9585
|
+
return {
|
|
9586
|
+
enabled: source?.trailers !== false,
|
|
9587
|
+
workspace: selected.name,
|
|
9588
|
+
from: source?.name ?? null
|
|
9589
|
+
};
|
|
9590
|
+
}, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
|
|
9591
|
+
const matched = matchRuntimeWorkspace(cwd);
|
|
9592
|
+
if (!matched)
|
|
9593
|
+
return null;
|
|
9594
|
+
const { file, selected } = matched;
|
|
9595
|
+
const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
|
|
9596
|
+
if (!policy.success) {
|
|
9597
|
+
const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
|
|
9598
|
+
throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
|
|
9599
|
+
}
|
|
9600
|
+
return policy.data;
|
|
9601
|
+
}, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
|
|
9602
|
+
const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
|
|
9603
|
+
if (!candidate)
|
|
9604
|
+
return { policy: undefined, source: "user-default" };
|
|
9605
|
+
const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
|
|
9606
|
+
const profileName = requestedProfile?.trim() || candidate.defaultProfile;
|
|
9607
|
+
if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
|
|
9608
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
|
|
9609
|
+
const profile = profileName ? candidate.profiles?.[profileName] : undefined;
|
|
9610
|
+
if (profileName && !profile)
|
|
9611
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
|
|
9612
|
+
const profilePolicy = profile?.[key];
|
|
9613
|
+
const workspacePolicy = candidate[key];
|
|
9614
|
+
return {
|
|
9615
|
+
policy: profilePolicy ?? workspacePolicy,
|
|
9616
|
+
source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
|
|
9617
|
+
};
|
|
9618
|
+
};
|
|
9619
|
+
var init_workspaces = __esm(() => {
|
|
9620
|
+
init_zod();
|
|
9621
|
+
init_config();
|
|
9622
|
+
init_branch_policy();
|
|
9623
|
+
nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
|
|
9624
|
+
branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
|
|
9625
|
+
commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
|
|
9626
|
+
workspaceBranchPolicySchema = object({
|
|
9627
|
+
preset: branchPreset,
|
|
9628
|
+
developBranch: nonBlank.optional(),
|
|
9629
|
+
prefixes: object({
|
|
9630
|
+
feature: nonBlank,
|
|
9631
|
+
bugfix: nonBlank,
|
|
9632
|
+
release: nonBlank,
|
|
9633
|
+
hotfix: nonBlank
|
|
9634
|
+
}).passthrough().optional(),
|
|
9635
|
+
allowed: array(nonBlank).optional(),
|
|
9636
|
+
protected: array(nonBlank).optional(),
|
|
9637
|
+
integration: _enum(["pr", "merge"]).optional()
|
|
9638
|
+
}).strict();
|
|
9639
|
+
workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
|
|
9640
|
+
workspaceProfileSchema = object({
|
|
9641
|
+
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9642
|
+
commitPolicy: workspaceCommitPolicySchema.optional()
|
|
9643
|
+
}).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
|
|
9644
|
+
message: "a profile must define branchPolicy or commitPolicy"
|
|
9645
|
+
});
|
|
9646
|
+
versionSourceSchema = discriminatedUnion("kind", [
|
|
9647
|
+
object({
|
|
9648
|
+
kind: literal("package-json"),
|
|
9649
|
+
path: nonBlank,
|
|
9650
|
+
field: nonBlank
|
|
9651
|
+
}).strict(),
|
|
9652
|
+
object({ kind: literal("git-tag") }).strict(),
|
|
9653
|
+
object({ kind: literal("manual") }).strict()
|
|
9908
9654
|
]);
|
|
9909
|
-
|
|
9910
|
-
|
|
9655
|
+
releaseTrackSchema = object({
|
|
9656
|
+
strategy: branchPreset.optional(),
|
|
9657
|
+
productionBranch: nonBlank,
|
|
9658
|
+
integrationBranch: nonBlank,
|
|
9659
|
+
naming: object({
|
|
9660
|
+
feature: nonBlank.optional(),
|
|
9661
|
+
release: nonBlank.optional(),
|
|
9662
|
+
hotfix: nonBlank.optional()
|
|
9663
|
+
}).passthrough().optional(),
|
|
9664
|
+
baseBranch: nonBlank.optional(),
|
|
9665
|
+
mergeBackBranches: array(nonBlank).optional(),
|
|
9666
|
+
pullRequestTarget: nonBlank.optional(),
|
|
9667
|
+
tagNamespace: string2().optional(),
|
|
9668
|
+
versionSource: versionSourceSchema.optional(),
|
|
9669
|
+
requiredChecks: array(nonBlank).optional(),
|
|
9670
|
+
critical: array(nonBlank).optional()
|
|
9671
|
+
}).passthrough().superRefine((track, context) => {
|
|
9672
|
+
for (const [field, values] of [
|
|
9673
|
+
["mergeBackBranches", track.mergeBackBranches ?? []],
|
|
9674
|
+
["requiredChecks", track.requiredChecks ?? []]
|
|
9675
|
+
]) {
|
|
9676
|
+
if (new Set(values).size !== values.length) {
|
|
9677
|
+
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
if (track.versionSource?.kind === "package-json" && (path4.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
|
|
9681
|
+
context.addIssue({
|
|
9682
|
+
code: "custom",
|
|
9683
|
+
path: ["versionSource", "path"],
|
|
9684
|
+
message: "must stay inside the repository"
|
|
9685
|
+
});
|
|
9686
|
+
}
|
|
9687
|
+
});
|
|
9688
|
+
workspaceConfigSchema = object({
|
|
9689
|
+
name: nonBlank,
|
|
9690
|
+
glob: nonBlank,
|
|
9691
|
+
vcs: object({
|
|
9692
|
+
provider: _enum(["gitlab", "github"]),
|
|
9693
|
+
defaultTargetBranch: nonBlank.optional(),
|
|
9694
|
+
tokenFile: nonBlank.optional(),
|
|
9695
|
+
account: nonBlank.optional()
|
|
9696
|
+
}).passthrough().optional(),
|
|
9697
|
+
autoApprove: union([boolean2(), array(nonBlank)]).optional(),
|
|
9698
|
+
defaultEndpoint: unknown().optional(),
|
|
9699
|
+
verification: _enum(["self", "independent"]).optional(),
|
|
9700
|
+
trailers: boolean2().optional(),
|
|
9701
|
+
autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
|
|
9702
|
+
kind,
|
|
9703
|
+
union([boolean2(), literal("verified")]).optional()
|
|
9704
|
+
]))).passthrough().optional(),
|
|
9705
|
+
youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
|
|
9706
|
+
issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
|
|
9707
|
+
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9708
|
+
commitPolicy: workspaceCommitPolicySchema.optional(),
|
|
9709
|
+
defaultProfile: nonBlank.optional(),
|
|
9710
|
+
profiles: record(nonBlank, workspaceProfileSchema).optional(),
|
|
9711
|
+
releaseTracks: record(nonBlank, releaseTrackSchema).optional()
|
|
9712
|
+
}).passthrough().superRefine((workspace, context) => {
|
|
9713
|
+
if (workspace.issues && workspace.vcs?.provider !== "github") {
|
|
9714
|
+
context.addIssue({
|
|
9715
|
+
code: "custom",
|
|
9716
|
+
path: ["issues"],
|
|
9717
|
+
message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
|
|
9718
|
+
});
|
|
9719
|
+
}
|
|
9720
|
+
if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
|
|
9721
|
+
context.addIssue({
|
|
9722
|
+
code: "custom",
|
|
9723
|
+
path: ["defaultProfile"],
|
|
9724
|
+
message: `does not name a configured profile`
|
|
9725
|
+
});
|
|
9726
|
+
}
|
|
9727
|
+
});
|
|
9728
|
+
workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
|
|
9729
|
+
const names = new Set;
|
|
9730
|
+
for (const [index, workspace] of (file.workspaces ?? []).entries()) {
|
|
9731
|
+
if (names.has(workspace.name)) {
|
|
9732
|
+
context.addIssue({
|
|
9733
|
+
code: "custom",
|
|
9734
|
+
path: ["workspaces", index, "name"],
|
|
9735
|
+
message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
|
|
9736
|
+
});
|
|
9737
|
+
}
|
|
9738
|
+
names.add(workspace.name);
|
|
9739
|
+
}
|
|
9740
|
+
});
|
|
9741
|
+
UNSUPPORTED_GLOB = /[?[\]{}]/;
|
|
9742
|
+
UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
|
|
9743
|
+
WorkspaceConfigError = class WorkspaceConfigError extends Error {
|
|
9744
|
+
name = "WorkspaceConfigError";
|
|
9745
|
+
};
|
|
9746
|
+
mainCheckouts = new Map;
|
|
9747
|
+
runtimeWorkspaceIndexSchema = object({
|
|
9748
|
+
workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
|
|
9749
|
+
}).passthrough();
|
|
9911
9750
|
});
|
|
9912
9751
|
|
|
9913
|
-
// packages/workit-core/src/
|
|
9914
|
-
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9752
|
+
// packages/workit-core/src/core/release-tracks.ts
|
|
9915
9753
|
import { createHash as createHash5 } from "node:crypto";
|
|
9916
|
-
import
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
if (
|
|
9920
|
-
return
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9754
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9755
|
+
function readReleaseTracks(raw) {
|
|
9756
|
+
const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
|
|
9757
|
+
if (raw === undefined || raw === null)
|
|
9758
|
+
return out;
|
|
9759
|
+
if (!isRecord(raw)) {
|
|
9760
|
+
out.issues.push("releaseTracks is not an object; ignored");
|
|
9761
|
+
return out;
|
|
9762
|
+
}
|
|
9763
|
+
const fatal = [];
|
|
9764
|
+
const protectedNames = new Set;
|
|
9765
|
+
for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
|
|
9766
|
+
const label = `release track ${JSON.stringify(name)}`;
|
|
9767
|
+
if (!isRecord(entry)) {
|
|
9768
|
+
out.issues.push(`${label} is not an object; ignored`);
|
|
9769
|
+
continue;
|
|
9929
9770
|
}
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
try {
|
|
9936
|
-
entry = fs3.statSync(dotGit);
|
|
9937
|
-
} catch {}
|
|
9938
|
-
let gitDir = null;
|
|
9939
|
-
let top = null;
|
|
9940
|
-
if (entry) {
|
|
9941
|
-
top = dir;
|
|
9942
|
-
if (entry.isDirectory())
|
|
9943
|
-
gitDir = dotGit;
|
|
9944
|
-
else if (entry.isFile()) {
|
|
9945
|
-
let text = "";
|
|
9946
|
-
try {
|
|
9947
|
-
text = fs3.readFileSync(dotGit, "utf8");
|
|
9948
|
-
} catch {
|
|
9949
|
-
return null;
|
|
9950
|
-
}
|
|
9951
|
-
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
9952
|
-
if (!match)
|
|
9953
|
-
return null;
|
|
9954
|
-
gitDir = path6.resolve(dir, match[1].trim());
|
|
9955
|
-
} else
|
|
9956
|
-
return null;
|
|
9957
|
-
if (!fs3.existsSync(path6.join(gitDir, "HEAD")))
|
|
9958
|
-
return null;
|
|
9959
|
-
} else if (isGitDir(dir))
|
|
9960
|
-
gitDir = dir;
|
|
9961
|
-
if (gitDir) {
|
|
9962
|
-
let common = gitDir;
|
|
9963
|
-
try {
|
|
9964
|
-
common = path6.resolve(gitDir, fs3.readFileSync(path6.join(gitDir, "commondir"), "utf8").trim());
|
|
9965
|
-
} catch {}
|
|
9966
|
-
if (!fs3.existsSync(path6.join(common, "objects")))
|
|
9967
|
-
return null;
|
|
9968
|
-
return {
|
|
9969
|
-
kind: "repo",
|
|
9970
|
-
common: real(common),
|
|
9971
|
-
gitDir: real(gitDir),
|
|
9972
|
-
top: top === null ? null : real(top),
|
|
9973
|
-
reftable: reftableRefs(common)
|
|
9974
|
-
};
|
|
9771
|
+
for (const branch of namedBranches(entry))
|
|
9772
|
+
protectedNames.add(branch);
|
|
9773
|
+
if (!name.trim()) {
|
|
9774
|
+
out.issues.push("a release track with a blank name was ignored");
|
|
9775
|
+
continue;
|
|
9975
9776
|
}
|
|
9976
|
-
|
|
9977
|
-
if (
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
}
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
} catch {}
|
|
9988
|
-
if (!directory)
|
|
9989
|
-
return { dir: path6.join(root, ".workit"), shared: false, top: null };
|
|
9990
|
-
const fast = fastGit(root);
|
|
9991
|
-
if (fast?.kind === "none")
|
|
9992
|
-
return { dir: path6.join(root, ".workit"), shared: false, top: null };
|
|
9993
|
-
if (fast?.kind === "repo")
|
|
9994
|
-
return {
|
|
9995
|
-
dir: path6.join(fast.common, "workit"),
|
|
9996
|
-
shared: true,
|
|
9997
|
-
top: fast.top,
|
|
9998
|
-
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
9777
|
+
let critical = [];
|
|
9778
|
+
if (Array.isArray(entry.critical))
|
|
9779
|
+
critical = entry.critical.filter((item) => typeof item === "string");
|
|
9780
|
+
else if (entry.critical !== undefined)
|
|
9781
|
+
out.issues.push(`${label} critical: must be a list of field names; ignored`);
|
|
9782
|
+
const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
|
|
9783
|
+
const problem = (field, message) => {
|
|
9784
|
+
if (isCritical(field))
|
|
9785
|
+
fatal.push(`${label} ${field}: ${message}`);
|
|
9786
|
+
else
|
|
9787
|
+
out.issues.push(`${label} ${field}: ${message}`);
|
|
9999
9788
|
};
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
const
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
9789
|
+
for (const key of Object.keys(entry))
|
|
9790
|
+
if (!KNOWN_FIELDS.has(key))
|
|
9791
|
+
problem(key, "unknown field, ignored");
|
|
9792
|
+
const productionBranch = branchName(entry.productionBranch);
|
|
9793
|
+
const integrationBranch = branchName(entry.integrationBranch);
|
|
9794
|
+
if (!productionBranch)
|
|
9795
|
+
problem("productionBranch", "missing or not a branch name");
|
|
9796
|
+
if (!integrationBranch)
|
|
9797
|
+
problem("integrationBranch", "missing or not a branch name");
|
|
9798
|
+
const optionalBranch = (field) => {
|
|
9799
|
+
if (entry[field] === undefined)
|
|
9800
|
+
return null;
|
|
9801
|
+
const value = branchName(entry[field]);
|
|
9802
|
+
if (!value)
|
|
9803
|
+
problem(field, "not a branch name; using the integration branch");
|
|
9804
|
+
return value;
|
|
10008
9805
|
};
|
|
9806
|
+
const baseBranch = optionalBranch("baseBranch");
|
|
9807
|
+
const pullRequestTarget = optionalBranch("pullRequestTarget");
|
|
9808
|
+
let mergeBackBranches = [];
|
|
9809
|
+
if (entry.mergeBackBranches !== undefined) {
|
|
9810
|
+
const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
|
|
9811
|
+
if (list.some((item) => item === null))
|
|
9812
|
+
problem("mergeBackBranches", "must be a list of branch names; ignored");
|
|
9813
|
+
else
|
|
9814
|
+
mergeBackBranches = [...new Set(list)];
|
|
9815
|
+
}
|
|
9816
|
+
const naming = {};
|
|
9817
|
+
if (entry.naming !== undefined) {
|
|
9818
|
+
if (!isRecord(entry.naming))
|
|
9819
|
+
problem("naming", "not an object; ignored");
|
|
9820
|
+
else
|
|
9821
|
+
for (const kind of ["feature", "release", "hotfix"]) {
|
|
9822
|
+
const template = entry.naming[kind];
|
|
9823
|
+
if (template === undefined)
|
|
9824
|
+
continue;
|
|
9825
|
+
if (typeof template !== "string" || !template.trim())
|
|
9826
|
+
problem(`naming.${kind}`, "not a template string; ignored");
|
|
9827
|
+
else if (!literalOf(template))
|
|
9828
|
+
problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
|
|
9829
|
+
else
|
|
9830
|
+
naming[kind] = template.trim();
|
|
9831
|
+
}
|
|
9832
|
+
}
|
|
9833
|
+
if (!productionBranch || !integrationBranch) {
|
|
9834
|
+
out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
|
|
9835
|
+
continue;
|
|
9836
|
+
}
|
|
9837
|
+
out.tracks.push({
|
|
9838
|
+
name,
|
|
9839
|
+
productionBranch,
|
|
9840
|
+
integrationBranch,
|
|
9841
|
+
baseBranch: baseBranch ?? integrationBranch,
|
|
9842
|
+
pullRequestTarget: pullRequestTarget ?? integrationBranch,
|
|
9843
|
+
mergeBackBranches,
|
|
9844
|
+
naming
|
|
9845
|
+
});
|
|
10009
9846
|
}
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
9847
|
+
out.protectedBranches = [...protectedNames];
|
|
9848
|
+
if (fatal.length)
|
|
9849
|
+
out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
|
|
9850
|
+
return out;
|
|
10014
9851
|
}
|
|
10015
|
-
function
|
|
10016
|
-
|
|
10017
|
-
|
|
10018
|
-
const
|
|
10019
|
-
if (
|
|
10020
|
-
|
|
10021
|
-
try {
|
|
10022
|
-
head = fs3.readFileSync(path6.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10023
|
-
} catch {}
|
|
10024
|
-
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10025
|
-
if (ref && ref[1] !== ".invalid")
|
|
10026
|
-
return branchKey(ref[1]);
|
|
10027
|
-
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10028
|
-
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10029
|
-
if (rebasing)
|
|
10030
|
-
return branchKey(rebasing);
|
|
9852
|
+
function resolveReleaseTrack(input) {
|
|
9853
|
+
const tracks = [...input.tracks];
|
|
9854
|
+
const issues = [...input.issues ?? []];
|
|
9855
|
+
const requested = input.requested?.trim();
|
|
9856
|
+
if (tracks.length === 0) {
|
|
9857
|
+
if (requested)
|
|
10031
9858
|
return {
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
9859
|
+
status: "invalid",
|
|
9860
|
+
error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
|
|
9861
|
+
issues
|
|
10035
9862
|
};
|
|
10036
|
-
}
|
|
9863
|
+
return { status: "none", issues };
|
|
10037
9864
|
}
|
|
10038
|
-
const
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
9865
|
+
const names = tracks.map((track) => track.name).join(", ");
|
|
9866
|
+
let envProblem = null;
|
|
9867
|
+
const done = (track, source, detail, warnings = [], ambiguous = null) => ({
|
|
9868
|
+
status: "resolved",
|
|
9869
|
+
track: track && track.name !== IMPLICIT ? track : null,
|
|
9870
|
+
source,
|
|
9871
|
+
detail,
|
|
9872
|
+
warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
|
|
9873
|
+
tracks,
|
|
9874
|
+
blocking: envProblem ?? ambiguous
|
|
9875
|
+
});
|
|
9876
|
+
if (requested) {
|
|
9877
|
+
const named = tracks.find((track) => track.name === requested);
|
|
9878
|
+
if (!named)
|
|
9879
|
+
return {
|
|
9880
|
+
status: "invalid",
|
|
9881
|
+
error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
|
|
9882
|
+
issues
|
|
9883
|
+
};
|
|
9884
|
+
return done(named, "flag", `track ${named.name} was requested explicitly`);
|
|
10047
9885
|
}
|
|
10048
|
-
const
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
9886
|
+
const owned = (branch) => branch !== null && tracks.some((track) => [
|
|
9887
|
+
track.integrationBranch,
|
|
9888
|
+
track.productionBranch,
|
|
9889
|
+
track.baseBranch,
|
|
9890
|
+
track.pullRequestTarget
|
|
9891
|
+
].includes(branch));
|
|
9892
|
+
const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
|
|
9893
|
+
name: IMPLICIT,
|
|
9894
|
+
productionBranch: input.defaultBranch,
|
|
9895
|
+
integrationBranch: input.defaultBranch,
|
|
9896
|
+
baseBranch: input.defaultBranch,
|
|
9897
|
+
pullRequestTarget: input.defaultBranch,
|
|
9898
|
+
mergeBackBranches: [],
|
|
9899
|
+
naming: {}
|
|
9900
|
+
} : null;
|
|
9901
|
+
const candidates = implicit ? [...tracks, implicit] : tracks;
|
|
9902
|
+
const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
|
|
9903
|
+
const fallback = (why) => {
|
|
9904
|
+
const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
|
|
9905
|
+
const track = byDefault.length === 1 ? byDefault[0] : null;
|
|
9906
|
+
const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
|
|
9907
|
+
const ambiguous = candidates.length >= 2 ? `can't tell which release line ${input.branch ?? "HEAD"} is on (${why}); pass --track <name> (${names}) or --base <branch>` : null;
|
|
9908
|
+
return done(track, "default", `${why}; using the workspace ${target}`, [
|
|
9909
|
+
`release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
|
|
9910
|
+
], ambiguous);
|
|
9911
|
+
};
|
|
9912
|
+
const branch = input.branch;
|
|
9913
|
+
if (branch) {
|
|
9914
|
+
const owners = ownedBy(candidates, branch);
|
|
9915
|
+
if (owners.length === 1)
|
|
9916
|
+
return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
|
|
9917
|
+
if (owners.length > 1)
|
|
9918
|
+
return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
|
|
10059
9919
|
}
|
|
10060
|
-
const
|
|
10061
|
-
if (
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
return fs3.realpathSync(found.top);
|
|
10067
|
-
} catch {
|
|
10068
|
-
return canonical;
|
|
9920
|
+
const env = input.envRequested?.trim();
|
|
9921
|
+
if (env) {
|
|
9922
|
+
const named = tracks.find((track) => track.name === env);
|
|
9923
|
+
if (named)
|
|
9924
|
+
return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
|
|
9925
|
+
envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
|
|
10069
9926
|
}
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
if (!
|
|
10073
|
-
return [];
|
|
10074
|
-
|
|
10075
|
-
const
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
9927
|
+
if (!branch)
|
|
9928
|
+
return fallback("HEAD is detached");
|
|
9929
|
+
if (tracks.length === 1 && !implicit)
|
|
9930
|
+
return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
|
|
9931
|
+
let remotes = null;
|
|
9932
|
+
const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
|
|
9933
|
+
const ownerOf = (ref) => {
|
|
9934
|
+
const owner = ownedBy(candidates, short(ref));
|
|
9935
|
+
return owner.length === 1 ? owner[0] : null;
|
|
9936
|
+
};
|
|
9937
|
+
const followRecorded = (start) => {
|
|
9938
|
+
let cursor = start;
|
|
9939
|
+
const seen = new Set([start]);
|
|
9940
|
+
for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
|
|
9941
|
+
const recorded = input.probe.recordedBase(cursor);
|
|
9942
|
+
if (!recorded)
|
|
9943
|
+
return null;
|
|
9944
|
+
const base = short(recorded);
|
|
9945
|
+
const owner = ownerOf(base);
|
|
9946
|
+
if (owner)
|
|
9947
|
+
return owner;
|
|
9948
|
+
if (ownedBy(candidates, base).length > 1 || seen.has(base))
|
|
9949
|
+
return null;
|
|
9950
|
+
seen.add(base);
|
|
9951
|
+
cursor = base;
|
|
10081
9952
|
}
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
9953
|
+
return null;
|
|
9954
|
+
};
|
|
9955
|
+
const recordedOwner = followRecorded(branch);
|
|
9956
|
+
if (recordedOwner)
|
|
9957
|
+
return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
|
|
9958
|
+
for (const source of ["reflog", "checkout"]) {
|
|
9959
|
+
const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
|
|
9960
|
+
if (!ref)
|
|
9961
|
+
continue;
|
|
9962
|
+
const owner = ownerOf(ref) ?? followRecorded(short(ref));
|
|
9963
|
+
if (owner)
|
|
9964
|
+
return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
|
|
9965
|
+
}
|
|
9966
|
+
const forks = [];
|
|
9967
|
+
for (const track of candidates)
|
|
9968
|
+
for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
|
|
9969
|
+
const sha = input.probe.mergeBase(branch, ref);
|
|
9970
|
+
if (sha)
|
|
9971
|
+
forks.push({ track, ref, sha });
|
|
9972
|
+
}
|
|
9973
|
+
if (forks.length === 0)
|
|
9974
|
+
return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
|
|
9975
|
+
const ancestry = new Map;
|
|
9976
|
+
const descends = (older, newer) => {
|
|
9977
|
+
if (older === newer)
|
|
9978
|
+
return false;
|
|
9979
|
+
const key = `${older}>${newer}`;
|
|
9980
|
+
if (!ancestry.has(key))
|
|
9981
|
+
ancestry.set(key, input.probe.isAncestor(older, newer));
|
|
9982
|
+
return ancestry.get(key);
|
|
9983
|
+
};
|
|
9984
|
+
const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
|
|
9985
|
+
const winningTracks = [...new Set(winners.map((fork) => fork.track))];
|
|
9986
|
+
if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
|
|
9987
|
+
const win = winners[0];
|
|
9988
|
+
return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
|
|
9989
|
+
}
|
|
9990
|
+
if (winningTracks.length === 1)
|
|
9991
|
+
return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
|
|
9992
|
+
const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
|
|
9993
|
+
return fallback(`${branch} has the same or unrelated fork points with ${[...new Set(newest.map((fork) => `${fork.ref} (${fork.track.name === IMPLICIT ? "workspace default" : fork.track.name})`))].join(" and ")}`);
|
|
9994
|
+
}
|
|
9995
|
+
function gitTrackProbe(cwd) {
|
|
9996
|
+
let remote;
|
|
9997
|
+
let remotes;
|
|
9998
|
+
const tip = (branch) => {
|
|
9999
|
+
remote ??= pushRemoteName(cwd) ?? "origin";
|
|
10000
|
+
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
|
|
10001
|
+
};
|
|
10002
|
+
return {
|
|
10003
|
+
remotes: () => remotes ??= remoteNames(cwd),
|
|
10004
|
+
recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
|
|
10005
|
+
createdFrom: (branch) => {
|
|
10006
|
+
const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
|
|
10007
|
+
const oldest = log?.split(`
|
|
10008
|
+
`).findLast(Boolean) ?? "";
|
|
10009
|
+
const match = /^branch: Created from (\S+)$/u.exec(oldest);
|
|
10010
|
+
return match && match[1] !== "HEAD" ? match[1] : null;
|
|
10011
|
+
},
|
|
10012
|
+
checkedOutFrom: (branch) => {
|
|
10013
|
+
const own = reflog(cwd, `refs/heads/${branch}`);
|
|
10014
|
+
const created = own.at(-1);
|
|
10015
|
+
if (!created || created.subject !== "branch: Created from HEAD")
|
|
10016
|
+
return null;
|
|
10017
|
+
const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
|
|
10018
|
+
const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
|
|
10019
|
+
return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
|
|
10020
|
+
},
|
|
10021
|
+
mergeBase: (branch, trackBranch) => {
|
|
10022
|
+
const base = tip(trackBranch);
|
|
10023
|
+
return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
|
|
10024
|
+
},
|
|
10025
|
+
isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
|
|
10026
|
+
};
|
|
10027
|
+
}
|
|
10028
|
+
function workspaceReleaseTracks(cwd) {
|
|
10029
|
+
const raw = resolveRuntimeReleaseTracks(cwd);
|
|
10030
|
+
return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
|
|
10031
|
+
}
|
|
10032
|
+
function resolveTrackFor(cwd, options) {
|
|
10033
|
+
const read = workspaceReleaseTracks(cwd);
|
|
10034
|
+
if (read.error)
|
|
10035
|
+
return { status: "invalid", error: read.error, issues: read.issues };
|
|
10036
|
+
if (read.tracks.length === 0 && !options.track?.trim())
|
|
10037
|
+
return { status: "none", issues: read.issues };
|
|
10038
|
+
const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
|
|
10039
|
+
const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
|
|
10040
|
+
const input = {
|
|
10041
|
+
tracks: read.tracks,
|
|
10042
|
+
requested: options.track ?? null,
|
|
10043
|
+
envRequested,
|
|
10044
|
+
branch,
|
|
10045
|
+
defaultBranch: options.defaultBranch,
|
|
10046
|
+
issues: read.issues
|
|
10087
10047
|
};
|
|
10088
|
-
|
|
10089
|
-
if (
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10048
|
+
let key = null;
|
|
10049
|
+
if (!options.probe) {
|
|
10050
|
+
const refs = git2(cwd, [
|
|
10051
|
+
"for-each-ref",
|
|
10052
|
+
"--format=%(refname) %(objectname)",
|
|
10053
|
+
"refs/heads",
|
|
10054
|
+
"refs/remotes"
|
|
10055
|
+
]) ?? "";
|
|
10056
|
+
const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
|
|
10057
|
+
const own = branch ? [
|
|
10058
|
+
git2(cwd, [
|
|
10059
|
+
"log",
|
|
10060
|
+
"-g",
|
|
10061
|
+
"-n",
|
|
10062
|
+
"1",
|
|
10063
|
+
"--date=unix",
|
|
10064
|
+
"--format=%H %gd %gs",
|
|
10065
|
+
`refs/heads/${branch}`,
|
|
10066
|
+
"--"
|
|
10067
|
+
]) ?? "",
|
|
10068
|
+
git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
|
|
10069
|
+
] : [];
|
|
10070
|
+
key = createHash5("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
|
|
10071
|
+
const hit = memo2.get(key);
|
|
10072
|
+
if (hit)
|
|
10073
|
+
return hit;
|
|
10108
10074
|
}
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
current = rename.to;
|
|
10117
|
-
since = rename.at;
|
|
10118
|
-
}
|
|
10119
|
-
return current;
|
|
10075
|
+
const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
|
|
10076
|
+
if (key) {
|
|
10077
|
+
if (memo2.size >= MEMO_LIMIT)
|
|
10078
|
+
memo2.delete(memo2.keys().next().value);
|
|
10079
|
+
memo2.set(key, resolution);
|
|
10080
|
+
}
|
|
10081
|
+
return resolution;
|
|
10120
10082
|
}
|
|
10121
|
-
var
|
|
10083
|
+
var ReleaseTrackError, KNOWN_FIELDS, BRANCH_FIELDS, PLACEHOLDER, HAS_PLACEHOLDER, isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value), branchName = (value) => typeof value === "string" && value.trim() && !value.trim().startsWith("-") ? value.trim() : null, namedBranches = (entry) => [
|
|
10084
|
+
...BRANCH_FIELDS.map((field) => branchName(entry[field])),
|
|
10085
|
+
...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
|
|
10086
|
+
].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
|
|
10087
|
+
...new Set(tracks.flatMap((track) => [
|
|
10088
|
+
track.productionBranch,
|
|
10089
|
+
track.integrationBranch,
|
|
10090
|
+
track.baseBranch,
|
|
10091
|
+
track.pullRequestTarget,
|
|
10092
|
+
...track.mergeBackBranches
|
|
10093
|
+
]))
|
|
10094
|
+
], namingGlob = (template) => HAS_PLACEHOLDER.test(template) ? template.replace(PLACEHOLDER, "*") : `${template.replace(/\/?\*?$/u, "")}/*`, IMPLICIT = "", ownedBy = (tracks, branch) => tracks.filter((track) => [track.integrationBranch, track.productionBranch, track.baseBranch].includes(branch)), shortRef = (ref, remotes) => {
|
|
10095
|
+
const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
|
|
10096
|
+
const remote = remotes.find((name) => bare.startsWith(`${name}/`));
|
|
10097
|
+
return remote ? bare.slice(remote.length + 1) : bare;
|
|
10098
|
+
}, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
|
|
10122
10099
|
const run = spawnSync6("git", args, {
|
|
10123
10100
|
cwd,
|
|
10124
10101
|
encoding: "utf8",
|
|
10125
10102
|
timeout: 20000,
|
|
10126
|
-
killSignal: "SIGKILL",
|
|
10127
10103
|
windowsHide: true,
|
|
10128
|
-
|
|
10129
|
-
...process.env,
|
|
10130
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
10131
|
-
GIT_OPTIONAL_LOCKS: "0",
|
|
10132
|
-
LC_ALL: "C",
|
|
10133
|
-
LANGUAGE: "C"
|
|
10134
|
-
}
|
|
10104
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10135
10105
|
});
|
|
10136
|
-
|
|
10106
|
+
return run.status === 0 ? (run.stdout ?? "").trim() : null;
|
|
10107
|
+
}, recordedBaseKey = (branch) => `branch.${branch}.workitBase`, escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), reflog = (cwd, ref) => (git2(cwd, ["log", "-g", "--date=unix", "--format=%H%x09%gd%x09%gs", ref, "--"]) ?? "").split(`
|
|
10108
|
+
`).map((line) => {
|
|
10109
|
+
const [sha = "", selector = "", ...subject] = line.split("\t");
|
|
10137
10110
|
return {
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
missing: error?.code === "ENOENT"
|
|
10111
|
+
sha,
|
|
10112
|
+
time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
|
|
10113
|
+
subject: subject.join("\t")
|
|
10142
10114
|
};
|
|
10143
|
-
}
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10115
|
+
}).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
|
|
10116
|
+
var init_release_tracks = __esm(() => {
|
|
10117
|
+
init_rev();
|
|
10118
|
+
init_config();
|
|
10119
|
+
init_workspaces();
|
|
10120
|
+
ReleaseTrackError = class ReleaseTrackError extends Error {
|
|
10121
|
+
};
|
|
10122
|
+
KNOWN_FIELDS = new Set([
|
|
10123
|
+
"strategy",
|
|
10124
|
+
"productionBranch",
|
|
10125
|
+
"integrationBranch",
|
|
10126
|
+
"naming",
|
|
10127
|
+
"baseBranch",
|
|
10128
|
+
"mergeBackBranches",
|
|
10129
|
+
"pullRequestTarget",
|
|
10130
|
+
"tagNamespace",
|
|
10131
|
+
"versionSource",
|
|
10132
|
+
"requiredChecks",
|
|
10133
|
+
"critical"
|
|
10134
|
+
]);
|
|
10135
|
+
BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
|
|
10136
|
+
PLACEHOLDER = /\{[^}]*\}/gu;
|
|
10137
|
+
HAS_PLACEHOLDER = /\{[^}]*\}/u;
|
|
10138
|
+
memo2 = new Map;
|
|
10139
|
+
});
|
|
10140
|
+
|
|
10141
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
10142
|
+
import fs4 from "node:fs";
|
|
10143
|
+
import path5 from "node:path";
|
|
10144
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
10145
|
+
function vcsConfig(mode, cwd, options = {}) {
|
|
10146
|
+
const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
|
|
10147
|
+
const wsVcs = ws?.vcs ?? {};
|
|
10148
|
+
const wsYt = ws?.youtrack ?? {};
|
|
10149
|
+
const wsIssues = ws?.issues ?? {};
|
|
10150
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
10151
|
+
const root = vcsCwd(cwd);
|
|
10152
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
10153
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
10154
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
10155
|
+
const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
|
|
10156
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
10157
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
10158
|
+
let issuesProvider = null;
|
|
10159
|
+
let linkOnPr = null;
|
|
10160
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
10161
|
+
issuesProvider = "github";
|
|
10162
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
10148
10163
|
}
|
|
10149
|
-
|
|
10164
|
+
if (mode === "resolve") {
|
|
10165
|
+
if (cfgStatus === "malformed")
|
|
10166
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10167
|
+
const trackResolution = resolveTrackFor(root, {
|
|
10168
|
+
track: options.track,
|
|
10169
|
+
...options.branch !== undefined ? { branch: options.branch } : {},
|
|
10170
|
+
defaultBranch: workspaceDefault
|
|
10171
|
+
});
|
|
10172
|
+
if (trackResolution.status === "invalid")
|
|
10173
|
+
return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
|
|
10174
|
+
const releaseTrack = trackSummary(trackResolution);
|
|
10175
|
+
const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
|
|
10176
|
+
const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
|
|
10177
|
+
return {
|
|
10178
|
+
ok: true,
|
|
10179
|
+
workspace_name: ws?.name ?? null,
|
|
10180
|
+
provider,
|
|
10181
|
+
defaultTargetBranch: defaultTarget,
|
|
10182
|
+
workspaceDefaultTargetBranch: workspaceDefault,
|
|
10183
|
+
baseBranch,
|
|
10184
|
+
releaseTrack,
|
|
10185
|
+
link_issues: linkIssues,
|
|
10186
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10187
|
+
issues_provider: issuesProvider,
|
|
10188
|
+
link_on_pr: linkOnPr
|
|
10189
|
+
};
|
|
10190
|
+
}
|
|
10191
|
+
if (cfgStatus === "malformed") {
|
|
10192
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10193
|
+
}
|
|
10194
|
+
if (cfgStatus === "missing" && provider === null)
|
|
10195
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
10196
|
+
if (provider === null) {
|
|
10197
|
+
return {
|
|
10198
|
+
ok: false,
|
|
10199
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
10200
|
+
configPath: cfgPath
|
|
10201
|
+
};
|
|
10202
|
+
}
|
|
10203
|
+
const prov = cfg[provider] ?? {};
|
|
10204
|
+
const out = {
|
|
10205
|
+
ok: true,
|
|
10206
|
+
configPath: path5.resolve(cfgPath),
|
|
10207
|
+
provider,
|
|
10208
|
+
defaultTargetBranch: workspaceDefault,
|
|
10209
|
+
pr: cfg.pr ?? {},
|
|
10210
|
+
workspace_name: ws?.name ?? null,
|
|
10211
|
+
link_issues: linkIssues,
|
|
10212
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10213
|
+
issues_provider: issuesProvider,
|
|
10214
|
+
link_on_pr: linkOnPr
|
|
10215
|
+
};
|
|
10216
|
+
if (provider === "gitlab") {
|
|
10217
|
+
out.gitlab = {
|
|
10218
|
+
host: prov.host ?? "gitlab.com",
|
|
10219
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
10220
|
+
};
|
|
10221
|
+
} else if (provider === "github") {
|
|
10222
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
10223
|
+
}
|
|
10224
|
+
return out;
|
|
10225
|
+
}
|
|
10226
|
+
var workspacesPath = () => path5.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
|
|
10227
|
+
const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
10228
|
+
if (r.status !== 0)
|
|
10229
|
+
return null;
|
|
10230
|
+
const url = (r.stdout ?? "").trim();
|
|
10231
|
+
if (/github\.com[:/]/.test(url))
|
|
10232
|
+
return "github";
|
|
10233
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
10234
|
+
return "gitlab";
|
|
10235
|
+
return null;
|
|
10236
|
+
}, readVcsConfig = () => {
|
|
10237
|
+
const file = vcsConfigPath();
|
|
10238
|
+
let raw;
|
|
10150
10239
|
try {
|
|
10151
|
-
|
|
10240
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
10152
10241
|
} catch {
|
|
10153
|
-
return
|
|
10242
|
+
return { status: "missing", path: file, config: {} };
|
|
10154
10243
|
}
|
|
10155
|
-
|
|
10244
|
+
let parsed;
|
|
10156
10245
|
try {
|
|
10157
|
-
|
|
10246
|
+
parsed = JSON.parse(raw);
|
|
10158
10247
|
} catch {
|
|
10159
|
-
return
|
|
10248
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
10160
10249
|
}
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10250
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10251
|
+
return { status: "valid", path: file, config: parsed };
|
|
10252
|
+
}
|
|
10253
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
10254
|
+
}, trackSummary = (resolution) => resolution.status === "resolved" ? {
|
|
10255
|
+
name: resolution.track?.name ?? null,
|
|
10256
|
+
source: resolution.source,
|
|
10257
|
+
detail: resolution.detail,
|
|
10258
|
+
warnings: resolution.warnings,
|
|
10259
|
+
track: resolution.track,
|
|
10260
|
+
tracks: resolution.tracks,
|
|
10261
|
+
blocking: resolution.blocking
|
|
10262
|
+
} : null;
|
|
10263
|
+
var init_vcs_config = __esm(() => {
|
|
10264
|
+
init_config();
|
|
10265
|
+
init_workspaces();
|
|
10266
|
+
init_release_tracks();
|
|
10267
|
+
});
|
|
10268
|
+
|
|
10269
|
+
// packages/workit-core/src/core/logger.ts
|
|
10270
|
+
import os3 from "node:os";
|
|
10271
|
+
import path6 from "node:path";
|
|
10272
|
+
var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
|
|
10273
|
+
const override = process.env.WORKFLOW_TOOLKIT_STATE;
|
|
10274
|
+
if (override)
|
|
10275
|
+
return override;
|
|
10276
|
+
const home = os3.homedir();
|
|
10277
|
+
if (process.env.XDG_STATE_HOME)
|
|
10278
|
+
return path6.join(process.env.XDG_STATE_HOME, "workit");
|
|
10279
|
+
switch (os3.platform()) {
|
|
10280
|
+
case "darwin":
|
|
10281
|
+
return path6.join(home, "Library", "Application Support", "workit");
|
|
10282
|
+
case "win32":
|
|
10283
|
+
return path6.join(process.env.LOCALAPPDATA ?? path6.join(home, "AppData", "Local"), "workit");
|
|
10284
|
+
default:
|
|
10285
|
+
return path6.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
10169
10286
|
}
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
"
|
|
10182
|
-
"
|
|
10183
|
-
"
|
|
10184
|
-
"
|
|
10185
|
-
"
|
|
10186
|
-
|
|
10287
|
+
}, homePattern;
|
|
10288
|
+
var init_logger = __esm(() => {
|
|
10289
|
+
SENSITIVE_WORDS = new Set([
|
|
10290
|
+
"token",
|
|
10291
|
+
"secret",
|
|
10292
|
+
"password",
|
|
10293
|
+
"passwd",
|
|
10294
|
+
"authorization",
|
|
10295
|
+
"credential",
|
|
10296
|
+
"credentials",
|
|
10297
|
+
"cookie",
|
|
10298
|
+
"api",
|
|
10299
|
+
"apikey",
|
|
10300
|
+
"apitoken",
|
|
10301
|
+
"accesstoken",
|
|
10302
|
+
"clientsecret",
|
|
10303
|
+
"refreshtoken",
|
|
10304
|
+
"key",
|
|
10305
|
+
"bearer",
|
|
10306
|
+
"prompt",
|
|
10307
|
+
"message",
|
|
10308
|
+
"messages",
|
|
10309
|
+
"content",
|
|
10310
|
+
"body",
|
|
10311
|
+
"args",
|
|
10312
|
+
"argument",
|
|
10313
|
+
"result",
|
|
10314
|
+
"results",
|
|
10315
|
+
"output",
|
|
10316
|
+
"issue",
|
|
10317
|
+
"description",
|
|
10318
|
+
"summary",
|
|
10319
|
+
"payload",
|
|
10320
|
+
"script",
|
|
10321
|
+
"command",
|
|
10322
|
+
"text",
|
|
10323
|
+
"query"
|
|
10324
|
+
]);
|
|
10325
|
+
STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
|
|
10326
|
+
homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
10187
10327
|
});
|
|
10188
10328
|
|
|
10189
10329
|
// packages/workit-core/src/host-session.ts
|
|
10190
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
10191
|
-
import
|
|
10330
|
+
import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10331
|
+
import fs5 from "node:fs";
|
|
10192
10332
|
import path7 from "node:path";
|
|
10193
10333
|
function hostSessionFromEnv(env) {
|
|
10194
10334
|
const value = (key) => env[key]?.trim() || null;
|
|
@@ -10217,7 +10357,7 @@ function migrateRepoIssued(cwd) {
|
|
|
10217
10357
|
if (location instanceof Error)
|
|
10218
10358
|
return;
|
|
10219
10359
|
old = path7.join(location.dir, "hooks", "issued");
|
|
10220
|
-
if (!
|
|
10360
|
+
if (!fs5.existsSync(old))
|
|
10221
10361
|
return;
|
|
10222
10362
|
} catch {
|
|
10223
10363
|
return;
|
|
@@ -10225,13 +10365,18 @@ function migrateRepoIssued(cwd) {
|
|
|
10225
10365
|
for (const lead of safeList(old)) {
|
|
10226
10366
|
try {
|
|
10227
10367
|
const from = path7.join(old, lead);
|
|
10228
|
-
if (!
|
|
10368
|
+
if (!fs5.statSync(from).isDirectory())
|
|
10229
10369
|
continue;
|
|
10230
10370
|
const to = path7.join(userIssuedRoot(), lead);
|
|
10231
|
-
|
|
10232
|
-
for (const id of
|
|
10233
|
-
|
|
10234
|
-
|
|
10371
|
+
fs5.mkdirSync(to, { recursive: true, mode: 448 });
|
|
10372
|
+
for (const id of fs5.readdirSync(from)) {
|
|
10373
|
+
try {
|
|
10374
|
+
fs5.copyFileSync(path7.join(from, id), path7.join(to, id), fs5.constants.COPYFILE_EXCL);
|
|
10375
|
+
} catch (error) {
|
|
10376
|
+
if (error.code !== "EEXIST")
|
|
10377
|
+
throw error;
|
|
10378
|
+
}
|
|
10379
|
+
fs5.rmSync(path7.join(from, id), { force: true });
|
|
10235
10380
|
}
|
|
10236
10381
|
removeIfEmpty(from);
|
|
10237
10382
|
} catch {}
|
|
@@ -10239,30 +10384,76 @@ function migrateRepoIssued(cwd) {
|
|
|
10239
10384
|
removeIfEmpty(old);
|
|
10240
10385
|
}
|
|
10241
10386
|
function recordIssuedSession(cwd, lead, issued) {
|
|
10387
|
+
const token = randomBytes(18).toString("base64url");
|
|
10242
10388
|
try {
|
|
10243
10389
|
migrateRepoIssued(cwd);
|
|
10244
|
-
const dir = path7.join(userIssuedRoot(), hash(lead));
|
|
10245
|
-
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10246
10390
|
const own = hash(issued);
|
|
10247
|
-
|
|
10248
|
-
|
|
10391
|
+
const tokens = path7.join(userTokenRoot(), hash(lead));
|
|
10392
|
+
fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
|
|
10393
|
+
fs5.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
|
|
10394
|
+
`, { mode: 384, flag: "wx" });
|
|
10395
|
+
const dir = path7.join(userIssuedRoot(), hash(lead));
|
|
10396
|
+
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10397
|
+
fs5.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
|
|
10398
|
+
`, { mode: 384 });
|
|
10399
|
+
pruneIssued(dir, own, tokens);
|
|
10400
|
+
return token;
|
|
10401
|
+
} catch {
|
|
10402
|
+
return null;
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
function pruneIssued(dir, own, tokens) {
|
|
10406
|
+
try {
|
|
10407
|
+
const entries = fs5.readdirSync(dir);
|
|
10249
10408
|
if (entries.length <= ISSUED_PER_LEAD)
|
|
10250
10409
|
return;
|
|
10251
|
-
const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at:
|
|
10252
|
-
for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
|
|
10253
|
-
|
|
10410
|
+
const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at: fs5.statSync(path7.join(dir, name)).mtimeMs })).toSorted((a, b) => a.at - b.at);
|
|
10411
|
+
for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
|
|
10412
|
+
fs5.rmSync(path7.join(dir, name), { force: true });
|
|
10413
|
+
fs5.rmSync(path7.join(tokens, name), { force: true });
|
|
10414
|
+
}
|
|
10254
10415
|
} catch {}
|
|
10255
10416
|
}
|
|
10256
|
-
|
|
10417
|
+
function issuedBinding(cwd, claimed) {
|
|
10418
|
+
const record = issuedRecord(cwd, claimed);
|
|
10419
|
+
const { stored, leadBound } = issuedToken(claimed);
|
|
10420
|
+
if (stored)
|
|
10421
|
+
return "token";
|
|
10422
|
+
if (record === null)
|
|
10423
|
+
return null;
|
|
10424
|
+
return leadBound ? "token" : "legacy";
|
|
10425
|
+
}
|
|
10426
|
+
var HOST_SESSION_VARS, family = (host) => host?.replace(/_(?:cli|desktop)$/, "") ?? null, ISSUED_PER_LEAD = 64, ISSUED_SESSIONS_MARKER = "workit-issued-sessions/3", hash = (value) => createHash6("sha256").update(value).digest("hex").slice(0, 32), userIssuedRoot = () => path7.join(resolveStateDir(), "issued"), userTokenRoot = () => path7.join(resolveStateDir(), "issued-tokens"), safeList = (dir) => {
|
|
10257
10427
|
try {
|
|
10258
|
-
return
|
|
10428
|
+
return fs5.readdirSync(dir);
|
|
10259
10429
|
} catch {
|
|
10260
10430
|
return [];
|
|
10261
10431
|
}
|
|
10262
10432
|
}, removeIfEmpty = (dir) => {
|
|
10263
10433
|
try {
|
|
10264
|
-
|
|
10434
|
+
fs5.rmdirSync(dir);
|
|
10265
10435
|
} catch {}
|
|
10436
|
+
}, tokenHash = (token) => createHash6("sha256").update(token).digest("hex"), issuedRecord = (cwd, claimed) => {
|
|
10437
|
+
const cut = claimed.lastIndexOf(":");
|
|
10438
|
+
if (cut <= 0)
|
|
10439
|
+
return null;
|
|
10440
|
+
migrateRepoIssued(cwd);
|
|
10441
|
+
try {
|
|
10442
|
+
return fs5.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
|
|
10443
|
+
} catch {
|
|
10444
|
+
return null;
|
|
10445
|
+
}
|
|
10446
|
+
}, TOKEN_HASH, issuedToken = (claimed) => {
|
|
10447
|
+
const cut = claimed.lastIndexOf(":");
|
|
10448
|
+
if (cut <= 0)
|
|
10449
|
+
return { stored: null, leadBound: false };
|
|
10450
|
+
const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
|
|
10451
|
+
try {
|
|
10452
|
+
const text = fs5.readFileSync(path7.join(dir, hash(claimed)), "utf8");
|
|
10453
|
+
return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
|
|
10454
|
+
} catch {
|
|
10455
|
+
return { stored: null, leadBound: safeList(dir).length > 0 };
|
|
10456
|
+
}
|
|
10266
10457
|
}, OWN_CHILD_SHELL;
|
|
10267
10458
|
var init_host_session = __esm(() => {
|
|
10268
10459
|
init_logger();
|
|
@@ -10275,13 +10466,14 @@ var init_host_session = __esm(() => {
|
|
|
10275
10466
|
(env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
|
|
10276
10467
|
]
|
|
10277
10468
|
];
|
|
10469
|
+
TOKEN_HASH = /^([0-9a-f]{64})$/mu;
|
|
10278
10470
|
OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
|
|
10279
10471
|
});
|
|
10280
10472
|
|
|
10281
10473
|
// packages/workit-core/src/ledger.ts
|
|
10282
|
-
import { spawnSync as
|
|
10283
|
-
import { createHash as createHash7, randomBytes } from "node:crypto";
|
|
10284
|
-
import
|
|
10474
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10475
|
+
import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
|
|
10476
|
+
import fs6 from "node:fs";
|
|
10285
10477
|
import path8 from "node:path";
|
|
10286
10478
|
function resolveCommit(cwd, ref) {
|
|
10287
10479
|
if (!safeRef(ref))
|
|
@@ -10357,7 +10549,7 @@ function ledgerPath(cwd) {
|
|
|
10357
10549
|
const root = storeRoot(cwd);
|
|
10358
10550
|
return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
|
|
10359
10551
|
}
|
|
10360
|
-
function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
10552
|
+
function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
|
|
10361
10553
|
const lock = `${file}.lock`;
|
|
10362
10554
|
const deadline = Date.now() + waitMs;
|
|
10363
10555
|
let token = null;
|
|
@@ -10381,13 +10573,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
|
10381
10573
|
}
|
|
10382
10574
|
function appendLine(file, build, options) {
|
|
10383
10575
|
const write = () => {
|
|
10384
|
-
const fd =
|
|
10576
|
+
const fd = fs6.openSync(file, "a+", 384);
|
|
10385
10577
|
try {
|
|
10386
|
-
|
|
10578
|
+
if (process.platform !== "win32")
|
|
10579
|
+
tighten(fd);
|
|
10580
|
+
const size = fs6.fstatSync(fd).size;
|
|
10387
10581
|
let fence = "";
|
|
10388
10582
|
if (size > 0) {
|
|
10389
10583
|
const last = Buffer.alloc(1);
|
|
10390
|
-
|
|
10584
|
+
fs6.readSync(fd, last, 0, 1, size - 1);
|
|
10391
10585
|
if (last[0] !== 10)
|
|
10392
10586
|
fence = `
|
|
10393
10587
|
`;
|
|
@@ -10396,16 +10590,16 @@ function appendLine(file, build, options) {
|
|
|
10396
10590
|
if (!line.ok)
|
|
10397
10591
|
return line;
|
|
10398
10592
|
const bytes = Buffer.from(fence + line.value);
|
|
10399
|
-
const written =
|
|
10593
|
+
const written = fs6.writeSync(fd, bytes);
|
|
10400
10594
|
if (written !== bytes.length)
|
|
10401
10595
|
return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
|
|
10402
10596
|
return { ok: true, value: undefined };
|
|
10403
10597
|
} finally {
|
|
10404
|
-
|
|
10598
|
+
fs6.closeSync(fd);
|
|
10405
10599
|
}
|
|
10406
10600
|
};
|
|
10407
10601
|
try {
|
|
10408
|
-
|
|
10602
|
+
fs6.mkdirSync(path8.dirname(file), { recursive: true });
|
|
10409
10603
|
if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
|
|
10410
10604
|
return write();
|
|
10411
10605
|
const locked = withLedgerLock(file, write);
|
|
@@ -10459,8 +10653,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10459
10653
|
if (!file.ok)
|
|
10460
10654
|
return file;
|
|
10461
10655
|
let text = "";
|
|
10656
|
+
let stat = null;
|
|
10462
10657
|
try {
|
|
10463
|
-
|
|
10658
|
+
stat = fs6.statSync(file.value);
|
|
10659
|
+
text = fs6.readFileSync(file.value, "utf8");
|
|
10464
10660
|
} catch (error) {
|
|
10465
10661
|
if (error.code === "ENOENT")
|
|
10466
10662
|
return {
|
|
@@ -10479,6 +10675,7 @@ function readLedger(cwd, options = {}) {
|
|
|
10479
10675
|
const integrity = { chained: 0, legacy: 0, unverified: [] };
|
|
10480
10676
|
const earlier = new Set([null]);
|
|
10481
10677
|
let chainStarted = false;
|
|
10678
|
+
const ids = new Set;
|
|
10482
10679
|
for (const raw of text.split(`
|
|
10483
10680
|
`)) {
|
|
10484
10681
|
const trimmed = raw.trim();
|
|
@@ -10512,6 +10709,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10512
10709
|
reason = "unsigned";
|
|
10513
10710
|
else
|
|
10514
10711
|
rowIntegrity = "legacy";
|
|
10712
|
+
if (options.integrity && !reason && ids.has(parsed.id))
|
|
10713
|
+
reason = "duplicate_id";
|
|
10714
|
+
if (options.integrity)
|
|
10715
|
+
ids.add(parsed.id);
|
|
10515
10716
|
if (reason)
|
|
10516
10717
|
rowIntegrity = "unverified";
|
|
10517
10718
|
const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
|
|
@@ -10535,7 +10736,8 @@ function readLedger(cwd, options = {}) {
|
|
|
10535
10736
|
seq: rows.length + 1,
|
|
10536
10737
|
superseded: false,
|
|
10537
10738
|
supersedeIgnored: false,
|
|
10538
|
-
...rowIntegrity ? { integrity: rowIntegrity } : {}
|
|
10739
|
+
...rowIntegrity ? { integrity: rowIntegrity } : {},
|
|
10740
|
+
...reason ? { integrityReason: reason } : {}
|
|
10539
10741
|
};
|
|
10540
10742
|
rows.push(row);
|
|
10541
10743
|
if (!options.integrity)
|
|
@@ -10563,18 +10765,44 @@ function readLedger(cwd, options = {}) {
|
|
|
10563
10765
|
const byId = new Map;
|
|
10564
10766
|
for (const row of rows) {
|
|
10565
10767
|
const target = row.supersedes ? byId.get(row.supersedes) : undefined;
|
|
10566
|
-
if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10768
|
+
if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10567
10769
|
target.superseded = true;
|
|
10568
10770
|
else if (row.supersedes)
|
|
10569
10771
|
row.supersedeIgnored = true;
|
|
10570
10772
|
if (!byId.has(row.id))
|
|
10571
10773
|
byId.set(row.id, row);
|
|
10572
10774
|
}
|
|
10775
|
+
if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
|
|
10776
|
+
tamperCache.set(file.value, {
|
|
10777
|
+
size: stat.size,
|
|
10778
|
+
stamp: fileStamp(stat),
|
|
10779
|
+
breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
|
|
10780
|
+
});
|
|
10573
10781
|
return {
|
|
10574
10782
|
ok: true,
|
|
10575
10783
|
value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
|
|
10576
10784
|
};
|
|
10577
10785
|
}
|
|
10786
|
+
function ledgerTampering(cwd) {
|
|
10787
|
+
const file = ledgerPath(cwd);
|
|
10788
|
+
if (!file.ok)
|
|
10789
|
+
return [];
|
|
10790
|
+
try {
|
|
10791
|
+
const stat = fs6.statSync(file.value);
|
|
10792
|
+
const cached = tamperCache.get(file.value);
|
|
10793
|
+
if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
|
|
10794
|
+
return cached.breaks;
|
|
10795
|
+
} catch {
|
|
10796
|
+
return [];
|
|
10797
|
+
}
|
|
10798
|
+
const read = readLedger(cwd, { integrity: true });
|
|
10799
|
+
return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
|
|
10800
|
+
}
|
|
10801
|
+
function tamperedUnblock(cwd) {
|
|
10802
|
+
const file = ledgerPath(cwd);
|
|
10803
|
+
const where = file.ok ? file.value : "the ledger file";
|
|
10804
|
+
return `workit ledger verify-integrity # lists the edited or missing rows; if the edit is understood, move ${where} aside (mv ${where} ${where}.tampered-<date>) and record the verdicts again`;
|
|
10805
|
+
}
|
|
10578
10806
|
function verdictBasis(verdict, current) {
|
|
10579
10807
|
if (verdict.head && verdict.head === current.head)
|
|
10580
10808
|
return "fresh";
|
|
@@ -10641,15 +10869,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
|
|
|
10641
10869
|
sessions.add(session);
|
|
10642
10870
|
return sessions;
|
|
10643
10871
|
}
|
|
10644
|
-
function
|
|
10872
|
+
function sessionTrust(cwd, row) {
|
|
10873
|
+
const session = row.actor.session;
|
|
10874
|
+
if (!session || row.self === true)
|
|
10875
|
+
return null;
|
|
10876
|
+
const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
|
|
10877
|
+
const host = row.actor.host;
|
|
10878
|
+
const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
|
|
10879
|
+
if (!issuedOnly)
|
|
10880
|
+
return null;
|
|
10881
|
+
const binding = issuedBinding(cwd, session);
|
|
10882
|
+
if (binding === null)
|
|
10883
|
+
return "unissued_session";
|
|
10884
|
+
return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
|
|
10885
|
+
}
|
|
10886
|
+
function trustReasons(cwd, row) {
|
|
10887
|
+
if (row.integrity === "unverified")
|
|
10888
|
+
return [row.integrityReason ?? "unsigned"];
|
|
10889
|
+
if (row.integrity !== "verified")
|
|
10890
|
+
return ["unsigned"];
|
|
10891
|
+
const session = sessionTrust(cwd, row);
|
|
10892
|
+
return session ? [session] : [];
|
|
10893
|
+
}
|
|
10894
|
+
function withIntegrity(cwd, rows) {
|
|
10895
|
+
if (rows.every((row) => row.integrity !== undefined))
|
|
10896
|
+
return rows;
|
|
10897
|
+
const read = readLedger(cwd, { integrity: true });
|
|
10898
|
+
const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
|
|
10899
|
+
return rows.map((row) => {
|
|
10900
|
+
if (row.integrity !== undefined)
|
|
10901
|
+
return row;
|
|
10902
|
+
const fresh = bySeq.get(row.seq);
|
|
10903
|
+
return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
|
|
10904
|
+
});
|
|
10905
|
+
}
|
|
10906
|
+
function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
|
|
10645
10907
|
let effective = null;
|
|
10646
10908
|
for (const row of rows) {
|
|
10647
10909
|
if (!effective) {
|
|
10648
10910
|
effective = row;
|
|
10649
10911
|
continue;
|
|
10650
10912
|
}
|
|
10651
|
-
|
|
10652
|
-
|
|
10913
|
+
if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
|
|
10914
|
+
continue;
|
|
10915
|
+
const rowIndependent = reasonsOf(row).length === 0;
|
|
10916
|
+
const effIndependent = reasonsOf(effective).length === 0;
|
|
10653
10917
|
if (effIndependent && !rowIndependent && isCurrent(effective))
|
|
10654
10918
|
continue;
|
|
10655
10919
|
if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
|
|
@@ -10663,7 +10927,7 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10663
10927
|
return false;
|
|
10664
10928
|
const edges = new Map;
|
|
10665
10929
|
for (const row of rows)
|
|
10666
|
-
if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
|
|
10930
|
+
if (row.type === "stack.restacked" && row.observer === "workit_cli" && row.integrity === "verified" && row.branch === branch && row.patchEqual === true && typeof row.fromHead === "string" && row.head)
|
|
10667
10931
|
edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
|
|
10668
10932
|
const seen = new Set([from]);
|
|
10669
10933
|
const queue = [from];
|
|
@@ -10679,7 +10943,8 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10679
10943
|
}
|
|
10680
10944
|
return false;
|
|
10681
10945
|
}
|
|
10682
|
-
function checkVerdicts(cwd, branch,
|
|
10946
|
+
function checkVerdicts(cwd, branch, ledgerRows) {
|
|
10947
|
+
const rows = withIntegrity(cwd, ledgerRows);
|
|
10683
10948
|
const head = branchHead(cwd, branch);
|
|
10684
10949
|
const fallbackBase = defaultBase(cwd);
|
|
10685
10950
|
const scope = branchScope(cwd, branch);
|
|
@@ -10709,21 +10974,47 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10709
10974
|
return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
|
|
10710
10975
|
};
|
|
10711
10976
|
const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
|
|
10977
|
+
const failureAuthorCache = new Map;
|
|
10978
|
+
const failureAuthors = (row) => {
|
|
10979
|
+
let set = failureAuthorCache.get(row.seq);
|
|
10980
|
+
if (!set) {
|
|
10981
|
+
set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
|
|
10982
|
+
failureAuthorCache.set(row.seq, set);
|
|
10983
|
+
}
|
|
10984
|
+
return set;
|
|
10985
|
+
};
|
|
10986
|
+
const reasonCache = new Map;
|
|
10987
|
+
const reasonsOf = (row) => {
|
|
10988
|
+
let reasons = reasonCache.get(row);
|
|
10989
|
+
if (!reasons) {
|
|
10990
|
+
const failing = FAILING.has(String(row.result));
|
|
10991
|
+
reasons = [
|
|
10992
|
+
...trustReasons(cwd, row),
|
|
10993
|
+
...independenceReasons(row, failing ? failureAuthors(row) : authors)
|
|
10994
|
+
];
|
|
10995
|
+
reasonCache.set(row, reasons);
|
|
10996
|
+
}
|
|
10997
|
+
return reasons;
|
|
10998
|
+
};
|
|
10999
|
+
const retired = new Set;
|
|
11000
|
+
for (const row of rows)
|
|
11001
|
+
if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
|
|
11002
|
+
retired.add(row.supersedes);
|
|
10712
11003
|
const byKind = new Map;
|
|
10713
11004
|
for (const row of rows)
|
|
10714
|
-
if (isLiveVerdict(row) && scope(row)) {
|
|
11005
|
+
if (isLiveVerdict(row, retired) && scope(row)) {
|
|
10715
11006
|
const kind = typeof row.kind === "string" ? row.kind : "review";
|
|
10716
11007
|
byKind.set(kind, [...byKind.get(kind) ?? [], row]);
|
|
10717
11008
|
}
|
|
10718
11009
|
const verdicts = [];
|
|
10719
11010
|
const warnings = [];
|
|
10720
11011
|
for (const [kind, list] of byKind) {
|
|
10721
|
-
const row = effectiveOf(list,
|
|
11012
|
+
const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
|
|
10722
11013
|
if (!row)
|
|
10723
11014
|
continue;
|
|
10724
11015
|
const basis = basisOf(row);
|
|
10725
11016
|
const current = basis !== "stale";
|
|
10726
|
-
const independence =
|
|
11017
|
+
const independence = reasonsOf(row);
|
|
10727
11018
|
const reasons = [
|
|
10728
11019
|
...current ? [] : ["stale"],
|
|
10729
11020
|
...PASSING.has(String(row.result)) ? [] : ["not_passing"],
|
|
@@ -10740,23 +11031,31 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10740
11031
|
});
|
|
10741
11032
|
if (reasons.length === 0) {
|
|
10742
11033
|
for (const later of list)
|
|
10743
|
-
if (later.seq > row.seq &&
|
|
11034
|
+
if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
|
|
10744
11035
|
warnings.push(`the author's own (self) ${String(later.result)} ${kind} verdict ${later.id} by ${later.actor.session ?? "no session"} on the current code is newer than the accepted pass ${row.id}: read it before merging`);
|
|
10745
11036
|
}
|
|
10746
11037
|
}
|
|
10747
11038
|
for (const row of rows)
|
|
10748
11039
|
if (row.integrity === "unverified" && scope(row))
|
|
10749
11040
|
warnings.push(`ledger row ${row.id} (${row.type}) on ${row.branch ?? branch} is unverified: the workit CLI did not write it or it changed afterwards (workit ledger verify-integrity)`);
|
|
11041
|
+
const tampered = ledgerTampering(cwd);
|
|
11042
|
+
if (tampered.length) {
|
|
11043
|
+
warnings.push(tamperedWarning(cwd, tampered));
|
|
11044
|
+
for (const entry of verdicts) {
|
|
11045
|
+
entry.accepted = false;
|
|
11046
|
+
entry.reasons = [...entry.reasons, "ledger_tampered"];
|
|
11047
|
+
}
|
|
11048
|
+
}
|
|
10750
11049
|
verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
|
|
10751
11050
|
const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
|
|
10752
11051
|
const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
|
|
10753
11052
|
const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
|
|
10754
|
-
const accepted = newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
|
|
11053
|
+
const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
|
|
10755
11054
|
accepted: false,
|
|
10756
11055
|
verdict: failing?.verdict ?? null,
|
|
10757
11056
|
reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
|
|
10758
11057
|
};
|
|
10759
|
-
const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
|
|
11058
|
+
const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
|
|
10760
11059
|
return {
|
|
10761
11060
|
branch,
|
|
10762
11061
|
head,
|
|
@@ -10777,7 +11076,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10777
11076
|
error,
|
|
10778
11077
|
...unblock ? { unblock } : {}
|
|
10779
11078
|
}), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
10780
|
-
const run =
|
|
11079
|
+
const run = spawnSync8("git", args, {
|
|
10781
11080
|
cwd,
|
|
10782
11081
|
encoding: "utf8",
|
|
10783
11082
|
timeout: timeoutMs,
|
|
@@ -10811,24 +11110,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10811
11110
|
} catch {
|
|
10812
11111
|
return null;
|
|
10813
11112
|
}
|
|
10814
|
-
}, newId2 = () => `${Date.now().toString(36)}-${
|
|
11113
|
+
}, newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
|
|
10815
11114
|
if (process.platform !== "linux")
|
|
10816
11115
|
return false;
|
|
10817
11116
|
try {
|
|
10818
|
-
return isNetworkFsType(
|
|
11117
|
+
return isNetworkFsType(fs6.statfsSync(dir).type);
|
|
10819
11118
|
} catch {
|
|
10820
11119
|
return false;
|
|
10821
11120
|
}
|
|
10822
11121
|
}, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
|
|
10823
11122
|
try {
|
|
10824
|
-
return
|
|
11123
|
+
return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
|
|
10825
11124
|
} catch {
|
|
10826
11125
|
return null;
|
|
10827
11126
|
}
|
|
10828
11127
|
}, lockAgeMs = (lock) => {
|
|
10829
11128
|
for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
|
|
10830
11129
|
try {
|
|
10831
|
-
return Date.now() -
|
|
11130
|
+
return Date.now() - fs6.statSync(target).mtimeMs;
|
|
10832
11131
|
} catch {}
|
|
10833
11132
|
}
|
|
10834
11133
|
return null;
|
|
@@ -10836,7 +11135,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10836
11135
|
${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
10837
11136
|
for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
|
|
10838
11137
|
const buffer = Buffer.alloc(span);
|
|
10839
|
-
|
|
11138
|
+
fs6.readSync(fd, buffer, 0, span, size - span);
|
|
10840
11139
|
const end = buffer.lastIndexOf(10);
|
|
10841
11140
|
if (end < 0) {
|
|
10842
11141
|
if (span === size)
|
|
@@ -10864,6 +11163,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10864
11163
|
return text || null;
|
|
10865
11164
|
}
|
|
10866
11165
|
return null;
|
|
11166
|
+
}, tighten = (fd) => {
|
|
11167
|
+
try {
|
|
11168
|
+
if ((fs6.fstatSync(fd).mode & 63) !== 0)
|
|
11169
|
+
fs6.fchmodSync(fd, 384);
|
|
11170
|
+
} catch {}
|
|
10867
11171
|
}, sealed = (row, id, at, previous) => {
|
|
10868
11172
|
const { prevHash: _prev, rowHash: _row, ...fields } = row;
|
|
10869
11173
|
const prevHash = previous === null ? null : sha2562(previous);
|
|
@@ -10878,7 +11182,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10878
11182
|
session: str(actor.session),
|
|
10879
11183
|
agentId: str(actor.agentId)
|
|
10880
11184
|
};
|
|
10881
|
-
}, isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.
|
|
11185
|
+
}, TAMPER_REASONS, tamperCache, fileStamp = (stat) => `${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}`, tamperedWarning = (cwd, breaks) => `the ledger was edited, or a row was removed or moved (${breaks.slice(0, 3).map((row) => `${row.reason} at row ${row.seq}`).join(", ")}${breaks.length > 3 ? ", …" : ""}): no verdict is accepted until it is repaired. ${tamperedUnblock(cwd)}`, isLiveVerdict = (row, retired) => row.type === "verdict" && typeof row.result === "string" && !retired.has(row.id), RENAMED, renameAwareBranch = (scopeOf, branch) => {
|
|
10882
11186
|
return (target) => {
|
|
10883
11187
|
const own = typeof branch === "string" ? branch : null;
|
|
10884
11188
|
const theirs = typeof target.branch === "string" ? target.branch : null;
|
|
@@ -10895,7 +11199,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10895
11199
|
if (derivesFrom(author, session) || derivesFrom(session, author))
|
|
10896
11200
|
return true;
|
|
10897
11201
|
return false;
|
|
10898
|
-
}, independenceReasons = (row, authors) => {
|
|
11202
|
+
}, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
|
|
10899
11203
|
const reasons = [];
|
|
10900
11204
|
if (!row.actor.session)
|
|
10901
11205
|
reasons.push("no_session");
|
|
@@ -10906,6 +11210,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10906
11210
|
return reasons;
|
|
10907
11211
|
}, OBSERVED_ONLY;
|
|
10908
11212
|
var init_ledger = __esm(() => {
|
|
11213
|
+
init_store_lock();
|
|
10909
11214
|
init_vcs_config();
|
|
10910
11215
|
init_rev();
|
|
10911
11216
|
init_host_session();
|
|
@@ -10929,56 +11234,73 @@ var init_ledger = __esm(() => {
|
|
|
10929
11234
|
ledgerLock = {
|
|
10930
11235
|
acquire(lock) {
|
|
10931
11236
|
try {
|
|
10932
|
-
|
|
11237
|
+
fs6.mkdirSync(lock);
|
|
10933
11238
|
} catch (error) {
|
|
10934
11239
|
if (error.code === "EEXIST")
|
|
10935
11240
|
return null;
|
|
10936
11241
|
throw error;
|
|
10937
11242
|
}
|
|
10938
|
-
const token = `${process.pid}-${
|
|
10939
|
-
|
|
11243
|
+
const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
|
|
11244
|
+
fs6.writeFileSync(path8.join(lock, LOCK_OWNER), token);
|
|
10940
11245
|
return token;
|
|
10941
11246
|
},
|
|
10942
11247
|
release(lock, token) {
|
|
10943
11248
|
if (lockToken(lock) !== token)
|
|
10944
11249
|
return false;
|
|
10945
|
-
const tomb = `${lock}.release-${
|
|
11250
|
+
const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
|
|
10946
11251
|
try {
|
|
10947
|
-
|
|
11252
|
+
fs6.renameSync(lock, tomb);
|
|
10948
11253
|
} catch {
|
|
10949
11254
|
return false;
|
|
10950
11255
|
}
|
|
10951
11256
|
if (lockToken(tomb) !== token) {
|
|
10952
11257
|
try {
|
|
10953
|
-
|
|
11258
|
+
fs6.renameSync(tomb, lock);
|
|
10954
11259
|
} catch {}
|
|
10955
11260
|
return false;
|
|
10956
11261
|
}
|
|
10957
|
-
|
|
11262
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10958
11263
|
return true;
|
|
10959
11264
|
},
|
|
10960
11265
|
token: lockToken,
|
|
10961
11266
|
reap(lock, observed) {
|
|
10962
|
-
const tomb = `${lock}.reap-${
|
|
11267
|
+
const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
|
|
10963
11268
|
try {
|
|
10964
|
-
|
|
11269
|
+
fs6.renameSync(lock, tomb);
|
|
10965
11270
|
} catch {
|
|
10966
11271
|
return false;
|
|
10967
11272
|
}
|
|
10968
11273
|
if (lockToken(tomb) === observed) {
|
|
10969
|
-
|
|
11274
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10970
11275
|
return true;
|
|
10971
11276
|
}
|
|
10972
11277
|
try {
|
|
10973
|
-
|
|
11278
|
+
fs6.renameSync(tomb, lock);
|
|
10974
11279
|
} catch {
|
|
10975
|
-
|
|
11280
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10976
11281
|
}
|
|
10977
11282
|
return false;
|
|
10978
11283
|
}
|
|
10979
11284
|
};
|
|
10980
11285
|
TAIL_BYTES = 64 * 1024;
|
|
11286
|
+
TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
|
|
11287
|
+
tamperCache = new Map;
|
|
10981
11288
|
RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
|
|
11289
|
+
TRUST_REASONS = new Set([
|
|
11290
|
+
"unsigned",
|
|
11291
|
+
"hash_mismatch",
|
|
11292
|
+
"broken_link",
|
|
11293
|
+
"duplicate_id",
|
|
11294
|
+
"unissued_session",
|
|
11295
|
+
"unbound_session"
|
|
11296
|
+
]);
|
|
11297
|
+
SHELL_VOUCHED_HOSTS = new Set([
|
|
11298
|
+
"cli",
|
|
11299
|
+
"opencode",
|
|
11300
|
+
"pi",
|
|
11301
|
+
"codex_cli",
|
|
11302
|
+
"codex_desktop"
|
|
11303
|
+
]);
|
|
10982
11304
|
OBSERVED_ONLY = new Set([
|
|
10983
11305
|
"check",
|
|
10984
11306
|
"merge.unverified",
|
|
@@ -11928,7 +12250,7 @@ function normalizeMaxBytes(value, options = {}) {
|
|
|
11928
12250
|
}
|
|
11929
12251
|
|
|
11930
12252
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
11931
|
-
import
|
|
12253
|
+
import fs7 from "node:fs";
|
|
11932
12254
|
function createScratchBuffer(maxBytes) {
|
|
11933
12255
|
const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
|
|
11934
12256
|
return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
|
|
@@ -11951,7 +12273,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
11951
12273
|
let total = 0;
|
|
11952
12274
|
while (true) {
|
|
11953
12275
|
const length = nextReadLength(total, maxBytes, scratch.length);
|
|
11954
|
-
const bytesRead =
|
|
12276
|
+
const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
|
|
11955
12277
|
if (bytesRead === 0) {
|
|
11956
12278
|
return Buffer.concat(chunks, total);
|
|
11957
12279
|
}
|
|
@@ -11965,9 +12287,9 @@ var init_bounded_read = __esm(() => {
|
|
|
11965
12287
|
});
|
|
11966
12288
|
|
|
11967
12289
|
// node_modules/@openclaw/fs-safe/dist/read-open-flags.js
|
|
11968
|
-
import
|
|
12290
|
+
import fs8 from "node:fs";
|
|
11969
12291
|
function resolveReadOpenFlags(options) {
|
|
11970
|
-
const constants = options?.constants ??
|
|
12292
|
+
const constants = options?.constants ?? fs8.constants;
|
|
11971
12293
|
const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
11972
12294
|
const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
|
|
11973
12295
|
return constants.O_RDONLY | noFollow | nonBlocking;
|
|
@@ -11975,7 +12297,7 @@ function resolveReadOpenFlags(options) {
|
|
|
11975
12297
|
var init_read_open_flags = () => {};
|
|
11976
12298
|
|
|
11977
12299
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
11978
|
-
import { randomBytes as
|
|
12300
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
11979
12301
|
import fsSync from "node:fs";
|
|
11980
12302
|
import path11 from "node:path";
|
|
11981
12303
|
function parseSidecarLockSnapshot(snapshot, parser) {
|
|
@@ -11983,7 +12305,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
|
|
|
11983
12305
|
}
|
|
11984
12306
|
function createSidecarLockOwnershipToken() {
|
|
11985
12307
|
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
11986
|
-
for (const byte of
|
|
12308
|
+
for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
11987
12309
|
for (let bit = 7;bit >= 0; bit -= 1) {
|
|
11988
12310
|
token += byte & 1 << bit ? "\t" : " ";
|
|
11989
12311
|
}
|
|
@@ -12180,7 +12502,7 @@ function sleepSync(ms) {
|
|
|
12180
12502
|
}
|
|
12181
12503
|
|
|
12182
12504
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
12183
|
-
import
|
|
12505
|
+
import fs9 from "node:fs";
|
|
12184
12506
|
import path12 from "node:path";
|
|
12185
12507
|
function getSyncHeldLocks() {
|
|
12186
12508
|
const globalWithState = globalThis;
|
|
@@ -12198,7 +12520,7 @@ function releaseAllSyncHeldLocks() {
|
|
|
12198
12520
|
}
|
|
12199
12521
|
try {
|
|
12200
12522
|
if (held.fd !== undefined)
|
|
12201
|
-
|
|
12523
|
+
fs9.closeSync(held.fd);
|
|
12202
12524
|
} catch {}
|
|
12203
12525
|
try {
|
|
12204
12526
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
@@ -12233,7 +12555,7 @@ function releaseSyncHeldLock(held) {
|
|
|
12233
12555
|
if (held.fd !== undefined) {
|
|
12234
12556
|
const fd = held.fd;
|
|
12235
12557
|
held.fd = undefined;
|
|
12236
|
-
|
|
12558
|
+
fs9.closeSync(fd);
|
|
12237
12559
|
}
|
|
12238
12560
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
12239
12561
|
heldLocks.delete(held.normalizedTargetPath);
|
|
@@ -12256,11 +12578,11 @@ function createSyncHeldLockHandle(held) {
|
|
|
12256
12578
|
};
|
|
12257
12579
|
}
|
|
12258
12580
|
function canonicalLockParentSync(parent) {
|
|
12259
|
-
return process.platform === "win32" ?
|
|
12581
|
+
return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
|
|
12260
12582
|
}
|
|
12261
12583
|
function normalizeTargetPath(targetPath) {
|
|
12262
12584
|
const resolved = path12.resolve(targetPath);
|
|
12263
|
-
|
|
12585
|
+
fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
|
|
12264
12586
|
try {
|
|
12265
12587
|
return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
|
|
12266
12588
|
} catch {
|
|
@@ -12288,7 +12610,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
|
12288
12610
|
}
|
|
12289
12611
|
function reclaimGuardExists(reclaimGuardPath) {
|
|
12290
12612
|
try {
|
|
12291
|
-
|
|
12613
|
+
fs9.lstatSync(reclaimGuardPath);
|
|
12292
12614
|
return true;
|
|
12293
12615
|
} catch (error) {
|
|
12294
12616
|
if (error.code === "ENOENT")
|
|
@@ -12352,19 +12674,19 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12352
12674
|
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
12353
12675
|
let lockFileCreateDenied = false;
|
|
12354
12676
|
try {
|
|
12355
|
-
const noFollow = process.platform !== "win32" && typeof
|
|
12677
|
+
const noFollow = process.platform !== "win32" && typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
|
|
12356
12678
|
try {
|
|
12357
|
-
fd =
|
|
12679
|
+
fd = fs9.openSync(lockPath, fs9.constants.O_WRONLY | fs9.constants.O_CREAT | fs9.constants.O_EXCL | noFollow, 384);
|
|
12358
12680
|
} catch (error) {
|
|
12359
12681
|
lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
|
|
12360
12682
|
throw error;
|
|
12361
12683
|
}
|
|
12362
|
-
|
|
12363
|
-
|
|
12684
|
+
fs9.writeFileSync(fd, raw, "utf8");
|
|
12685
|
+
fs9.fsyncSync(fd);
|
|
12364
12686
|
const snapshot = {
|
|
12365
12687
|
raw,
|
|
12366
12688
|
payload,
|
|
12367
|
-
stat:
|
|
12689
|
+
stat: fs9.fstatSync(fd),
|
|
12368
12690
|
ownershipToken
|
|
12369
12691
|
};
|
|
12370
12692
|
const createdHeld = {
|
|
@@ -12399,8 +12721,8 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12399
12721
|
return returnedHandle;
|
|
12400
12722
|
} catch (error) {
|
|
12401
12723
|
if (fd !== undefined) {
|
|
12402
|
-
const failed = { payload: null, stat:
|
|
12403
|
-
|
|
12724
|
+
const failed = { payload: null, stat: fs9.fstatSync(fd) };
|
|
12725
|
+
fs9.closeSync(fd);
|
|
12404
12726
|
fd = undefined;
|
|
12405
12727
|
removeSidecarLockIfUnchangedSync(lockPath, failed);
|
|
12406
12728
|
}
|
|
@@ -12449,7 +12771,7 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12449
12771
|
})) {
|
|
12450
12772
|
let ownsReclaimGuard = false;
|
|
12451
12773
|
try {
|
|
12452
|
-
|
|
12774
|
+
fs9.mkdirSync(reclaimGuardPath);
|
|
12453
12775
|
ownsReclaimGuard = true;
|
|
12454
12776
|
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
12455
12777
|
continue;
|
|
@@ -12462,7 +12784,7 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12462
12784
|
} finally {
|
|
12463
12785
|
if (ownsReclaimGuard) {
|
|
12464
12786
|
try {
|
|
12465
|
-
|
|
12787
|
+
fs9.rmdirSync(reclaimGuardPath);
|
|
12466
12788
|
} catch {}
|
|
12467
12789
|
}
|
|
12468
12790
|
}
|
|
@@ -12508,137 +12830,6 @@ var packageRoot = () => {
|
|
|
12508
12830
|
};
|
|
12509
12831
|
var init_package_root = () => {};
|
|
12510
12832
|
|
|
12511
|
-
// packages/workit-core/src/core/store-lock.ts
|
|
12512
|
-
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
12513
|
-
import fs9 from "node:fs";
|
|
12514
|
-
import { hostname } from "node:os";
|
|
12515
|
-
var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
|
|
12516
|
-
let value;
|
|
12517
|
-
try {
|
|
12518
|
-
value = JSON.parse(raw);
|
|
12519
|
-
} catch {
|
|
12520
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
12521
|
-
}
|
|
12522
|
-
const parsed = metadataLockSchema.safeParse(value);
|
|
12523
|
-
if (!parsed.success)
|
|
12524
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
12525
|
-
return parsed.data;
|
|
12526
|
-
}, parseMetadataLockOrNull = (raw) => {
|
|
12527
|
-
try {
|
|
12528
|
-
return parseMetadataLock(raw);
|
|
12529
|
-
} catch {
|
|
12530
|
-
return null;
|
|
12531
|
-
}
|
|
12532
|
-
}, parseProcStatStart = (stat) => {
|
|
12533
|
-
const close = stat.lastIndexOf(")");
|
|
12534
|
-
if (close < 0)
|
|
12535
|
-
return null;
|
|
12536
|
-
return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
|
|
12537
|
-
}, processStartOf = (pid) => {
|
|
12538
|
-
if (process.platform === "linux") {
|
|
12539
|
-
try {
|
|
12540
|
-
return parseProcStatStart(fs9.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
12541
|
-
} catch {
|
|
12542
|
-
return null;
|
|
12543
|
-
}
|
|
12544
|
-
}
|
|
12545
|
-
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
12546
|
-
try {
|
|
12547
|
-
const run = spawnSync8("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
12548
|
-
encoding: "utf8",
|
|
12549
|
-
timeout: 1000
|
|
12550
|
-
});
|
|
12551
|
-
const value = run.status === 0 ? run.stdout.trim() : "";
|
|
12552
|
-
return value || null;
|
|
12553
|
-
} catch {
|
|
12554
|
-
return null;
|
|
12555
|
-
}
|
|
12556
|
-
}
|
|
12557
|
-
return null;
|
|
12558
|
-
}, readTrimmed = (file) => {
|
|
12559
|
-
try {
|
|
12560
|
-
return fs9.readFileSync(file, "utf8").trim() || null;
|
|
12561
|
-
} catch {
|
|
12562
|
-
return null;
|
|
12563
|
-
}
|
|
12564
|
-
}, cachedLockHost = null, localLockHost = () => {
|
|
12565
|
-
if (cachedLockHost !== null)
|
|
12566
|
-
return cachedLockHost;
|
|
12567
|
-
let pidns = null;
|
|
12568
|
-
try {
|
|
12569
|
-
pidns = /\[(\d+)\]/.exec(fs9.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
12570
|
-
} catch {}
|
|
12571
|
-
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
12572
|
-
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
12573
|
-
return cachedLockHost;
|
|
12574
|
-
}, pidAlive = (pid) => {
|
|
12575
|
-
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
12576
|
-
return false;
|
|
12577
|
-
try {
|
|
12578
|
-
process.kill(pid, 0);
|
|
12579
|
-
return true;
|
|
12580
|
-
} catch (error) {
|
|
12581
|
-
return error.code === "EPERM";
|
|
12582
|
-
}
|
|
12583
|
-
}, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
|
|
12584
|
-
const lock = metadataLockSchema.safeParse(payload);
|
|
12585
|
-
if (!lock.success)
|
|
12586
|
-
return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
|
|
12587
|
-
const { pid, processStart, host } = lock.data;
|
|
12588
|
-
const [lockName, lockSpace] = host.split("#");
|
|
12589
|
-
const [localName, localSpace] = localHost.split("#");
|
|
12590
|
-
if (lockSpace && localSpace && lockName === localName) {
|
|
12591
|
-
const [lockNs, lockBoot] = lockSpace.split(":");
|
|
12592
|
-
const [localNs, localBoot] = localSpace.split(":");
|
|
12593
|
-
if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
|
|
12594
|
-
return { state: "stale", reason: "lock was taken before this machine rebooted" };
|
|
12595
|
-
}
|
|
12596
|
-
if (host !== localHost)
|
|
12597
|
-
return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
|
|
12598
|
-
if (!pidAlive(pid))
|
|
12599
|
-
return { state: "stale", reason: `pid ${pid} is not running` };
|
|
12600
|
-
const currentStart = processStartOf(pid);
|
|
12601
|
-
if (processStart !== null && currentStart !== null && processStart !== currentStart)
|
|
12602
|
-
return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
|
|
12603
|
-
if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
|
|
12604
|
-
return {
|
|
12605
|
-
state: "stale",
|
|
12606
|
-
reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
|
|
12607
|
-
};
|
|
12608
|
-
return { state: "live", reason: `held by running pid ${pid}` };
|
|
12609
|
-
}, ageOf = (file, nowMs) => {
|
|
12610
|
-
try {
|
|
12611
|
-
return nowMs - fs9.lstatSync(file).mtimeMs;
|
|
12612
|
-
} catch {
|
|
12613
|
-
return null;
|
|
12614
|
-
}
|
|
12615
|
-
}, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
12616
|
-
const guard = `${lockPath}.reclaim`;
|
|
12617
|
-
const age = ageOf(guard, nowMs);
|
|
12618
|
-
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
12619
|
-
return false;
|
|
12620
|
-
try {
|
|
12621
|
-
fs9.rmdirSync(guard);
|
|
12622
|
-
return true;
|
|
12623
|
-
} catch {
|
|
12624
|
-
return false;
|
|
12625
|
-
}
|
|
12626
|
-
};
|
|
12627
|
-
var init_store_lock = __esm(() => {
|
|
12628
|
-
init_zod();
|
|
12629
|
-
init_task_contract();
|
|
12630
|
-
init_paths();
|
|
12631
|
-
metadataLockSchema = object({
|
|
12632
|
-
pid: number2().int().nonnegative().safe(),
|
|
12633
|
-
processStart: string2().nullable(),
|
|
12634
|
-
host: string2().min(1),
|
|
12635
|
-
nonce: string2().min(1),
|
|
12636
|
-
externalAction: literal(true).optional()
|
|
12637
|
-
}).strict();
|
|
12638
|
-
FOREIGN_LOCK_TTL_MS = 10 * 60000;
|
|
12639
|
-
TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
|
|
12640
|
-
});
|
|
12641
|
-
|
|
12642
12833
|
// packages/workit-core/src/store/event-log.ts
|
|
12643
12834
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
12644
12835
|
import fs10 from "node:fs";
|
|
@@ -15650,11 +15841,11 @@ what and whether, the CLI does how and records what it observed.
|
|
|
15650
15841
|
Native host allow/ask/deny, sandbox and org rules stay authoritative. Never
|
|
15651
15842
|
switch paths to evade a denial, never claim enforcement a host cannot provide,
|
|
15652
15843
|
and never fabricate an approval or verdict. A question answer is not
|
|
15653
|
-
host permission, and imported decisions or handoffs
|
|
15844
|
+
host permission, and imported decisions or handoffs grant no authority.
|
|
15654
15845
|
|
|
15655
15846
|
Autonomy contract:
|
|
15656
15847
|
- Continue to the requested endpoint (commit, push, PR, green CI, verified,
|
|
15657
|
-
merged) without asking "continue?", running checks and repairs
|
|
15848
|
+
merged) without asking "continue?", running checks and repairs.
|
|
15658
15849
|
Stop only for a new consequential choice, a host denial, a conflicting edit
|
|
15659
15850
|
or an unresolved blocker; report open gaps and uncertain workers.
|
|
15660
15851
|
- No endpoint named: stop at a local commit on a policy-compliant branch and
|
|
@@ -15664,7 +15855,8 @@ Autonomy contract:
|
|
|
15664
15855
|
never merging; \`merged\` also lands it with \`workit pr merge\`.
|
|
15665
15856
|
Merge and release need a workspace grant only the user raises from their
|
|
15666
15857
|
own terminal (a blocked verb names how); never edit workspaces.json or route
|
|
15667
|
-
around
|
|
15858
|
+
around grant_required. Release: \`workit release start <v>\`, commits,
|
|
15859
|
+
\`finish\`; \`publish\` if asked.
|
|
15668
15860
|
- Ask only for a product or preference choice, or for authority you lack, with
|
|
15669
15861
|
your recommended answer. Facts are yours: read, run or prototype.
|
|
15670
15862
|
- Label claims measured (run and seen this session), inferred, or guess.
|
|
@@ -15679,22 +15871,22 @@ Autonomy contract:
|
|
|
15679
15871
|
reconcile every requested item and observe it (\`workit verify-delivery\`).
|
|
15680
15872
|
|
|
15681
15873
|
CLI first: if a step has one right answer, use the \`workit\` verb instead of
|
|
15682
|
-
hand-running git, gh or glab (\`workit git|pr|ci|check\`;
|
|
15683
|
-
|
|
15874
|
+
hand-running git, gh or glab (\`workit git|pr|ci|check|help\`;
|
|
15875
|
+
else \`npx -y @brainervirus/workit-cli\`).
|
|
15684
15876
|
Add Co-Authored-By via \`workit git commit --trailer "K: v" -- <paths>\`, never
|
|
15685
15877
|
an amend.
|
|
15686
15878
|
The task families (task, policy, evidence, finding, decision, worker, state)
|
|
15687
15879
|
are optional continuity for tracked work; each branch has one implicit task,
|
|
15688
|
-
and a solo edit needs no task. A decision
|
|
15689
|
-
|
|
15880
|
+
and a solo edit needs no task. A decision records a user choice; it never
|
|
15881
|
+
authorizes an effect. Judge work once with
|
|
15690
15882
|
\`workit policy assess --judge risk= behavior= product-choice= plan=\`.
|
|
15691
|
-
Across repositories, bind
|
|
15692
|
-
|
|
15693
|
-
external effect before
|
|
15883
|
+
Across repositories, bind items to checkout, branch and endpoint, resolve
|
|
15884
|
+
competing targets before a mutation, and reconcile an uncertain
|
|
15885
|
+
external effect before a retry.
|
|
15694
15886
|
|
|
15695
15887
|
Mid-task: never silently drop or resume an objective (workit-continue).
|
|
15696
15888
|
|
|
15697
|
-
Skills: load the skill, never act from memory
|
|
15889
|
+
Skills: load the skill, never act from memory (aliases /wk-<name>).
|
|
15698
15890
|
- brainstorm, plan, spec, grill, should we: workit-shape
|
|
15699
15891
|
- implement, build, add a feature: workit-implement
|
|
15700
15892
|
- review, blast radius: workit-review
|
|
@@ -19285,7 +19477,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
|
|
|
19285
19477
|
return `workit: raw ${quote(action.via === "api" ? `${action.forge} api` : action.forge === "gh" ? "gh pr merge" : "glab mr merge")}${action.via === "api" ? " merge" : ""} bypasses the merge grant and the verdict gate. Run ${quote("workit pr merge")} instead (it checks both; ${quote("workit help pr")}).`;
|
|
19286
19478
|
const plainPush = action.kind === "push" && !action.blindForce && !action.deletes;
|
|
19287
19479
|
if (dir && branch && plainPush && releaseBranch(dir, branch))
|
|
19288
|
-
return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release
|
|
19480
|
+
return `workit: raw ${quote("git push")} onto ${branch}, a protected release branch, bypasses workit's protected-branch check. To finish a release or hotfix, push it with ${quote(`workit git push --release ${branch} [--release ${releaseTagPrefixOf(dir)}<version>]`)}: it pushes ${branch} (fast-forward only) and the release tag, verifies the remote and records it, under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}). Feature work goes through ${quote("workit git push")} and ${quote("workit pr merge")}.`;
|
|
19289
19481
|
return `workit: raw ${quote("git push")}${action.kind === "push" && action.blindForce ? " --force" : ""} onto ${branch}, a protected branch or the default target, bypasses workit's protected-branch check. Push your feature branch with ${quote("workit git push")} (${quote("workit git push --force-with-lease")} to rewrite it) and land it with ${quote("workit pr merge")}.`;
|
|
19290
19482
|
}, refFacts = (repo, remote) => {
|
|
19291
19483
|
const facts = { heads: new Set, tags: new Set, remoteHeads: new Set };
|
|
@@ -19365,15 +19557,16 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
|
|
|
19365
19557
|
return bare;
|
|
19366
19558
|
}
|
|
19367
19559
|
return null;
|
|
19368
|
-
}, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts) => {
|
|
19560
|
+
}, isTag = (facts, name) => facts.tags.has(name) && !facts.heads.has(name), tagRefusal = (action, facts, dir) => {
|
|
19369
19561
|
const tags = [
|
|
19370
19562
|
...action.tags ?? [],
|
|
19371
19563
|
...action.targets.filter((name) => name !== null && isTag(facts, name))
|
|
19372
19564
|
];
|
|
19373
19565
|
if (!tags.length && !action.allTags)
|
|
19374
19566
|
return null;
|
|
19375
|
-
const
|
|
19376
|
-
|
|
19567
|
+
const shape = `${releaseTagPrefixOf(dir)}<version>`;
|
|
19568
|
+
const named = tags.length ? tags.join(" --release ") : shape;
|
|
19569
|
+
return `workit: raw ${quote(`git push${action.allTags ? " --tags" : ""}`)} of ${tags.length ? `tag ${tags.join(", ")}` : "tags"} bypasses the release grant. Push a release tag with ${quote(`workit git push --release ${named}`)} (${shape} tags whose commit the production branch already has; a tag never moves), under the ${quote("release")} grant (if refused, ask the user to run ${quote("workit grant set <workspace> release=true")}).`;
|
|
19377
19570
|
}, remoteHeads = (repo, remote) => {
|
|
19378
19571
|
const listed = spawnSync11("git", ["ls-remote", "--heads", remote], {
|
|
19379
19572
|
cwd: repo.dir,
|
|
@@ -19435,7 +19628,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
|
|
|
19435
19628
|
if (guarded)
|
|
19436
19629
|
return refusal(action, guarded, repo.dir);
|
|
19437
19630
|
}
|
|
19438
|
-
return tagRefusal(pushed, facts) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
|
|
19631
|
+
return tagRefusal(pushed, facts, repo.dir) ?? (action.deletes ? null : offPolicyPush(pushed, repo, facts, remote));
|
|
19439
19632
|
}, actionsIn = (cwd, command, dialect, cache = new Map) => {
|
|
19440
19633
|
const out = [];
|
|
19441
19634
|
const repos = new Map;
|
|
@@ -19539,6 +19732,7 @@ var NONE2, SHELLS, POWERSHELLS, EVALS, WRAPPERS, WRAPPER_VALUES, KEYWORDS, DECLA
|
|
|
19539
19732
|
};
|
|
19540
19733
|
var init_raw_git = __esm(() => {
|
|
19541
19734
|
init_branch();
|
|
19735
|
+
init_branch_policy();
|
|
19542
19736
|
init_workspaces();
|
|
19543
19737
|
init_ledger();
|
|
19544
19738
|
init_paths();
|
|
@@ -20045,10 +20239,10 @@ function stopObligation(input, descriptor) {
|
|
|
20045
20239
|
const branch = currentBranch(input.cwd);
|
|
20046
20240
|
if (!branch || guardedBranch(input.cwd, branch))
|
|
20047
20241
|
return null;
|
|
20048
|
-
const ledger = readLedger(input.cwd);
|
|
20242
|
+
const ledger = readLedger(input.cwd, { integrity: true });
|
|
20049
20243
|
if (!ledger.ok)
|
|
20050
20244
|
return null;
|
|
20051
|
-
const all = ledger.value.rows;
|
|
20245
|
+
const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
|
|
20052
20246
|
const rows = all.filter((row) => row.branch === branch);
|
|
20053
20247
|
const mine = rows.filter((row) => sessionOf(row) === session);
|
|
20054
20248
|
const commits = new Set(mine.filter((row) => row.type === "commit.recorded").map((row) => typeof row.sha === "string" ? row.sha : row.head).filter((sha) => typeof sha === "string"));
|
|
@@ -20576,9 +20770,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
|
|
|
20576
20770
|
if (role !== "judge" || !event.agentId)
|
|
20577
20771
|
return readOnly;
|
|
20578
20772
|
const own = subagentSession(session, event.agentId);
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result>
|
|
20773
|
+
const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
|
|
20774
|
+
const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
|
|
20775
|
+
return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result> ${flags} ...\` so the ledger tells it apart from the author.${token ? " The token is this subagent's alone: never write it into a report, a file or a commit. --token needs a workit CLI at least as new as this plugin; if the CLI answers that --token is an unknown option, update the workit CLI (workit upgrade --cli) and record again." : ""}`;
|
|
20582
20776
|
}, failureDecision = (descriptor, event, error) => {
|
|
20583
20777
|
if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
|
|
20584
20778
|
return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
|