@brainervirus/workit-cursor 8.13.0 → 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 +1837 -1654
- package/dist/mcp-server.js +592 -369
- package/dist/workit-hook.js +1215 -1022
- package/package.json +4 -4
- package/skills/workit-fanout/references/orchestration.md +4 -3
- package/skills/workit-review/SKILL.md +1 -1
|
@@ -8638,1565 +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
|
-
|
|
8884
|
-
|
|
8885
|
-
if (
|
|
8886
|
-
|
|
8887
|
-
const
|
|
8888
|
-
|
|
8889
|
-
encoding: "utf8"
|
|
8890
|
-
});
|
|
8891
|
-
return r.status === 0 ? (r.stdout ?? "").trim() : "v";
|
|
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)}`;
|
|
8892
8905
|
};
|
|
8893
|
-
var
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
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
|
+
];
|
|
8901
8915
|
});
|
|
8902
8916
|
|
|
8903
|
-
// packages/workit-core/src/core/
|
|
8917
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
8904
8918
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
8905
|
-
import
|
|
8906
|
-
import {
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
const policyDefault = resolveBranchPolicy(readConfig(), selected.policy ? { branchPolicy: selected.policy } : null, () => repoBranchPreset(cwd)).defaultTargetBranch;
|
|
8914
|
-
return String(ws?.vcs?.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : globalDefault ?? policyDefault) ?? "develop");
|
|
8915
|
-
}
|
|
8916
|
-
var nonBlank, branchPreset, commitPreset, workspaceBranchPolicySchema, workspaceCommitPolicySchema, workspaceProfileSchema, versionSourceSchema, releaseTrackSchema, workspaceConfigSchema, workspacesFileSchema, workspacesRevision = (contents) => createHash3("sha256").update(contents ?? "<missing>").digest("hex"), validateWorkspacesDocument = (parsed, file) => {
|
|
8917
|
-
if (!isConfigObject(parsed)) {
|
|
8918
|
-
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
8919
|
-
}
|
|
8920
|
-
const validated = workspacesFileSchema.safeParse(parsed, { reportInput: true });
|
|
8921
|
-
if (!validated.success) {
|
|
8922
|
-
const detail = validated.error.issues.map((issue) => {
|
|
8923
|
-
const field = String(issue.path[issue.path.length - 1] ?? "");
|
|
8924
|
-
const attempted = ["provider", "preset", "strategy"].includes(field) && typeof issue.input === "string" ? ` (${JSON.stringify(issue.input)})` : "";
|
|
8925
|
-
return `${issue.path.map(String).join(".") || "workspaces"}: ${issue.message}${attempted}`;
|
|
8926
|
-
}).join("; ");
|
|
8927
|
-
return {
|
|
8928
|
-
status: "invalid",
|
|
8929
|
-
path: file,
|
|
8930
|
-
entries: [],
|
|
8931
|
-
error: `${file} has invalid workspace configuration: ${detail}`
|
|
8932
|
-
};
|
|
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" });
|
|
8933
8927
|
}
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
}
|
|
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;
|
|
8944
8937
|
}
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
if (error.code === "ENOENT")
|
|
8958
|
-
return { status: "missing", path: file, entries: [], revision: workspacesRevision(null) };
|
|
8959
|
-
return {
|
|
8960
|
-
status: "malformed",
|
|
8961
|
-
path: file,
|
|
8962
|
-
entries: [],
|
|
8963
|
-
error: `${file} could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
8964
|
-
};
|
|
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;
|
|
8949
|
+
}
|
|
8965
8950
|
}
|
|
8966
|
-
|
|
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
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
return null;
|
|
8964
|
+
}, readTrimmed = (file) => {
|
|
8967
8965
|
try {
|
|
8968
|
-
|
|
8966
|
+
return fs3.readFileSync(file, "utf8").trim() || null;
|
|
8969
8967
|
} catch {
|
|
8970
|
-
return
|
|
8971
|
-
status: "malformed",
|
|
8972
|
-
path: file,
|
|
8973
|
-
entries: [],
|
|
8974
|
-
revision: workspacesRevision(raw),
|
|
8975
|
-
error: `${file} is not valid JSON`
|
|
8976
|
-
};
|
|
8968
|
+
return null;
|
|
8977
8969
|
}
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
out += ".*";
|
|
8997
|
-
}
|
|
8998
|
-
} else {
|
|
8999
|
-
out += ".*";
|
|
9000
|
-
}
|
|
9001
|
-
i++;
|
|
9002
|
-
}
|
|
9003
|
-
} else {
|
|
9004
|
-
out += "[^/]*";
|
|
9005
|
-
}
|
|
9006
|
-
} else {
|
|
9007
|
-
out += c.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
9008
|
-
}
|
|
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";
|
|
9009
8988
|
}
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
const
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
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" };
|
|
9021
9001
|
}
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
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)
|
|
9026
9010
|
return {
|
|
9027
|
-
|
|
9028
|
-
|
|
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)`
|
|
9029
9013
|
};
|
|
9030
|
-
}
|
|
9031
|
-
|
|
9032
|
-
}, WorkspaceConfigError, mainCheckouts, mainCheckoutOf = (cwd) => {
|
|
9033
|
-
const cached = mainCheckouts.get(cwd);
|
|
9034
|
-
if (cached !== undefined)
|
|
9035
|
-
return cached;
|
|
9036
|
-
const env = { ...process.env };
|
|
9037
|
-
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"])
|
|
9038
|
-
delete env[name];
|
|
9039
|
-
const git = (args) => {
|
|
9040
|
-
const run = spawnSync3("git", args, {
|
|
9041
|
-
cwd,
|
|
9042
|
-
env,
|
|
9043
|
-
encoding: "utf8",
|
|
9044
|
-
timeout: 5000,
|
|
9045
|
-
windowsHide: true
|
|
9046
|
-
});
|
|
9047
|
-
return run.status === 0 ? run.stdout : null;
|
|
9048
|
-
};
|
|
9049
|
-
let main = null;
|
|
9050
|
-
const dirs = git(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"]);
|
|
9051
|
-
const [gitDir, common] = (dirs ?? "").trim().split(/\r?\n/u);
|
|
9052
|
-
if (gitDir && common && path3.resolve(gitDir) !== path3.resolve(common)) {
|
|
9053
|
-
const listed = /^worktree (.+)$/mu.exec(git(["worktree", "list", "--porcelain"]) ?? "")?.[1];
|
|
9054
|
-
main = listed?.trim() || (path3.basename(common) === ".git" ? path3.dirname(common) : common);
|
|
9055
|
-
}
|
|
9056
|
-
mainCheckouts.set(cwd, main);
|
|
9057
|
-
return main;
|
|
9058
|
-
}, matchingEntries = (cwd, entries) => {
|
|
9059
|
-
const match = (where) => {
|
|
9060
|
-
const targets = [where, realpathOf(where)].map((value) => value.replaceAll("\\", "/"));
|
|
9061
|
-
return entries.filter((entry) => {
|
|
9062
|
-
const glob = entry.glob.replaceAll("\\", "/");
|
|
9063
|
-
const canonical = canonicalGlob(glob);
|
|
9064
|
-
return targets.some((target) => matchWorkspace(glob, target) || canonical !== glob && matchWorkspace(canonical, target));
|
|
9065
|
-
});
|
|
9066
|
-
};
|
|
9067
|
-
const direct = match(cwd);
|
|
9068
|
-
if (direct.length)
|
|
9069
|
-
return direct;
|
|
9070
|
-
const main = mainCheckoutOf(cwd);
|
|
9071
|
-
return main ? match(main) : direct;
|
|
9072
|
-
}, realpathOf = (p) => {
|
|
9073
|
-
const real = realpathSync.native ?? realpathSync;
|
|
9014
|
+
return { state: "live", reason: `held by running pid ${pid}` };
|
|
9015
|
+
}, ageOf = (file, nowMs) => {
|
|
9074
9016
|
try {
|
|
9075
|
-
return
|
|
9017
|
+
return nowMs - fs3.lstatSync(file).mtimeMs;
|
|
9076
9018
|
} catch {
|
|
9077
|
-
return
|
|
9078
|
-
}
|
|
9079
|
-
}, canonicalGlob = (glob) => {
|
|
9080
|
-
const m = /[*?[\]{]/.exec(glob);
|
|
9081
|
-
const prefix = m ? glob.slice(0, m.index) : glob;
|
|
9082
|
-
const rest = m ? glob.slice(m.index) : "";
|
|
9083
|
-
if (!prefix)
|
|
9084
|
-
return glob;
|
|
9085
|
-
const real = realpathOf(prefix.replace(/\/+$/, "") || "/");
|
|
9086
|
-
return real + (prefix.endsWith("/") ? "/" : "") + rest;
|
|
9087
|
-
}, globSpecificity = (glob) => glob.replaceAll("\\", "/").split("/").filter((segment) => segment.length > 0 && !/[*?[\]{]/.test(segment)).length, selectWorkspaceMatch = (matches, cwd, workspaceName) => {
|
|
9088
|
-
if (workspaceName !== undefined) {
|
|
9089
|
-
const named = matches.filter((entry) => entry.name === workspaceName);
|
|
9090
|
-
if (named.length === 1)
|
|
9091
|
-
return named[0];
|
|
9092
|
-
if (named.length > 1)
|
|
9093
|
-
throw new WorkspaceConfigError(`ambiguous workspace ${JSON.stringify(workspaceName)} for ${cwd}`);
|
|
9094
|
-
if (matches.length > 0) {
|
|
9095
|
-
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}; matching choices: ${matches.map((entry) => entry.name).join(", ")}`);
|
|
9096
|
-
}
|
|
9097
|
-
throw new WorkspaceConfigError(`workspace ${JSON.stringify(workspaceName)} does not match ${cwd}`);
|
|
9098
|
-
}
|
|
9099
|
-
if (matches.length < 2)
|
|
9100
|
-
return matches[0];
|
|
9101
|
-
const highestSpecificity = Math.max(...matches.map((entry) => globSpecificity(entry.glob)));
|
|
9102
|
-
const mostSpecific = matches.filter((entry) => globSpecificity(entry.glob) === highestSpecificity);
|
|
9103
|
-
if (mostSpecific.length > 1) {
|
|
9104
|
-
throw new WorkspaceConfigError(`ambiguous workspace for ${cwd}; choose one of: ${mostSpecific.map((entry) => entry.name).join(", ")}`);
|
|
9105
|
-
}
|
|
9106
|
-
return mostSpecific[0];
|
|
9107
|
-
}, resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
|
|
9108
|
-
const matches = matchingEntries(cwd, entries);
|
|
9109
|
-
return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
|
|
9110
|
-
}, resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
|
|
9111
|
-
const result = readWorkspacesResult(dir);
|
|
9112
|
-
if (result.status === "malformed" || result.status === "invalid")
|
|
9113
|
-
throw new WorkspaceConfigError(result.error);
|
|
9114
|
-
return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
|
|
9115
|
-
}, runtimeWorkspacePolicySchema = (kind) => {
|
|
9116
|
-
const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
|
|
9117
|
-
const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
|
|
9118
|
-
provider: _enum(["gitlab", "github"]),
|
|
9119
|
-
defaultTargetBranch: nonBlank.optional(),
|
|
9120
|
-
account: nonBlank.optional()
|
|
9121
|
-
}).passthrough();
|
|
9122
|
-
const profileSchema = object({ [key]: policySchema.optional() }).passthrough();
|
|
9123
|
-
const entrySchema = object({
|
|
9124
|
-
name: nonBlank,
|
|
9125
|
-
glob: nonBlank,
|
|
9126
|
-
[key]: policySchema.optional(),
|
|
9127
|
-
defaultProfile: nonBlank.optional(),
|
|
9128
|
-
profiles: record(nonBlank, profileSchema).optional()
|
|
9129
|
-
}).passthrough();
|
|
9130
|
-
return entrySchema;
|
|
9131
|
-
}, resolveRuntimeWorkspaceVcs = (cwd) => resolveRuntimeWorkspaceCandidate(cwd, "vcs"), runtimeWorkspaceIndexSchema, matchRuntimeWorkspace = (cwd, dir = configDir()) => {
|
|
9132
|
-
const file = path3.join(dir, "workspaces.json");
|
|
9133
|
-
let raw;
|
|
9134
|
-
try {
|
|
9135
|
-
raw = readFileSync2(file, "utf8");
|
|
9136
|
-
} catch (error) {
|
|
9137
|
-
if (error.code === "ENOENT")
|
|
9138
|
-
return null;
|
|
9139
|
-
throw new WorkspaceConfigError(`${file} could not be read: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
9019
|
+
return null;
|
|
9140
9020
|
}
|
|
9141
|
-
|
|
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;
|
|
9142
9026
|
try {
|
|
9143
|
-
|
|
9027
|
+
fs3.rmdirSync(guard);
|
|
9028
|
+
return true;
|
|
9144
9029
|
} catch {
|
|
9145
|
-
|
|
9030
|
+
return false;
|
|
9146
9031
|
}
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
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 };
|
|
9151
9053
|
}
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
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];
|
|
9088
|
+
return {
|
|
9089
|
+
preset,
|
|
9090
|
+
allowed: preset === "custom" ? input.allowed ?? current.branchPolicy.allowed : [...defs.allowed],
|
|
9091
|
+
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
9092
|
+
};
|
|
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;
|
|
9157
9099
|
}
|
|
9158
|
-
const
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
}
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
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) => {
|
|
9167
9132
|
try {
|
|
9168
|
-
|
|
9133
|
+
return readFileSync(p, "utf8");
|
|
9169
9134
|
} catch {
|
|
9170
|
-
return { enabled: true, workspace: null, from: null };
|
|
9171
|
-
}
|
|
9172
|
-
if (!matched)
|
|
9173
|
-
return { enabled: true, workspace: null, from: null };
|
|
9174
|
-
const { selected, matches } = matched;
|
|
9175
|
-
const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
|
|
9176
|
-
const source = [selected, ...enclosing].find((entry) => typeof entry.trailers === "boolean");
|
|
9177
|
-
return {
|
|
9178
|
-
enabled: source?.trailers !== false,
|
|
9179
|
-
workspace: selected.name,
|
|
9180
|
-
from: source?.name ?? null
|
|
9181
|
-
};
|
|
9182
|
-
}, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
|
|
9183
|
-
const matched = matchRuntimeWorkspace(cwd);
|
|
9184
|
-
if (!matched)
|
|
9185
9135
|
return null;
|
|
9186
|
-
const { file, selected } = matched;
|
|
9187
|
-
const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
|
|
9188
|
-
if (!policy.success) {
|
|
9189
|
-
const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
|
|
9190
|
-
throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
|
|
9191
9136
|
}
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
const
|
|
9205
|
-
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);
|
|
9206
9186
|
return {
|
|
9207
|
-
|
|
9208
|
-
|
|
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
|
|
9209
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" };
|
|
9210
9203
|
};
|
|
9211
|
-
var
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
prefixes: object({
|
|
9222
|
-
feature: nonBlank,
|
|
9223
|
-
bugfix: nonBlank,
|
|
9224
|
-
release: nonBlank,
|
|
9225
|
-
hotfix: nonBlank
|
|
9226
|
-
}).passthrough().optional(),
|
|
9227
|
-
allowed: array(nonBlank).optional(),
|
|
9228
|
-
protected: array(nonBlank).optional(),
|
|
9229
|
-
integration: _enum(["pr", "merge"]).optional()
|
|
9230
|
-
}).strict();
|
|
9231
|
-
workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
|
|
9232
|
-
workspaceProfileSchema = object({
|
|
9233
|
-
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9234
|
-
commitPolicy: workspaceCommitPolicySchema.optional()
|
|
9235
|
-
}).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
|
|
9236
|
-
message: "a profile must define branchPolicy or commitPolicy"
|
|
9237
|
-
});
|
|
9238
|
-
versionSourceSchema = discriminatedUnion("kind", [
|
|
9239
|
-
object({
|
|
9240
|
-
kind: literal("package-json"),
|
|
9241
|
-
path: nonBlank,
|
|
9242
|
-
field: nonBlank
|
|
9243
|
-
}).strict(),
|
|
9244
|
-
object({ kind: literal("git-tag") }).strict(),
|
|
9245
|
-
object({ kind: literal("manual") }).strict()
|
|
9246
|
-
]);
|
|
9247
|
-
releaseTrackSchema = object({
|
|
9248
|
-
strategy: branchPreset.optional(),
|
|
9249
|
-
productionBranch: nonBlank,
|
|
9250
|
-
integrationBranch: nonBlank,
|
|
9251
|
-
naming: object({
|
|
9252
|
-
feature: nonBlank.optional(),
|
|
9253
|
-
release: nonBlank.optional(),
|
|
9254
|
-
hotfix: nonBlank.optional()
|
|
9255
|
-
}).passthrough().optional(),
|
|
9256
|
-
baseBranch: nonBlank.optional(),
|
|
9257
|
-
mergeBackBranches: array(nonBlank).optional(),
|
|
9258
|
-
pullRequestTarget: nonBlank.optional(),
|
|
9259
|
-
tagNamespace: string2().optional(),
|
|
9260
|
-
versionSource: versionSourceSchema.optional(),
|
|
9261
|
-
requiredChecks: array(nonBlank).optional(),
|
|
9262
|
-
critical: array(nonBlank).optional()
|
|
9263
|
-
}).passthrough().superRefine((track, context) => {
|
|
9264
|
-
for (const [field, values] of [
|
|
9265
|
-
["mergeBackBranches", track.mergeBackBranches ?? []],
|
|
9266
|
-
["requiredChecks", track.requiredChecks ?? []]
|
|
9267
|
-
]) {
|
|
9268
|
-
if (new Set(values).size !== values.length) {
|
|
9269
|
-
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
9270
|
-
}
|
|
9271
|
-
}
|
|
9272
|
-
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(".."))) {
|
|
9273
|
-
context.addIssue({
|
|
9274
|
-
code: "custom",
|
|
9275
|
-
path: ["versionSource", "path"],
|
|
9276
|
-
message: "must stay inside the repository"
|
|
9277
|
-
});
|
|
9278
|
-
}
|
|
9279
|
-
});
|
|
9280
|
-
workspaceConfigSchema = object({
|
|
9281
|
-
name: nonBlank,
|
|
9282
|
-
glob: nonBlank,
|
|
9283
|
-
vcs: object({
|
|
9284
|
-
provider: _enum(["gitlab", "github"]),
|
|
9285
|
-
defaultTargetBranch: nonBlank.optional(),
|
|
9286
|
-
tokenFile: nonBlank.optional(),
|
|
9287
|
-
account: nonBlank.optional()
|
|
9288
|
-
}).passthrough().optional(),
|
|
9289
|
-
autoApprove: union([boolean2(), array(nonBlank)]).optional(),
|
|
9290
|
-
defaultEndpoint: unknown().optional(),
|
|
9291
|
-
verification: _enum(["self", "independent"]).optional(),
|
|
9292
|
-
trailers: boolean2().optional(),
|
|
9293
|
-
autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
|
|
9294
|
-
kind,
|
|
9295
|
-
union([boolean2(), literal("verified")]).optional()
|
|
9296
|
-
]))).passthrough().optional(),
|
|
9297
|
-
youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
|
|
9298
|
-
issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
|
|
9299
|
-
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9300
|
-
commitPolicy: workspaceCommitPolicySchema.optional(),
|
|
9301
|
-
defaultProfile: nonBlank.optional(),
|
|
9302
|
-
profiles: record(nonBlank, workspaceProfileSchema).optional(),
|
|
9303
|
-
releaseTracks: record(nonBlank, releaseTrackSchema).optional()
|
|
9304
|
-
}).passthrough().superRefine((workspace, context) => {
|
|
9305
|
-
if (workspace.issues && workspace.vcs?.provider !== "github") {
|
|
9306
|
-
context.addIssue({
|
|
9307
|
-
code: "custom",
|
|
9308
|
-
path: ["issues"],
|
|
9309
|
-
message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
|
|
9310
|
-
});
|
|
9311
|
-
}
|
|
9312
|
-
if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
|
|
9313
|
-
context.addIssue({
|
|
9314
|
-
code: "custom",
|
|
9315
|
-
path: ["defaultProfile"],
|
|
9316
|
-
message: `does not name a configured profile`
|
|
9317
|
-
});
|
|
9318
|
-
}
|
|
9319
|
-
});
|
|
9320
|
-
workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
|
|
9321
|
-
const names = new Set;
|
|
9322
|
-
for (const [index, workspace] of (file.workspaces ?? []).entries()) {
|
|
9323
|
-
if (names.has(workspace.name)) {
|
|
9324
|
-
context.addIssue({
|
|
9325
|
-
code: "custom",
|
|
9326
|
-
path: ["workspaces", index, "name"],
|
|
9327
|
-
message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
|
|
9328
|
-
});
|
|
9329
|
-
}
|
|
9330
|
-
names.add(workspace.name);
|
|
9331
|
-
}
|
|
9332
|
-
});
|
|
9333
|
-
UNSUPPORTED_GLOB = /[?[\]{}]/;
|
|
9334
|
-
UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
|
|
9335
|
-
WorkspaceConfigError = class WorkspaceConfigError extends Error {
|
|
9336
|
-
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: [] }
|
|
9337
9214
|
};
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
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
|
+
];
|
|
9342
9235
|
});
|
|
9343
9236
|
|
|
9344
|
-
// packages/workit-core/src/core/
|
|
9345
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
9237
|
+
// packages/workit-core/src/core/branch-policy.ts
|
|
9346
9238
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
9347
|
-
|
|
9348
|
-
const
|
|
9349
|
-
if (
|
|
9350
|
-
return
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
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
|
|
9259
|
+
};
|
|
9354
9260
|
}
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
for (const branch of namedBranches(entry))
|
|
9364
|
-
protectedNames.add(branch);
|
|
9365
|
-
if (!name.trim()) {
|
|
9366
|
-
out.issues.push("a release track with a blank name was ignored");
|
|
9367
|
-
continue;
|
|
9368
|
-
}
|
|
9369
|
-
let critical = [];
|
|
9370
|
-
if (Array.isArray(entry.critical))
|
|
9371
|
-
critical = entry.critical.filter((item) => typeof item === "string");
|
|
9372
|
-
else if (entry.critical !== undefined)
|
|
9373
|
-
out.issues.push(`${label} critical: must be a list of field names; ignored`);
|
|
9374
|
-
const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
|
|
9375
|
-
const problem = (field, message) => {
|
|
9376
|
-
if (isCritical(field))
|
|
9377
|
-
fatal.push(`${label} ${field}: ${message}`);
|
|
9378
|
-
else
|
|
9379
|
-
out.issues.push(`${label} ${field}: ${message}`);
|
|
9261
|
+
if (root === "main") {
|
|
9262
|
+
return {
|
|
9263
|
+
preset: "github-flow",
|
|
9264
|
+
developBranch: null,
|
|
9265
|
+
integration: "pr",
|
|
9266
|
+
protected: [root],
|
|
9267
|
+
allowed: ["*"],
|
|
9268
|
+
prefixes: PREFIXES
|
|
9380
9269
|
};
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
const optionalBranch = (field) => {
|
|
9391
|
-
if (entry[field] === undefined)
|
|
9392
|
-
return null;
|
|
9393
|
-
const value = branchName(entry[field]);
|
|
9394
|
-
if (!value)
|
|
9395
|
-
problem(field, "not a branch name; using the integration branch");
|
|
9396
|
-
return value;
|
|
9270
|
+
}
|
|
9271
|
+
if (root === "master") {
|
|
9272
|
+
return {
|
|
9273
|
+
preset: "trunk-based",
|
|
9274
|
+
developBranch: null,
|
|
9275
|
+
integration: "pr",
|
|
9276
|
+
protected: [root],
|
|
9277
|
+
allowed: ["*"],
|
|
9278
|
+
prefixes: PREFIXES
|
|
9397
9279
|
};
|
|
9398
|
-
const baseBranch = optionalBranch("baseBranch");
|
|
9399
|
-
const pullRequestTarget = optionalBranch("pullRequestTarget");
|
|
9400
|
-
let mergeBackBranches = [];
|
|
9401
|
-
if (entry.mergeBackBranches !== undefined) {
|
|
9402
|
-
const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
|
|
9403
|
-
if (list.some((item) => item === null))
|
|
9404
|
-
problem("mergeBackBranches", "must be a list of branch names; ignored");
|
|
9405
|
-
else
|
|
9406
|
-
mergeBackBranches = [...new Set(list)];
|
|
9407
|
-
}
|
|
9408
|
-
const naming = {};
|
|
9409
|
-
if (entry.naming !== undefined) {
|
|
9410
|
-
if (!isRecord(entry.naming))
|
|
9411
|
-
problem("naming", "not an object; ignored");
|
|
9412
|
-
else
|
|
9413
|
-
for (const kind of ["feature", "release", "hotfix"]) {
|
|
9414
|
-
const template = entry.naming[kind];
|
|
9415
|
-
if (template === undefined)
|
|
9416
|
-
continue;
|
|
9417
|
-
if (typeof template !== "string" || !template.trim())
|
|
9418
|
-
problem(`naming.${kind}`, "not a template string; ignored");
|
|
9419
|
-
else if (!literalOf(template))
|
|
9420
|
-
problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
|
|
9421
|
-
else
|
|
9422
|
-
naming[kind] = template.trim();
|
|
9423
|
-
}
|
|
9424
|
-
}
|
|
9425
|
-
if (!productionBranch || !integrationBranch) {
|
|
9426
|
-
out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
|
|
9427
|
-
continue;
|
|
9428
|
-
}
|
|
9429
|
-
out.tracks.push({
|
|
9430
|
-
name,
|
|
9431
|
-
productionBranch,
|
|
9432
|
-
integrationBranch,
|
|
9433
|
-
baseBranch: baseBranch ?? integrationBranch,
|
|
9434
|
-
pullRequestTarget: pullRequestTarget ?? integrationBranch,
|
|
9435
|
-
mergeBackBranches,
|
|
9436
|
-
naming
|
|
9437
|
-
});
|
|
9438
9280
|
}
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
}
|
|
9457
|
-
const names = tracks.map((track) => track.name).join(", ");
|
|
9458
|
-
let envProblem = null;
|
|
9459
|
-
const done = (track, source, detail, warnings = [], ambiguous = null) => ({
|
|
9460
|
-
status: "resolved",
|
|
9461
|
-
track: track && track.name !== IMPLICIT ? track : null,
|
|
9462
|
-
source,
|
|
9463
|
-
detail,
|
|
9464
|
-
warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
|
|
9465
|
-
tracks,
|
|
9466
|
-
blocking: envProblem ?? ambiguous
|
|
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"
|
|
9467
9298
|
});
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
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) {
|
|
9471
9345
|
return {
|
|
9472
9346
|
status: "invalid",
|
|
9473
|
-
|
|
9474
|
-
|
|
9347
|
+
path: file,
|
|
9348
|
+
entries: [],
|
|
9349
|
+
error: `${file} workspaces.${index}.glob: ${glob.error}`
|
|
9475
9350
|
};
|
|
9476
|
-
|
|
9351
|
+
}
|
|
9477
9352
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
].includes(branch));
|
|
9484
|
-
const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
|
|
9485
|
-
name: IMPLICIT,
|
|
9486
|
-
productionBranch: input.defaultBranch,
|
|
9487
|
-
integrationBranch: input.defaultBranch,
|
|
9488
|
-
baseBranch: input.defaultBranch,
|
|
9489
|
-
pullRequestTarget: input.defaultBranch,
|
|
9490
|
-
mergeBackBranches: [],
|
|
9491
|
-
naming: {}
|
|
9492
|
-
} : null;
|
|
9493
|
-
const candidates = implicit ? [...tracks, implicit] : tracks;
|
|
9494
|
-
const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
|
|
9495
|
-
const fallback = (why) => {
|
|
9496
|
-
const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
|
|
9497
|
-
const track = byDefault.length === 1 ? byDefault[0] : null;
|
|
9498
|
-
const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
|
|
9499
|
-
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;
|
|
9500
|
-
return done(track, "default", `${why}; using the workspace ${target}`, [
|
|
9501
|
-
`release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
|
|
9502
|
-
], ambiguous);
|
|
9353
|
+
return {
|
|
9354
|
+
status: "valid",
|
|
9355
|
+
path: file,
|
|
9356
|
+
entries: validated.data.workspaces ?? [],
|
|
9357
|
+
document: parsed
|
|
9503
9358
|
};
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
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
|
+
};
|
|
9511
9373
|
}
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
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
|
+
};
|
|
9518
9385
|
}
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
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, "\\$&");
|
|
9544
9416
|
}
|
|
9545
|
-
return null;
|
|
9546
|
-
};
|
|
9547
|
-
const recordedOwner = followRecorded(branch);
|
|
9548
|
-
if (recordedOwner)
|
|
9549
|
-
return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
|
|
9550
|
-
for (const source of ["reflog", "checkout"]) {
|
|
9551
|
-
const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
|
|
9552
|
-
if (!ref)
|
|
9553
|
-
continue;
|
|
9554
|
-
const owner = ownerOf(ref) ?? followRecorded(short(ref));
|
|
9555
|
-
if (owner)
|
|
9556
|
-
return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
|
|
9557
9417
|
}
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
if (older === newer)
|
|
9570
|
-
return false;
|
|
9571
|
-
const key = `${older}>${newer}`;
|
|
9572
|
-
if (!ancestry.has(key))
|
|
9573
|
-
ancestry.set(key, input.probe.isAncestor(older, newer));
|
|
9574
|
-
return ancestry.get(key);
|
|
9575
|
-
};
|
|
9576
|
-
const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
|
|
9577
|
-
const winningTracks = [...new Set(winners.map((fork) => fork.track))];
|
|
9578
|
-
if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
|
|
9579
|
-
const win = winners[0];
|
|
9580
|
-
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
|
+
};
|
|
9581
9429
|
}
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
const tip = (branch) => {
|
|
9591
|
-
remote ??= pushRemoteName(cwd) ?? "origin";
|
|
9592
|
-
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
|
|
9593
|
-
};
|
|
9594
|
-
return {
|
|
9595
|
-
remotes: () => remotes ??= remoteNames(cwd),
|
|
9596
|
-
recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
|
|
9597
|
-
createdFrom: (branch) => {
|
|
9598
|
-
const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
|
|
9599
|
-
const oldest = log?.split(`
|
|
9600
|
-
`).findLast(Boolean) ?? "";
|
|
9601
|
-
const match = /^branch: Created from (\S+)$/u.exec(oldest);
|
|
9602
|
-
return match && match[1] !== "HEAD" ? match[1] : null;
|
|
9603
|
-
},
|
|
9604
|
-
checkedOutFrom: (branch) => {
|
|
9605
|
-
const own = reflog(cwd, `refs/heads/${branch}`);
|
|
9606
|
-
const created = own.at(-1);
|
|
9607
|
-
if (!created || created.subject !== "branch: Created from HEAD")
|
|
9608
|
-
return null;
|
|
9609
|
-
const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
|
|
9610
|
-
const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
|
|
9611
|
-
return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
|
|
9612
|
-
},
|
|
9613
|
-
mergeBase: (branch, trackBranch) => {
|
|
9614
|
-
const base = tip(trackBranch);
|
|
9615
|
-
return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
|
|
9616
|
-
},
|
|
9617
|
-
isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
|
|
9618
|
-
};
|
|
9619
|
-
}
|
|
9620
|
-
function workspaceReleaseTracks(cwd) {
|
|
9621
|
-
const raw = resolveRuntimeReleaseTracks(cwd);
|
|
9622
|
-
return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
|
|
9623
|
-
}
|
|
9624
|
-
function resolveTrackFor(cwd, options) {
|
|
9625
|
-
const read = workspaceReleaseTracks(cwd);
|
|
9626
|
-
if (read.error)
|
|
9627
|
-
return { status: "invalid", error: read.error, issues: read.issues };
|
|
9628
|
-
if (read.tracks.length === 0 && !options.track?.trim())
|
|
9629
|
-
return { status: "none", issues: read.issues };
|
|
9630
|
-
const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
|
|
9631
|
-
const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
|
|
9632
|
-
const input = {
|
|
9633
|
-
tracks: read.tracks,
|
|
9634
|
-
requested: options.track ?? null,
|
|
9635
|
-
envRequested,
|
|
9636
|
-
branch,
|
|
9637
|
-
defaultBranch: options.defaultBranch,
|
|
9638
|
-
issues: read.issues
|
|
9639
|
-
};
|
|
9640
|
-
let key = null;
|
|
9641
|
-
if (!options.probe) {
|
|
9642
|
-
const refs = git2(cwd, [
|
|
9643
|
-
"for-each-ref",
|
|
9644
|
-
"--format=%(refname) %(objectname)",
|
|
9645
|
-
"refs/heads",
|
|
9646
|
-
"refs/remotes"
|
|
9647
|
-
]) ?? "";
|
|
9648
|
-
const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
|
|
9649
|
-
const own = branch ? [
|
|
9650
|
-
git2(cwd, [
|
|
9651
|
-
"log",
|
|
9652
|
-
"-g",
|
|
9653
|
-
"-n",
|
|
9654
|
-
"1",
|
|
9655
|
-
"--date=unix",
|
|
9656
|
-
"--format=%H %gd %gs",
|
|
9657
|
-
`refs/heads/${branch}`,
|
|
9658
|
-
"--"
|
|
9659
|
-
]) ?? "",
|
|
9660
|
-
git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
|
|
9661
|
-
] : [];
|
|
9662
|
-
key = createHash4("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
|
|
9663
|
-
const hit = memo2.get(key);
|
|
9664
|
-
if (hit)
|
|
9665
|
-
return hit;
|
|
9666
|
-
}
|
|
9667
|
-
const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
|
|
9668
|
-
if (key) {
|
|
9669
|
-
if (memo2.size >= MEMO_LIMIT)
|
|
9670
|
-
memo2.delete(memo2.keys().next().value);
|
|
9671
|
-
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
|
+
};
|
|
9672
9438
|
}
|
|
9673
|
-
return
|
|
9674
|
-
}
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
}, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
|
|
9691
|
-
const run = spawnSync4("git", args, {
|
|
9692
|
-
cwd,
|
|
9693
|
-
encoding: "utf8",
|
|
9694
|
-
timeout: 20000,
|
|
9695
|
-
windowsHide: true,
|
|
9696
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
9697
|
-
});
|
|
9698
|
-
return run.status === 0 ? (run.stdout ?? "").trim() : null;
|
|
9699
|
-
}, 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(`
|
|
9700
|
-
`).map((line) => {
|
|
9701
|
-
const [sha = "", selector = "", ...subject] = line.split("\t");
|
|
9702
|
-
return {
|
|
9703
|
-
sha,
|
|
9704
|
-
time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
|
|
9705
|
-
subject: subject.join("\t")
|
|
9706
|
-
};
|
|
9707
|
-
}).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
|
|
9708
|
-
var init_release_tracks = __esm(() => {
|
|
9709
|
-
init_rev();
|
|
9710
|
-
init_config();
|
|
9711
|
-
init_workspaces();
|
|
9712
|
-
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;
|
|
9713
9456
|
};
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
"
|
|
9719
|
-
"
|
|
9720
|
-
"mergeBackBranches",
|
|
9721
|
-
"pullRequestTarget",
|
|
9722
|
-
"tagNamespace",
|
|
9723
|
-
"versionSource",
|
|
9724
|
-
"requiredChecks",
|
|
9725
|
-
"critical"
|
|
9726
|
-
]);
|
|
9727
|
-
BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
|
|
9728
|
-
PLACEHOLDER = /\{[^}]*\}/gu;
|
|
9729
|
-
HAS_PLACEHOLDER = /\{[^}]*\}/u;
|
|
9730
|
-
memo2 = new Map;
|
|
9731
|
-
});
|
|
9732
|
-
|
|
9733
|
-
// packages/workit-core/src/core/vcs-config.ts
|
|
9734
|
-
import fs2 from "node:fs";
|
|
9735
|
-
import path4 from "node:path";
|
|
9736
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
9737
|
-
function vcsConfig(mode, cwd, options = {}) {
|
|
9738
|
-
const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
|
|
9739
|
-
const wsVcs = ws?.vcs ?? {};
|
|
9740
|
-
const wsYt = ws?.youtrack ?? {};
|
|
9741
|
-
const wsIssues = ws?.issues ?? {};
|
|
9742
|
-
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
9743
|
-
const root = vcsCwd(cwd);
|
|
9744
|
-
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
9745
|
-
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
9746
|
-
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
9747
|
-
const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
|
|
9748
|
-
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
9749
|
-
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
9750
|
-
let issuesProvider = null;
|
|
9751
|
-
let linkOnPr = null;
|
|
9752
|
-
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
9753
|
-
issuesProvider = "github";
|
|
9754
|
-
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);
|
|
9755
9463
|
}
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
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));
|
|
9763
9473
|
});
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
releaseTrack,
|
|
9777
|
-
link_issues: linkIssues,
|
|
9778
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
9779
|
-
issues_provider: issuesProvider,
|
|
9780
|
-
link_on_pr: linkOnPr
|
|
9781
|
-
};
|
|
9782
|
-
}
|
|
9783
|
-
if (cfgStatus === "malformed") {
|
|
9784
|
-
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;
|
|
9785
9486
|
}
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
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}`);
|
|
9794
9506
|
}
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
pr: cfg.pr ?? {},
|
|
9802
|
-
workspace_name: ws?.name ?? null,
|
|
9803
|
-
link_issues: linkIssues,
|
|
9804
|
-
youtrack_base_url: youtrackBaseUrl,
|
|
9805
|
-
issues_provider: issuesProvider,
|
|
9806
|
-
link_on_pr: linkOnPr
|
|
9807
|
-
};
|
|
9808
|
-
if (provider === "gitlab") {
|
|
9809
|
-
out.gitlab = {
|
|
9810
|
-
host: prov.host ?? "gitlab.com",
|
|
9811
|
-
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
9812
|
-
};
|
|
9813
|
-
} else if (provider === "github") {
|
|
9814
|
-
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(", ")}`);
|
|
9815
9513
|
}
|
|
9816
|
-
return
|
|
9817
|
-
}
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
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");
|
|
9830
9541
|
let raw;
|
|
9831
9542
|
try {
|
|
9832
|
-
raw =
|
|
9833
|
-
} catch {
|
|
9834
|
-
|
|
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 });
|
|
9835
9548
|
}
|
|
9836
9549
|
let parsed;
|
|
9837
9550
|
try {
|
|
9838
9551
|
parsed = JSON.parse(raw);
|
|
9839
9552
|
} catch {
|
|
9840
|
-
|
|
9553
|
+
throw new WorkspaceConfigError(`${file} is not valid JSON`);
|
|
9841
9554
|
}
|
|
9842
|
-
|
|
9843
|
-
|
|
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}`);
|
|
9844
9559
|
}
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
warnings: resolution.warnings,
|
|
9851
|
-
track: resolution.track,
|
|
9852
|
-
tracks: resolution.tracks,
|
|
9853
|
-
blocking: resolution.blocking
|
|
9854
|
-
} : null;
|
|
9855
|
-
var init_vcs_config = __esm(() => {
|
|
9856
|
-
init_config();
|
|
9857
|
-
init_workspaces();
|
|
9858
|
-
init_release_tracks();
|
|
9859
|
-
});
|
|
9860
|
-
|
|
9861
|
-
// packages/workit-core/src/core/logger.ts
|
|
9862
|
-
import os3 from "node:os";
|
|
9863
|
-
import path5 from "node:path";
|
|
9864
|
-
var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
|
|
9865
|
-
const override = process.env.WORKFLOW_TOOLKIT_STATE;
|
|
9866
|
-
if (override)
|
|
9867
|
-
return override;
|
|
9868
|
-
const home = os3.homedir();
|
|
9869
|
-
if (process.env.XDG_STATE_HOME)
|
|
9870
|
-
return path5.join(process.env.XDG_STATE_HOME, "workit");
|
|
9871
|
-
switch (os3.platform()) {
|
|
9872
|
-
case "darwin":
|
|
9873
|
-
return path5.join(home, "Library", "Application Support", "workit");
|
|
9874
|
-
case "win32":
|
|
9875
|
-
return path5.join(process.env.LOCALAPPDATA ?? path5.join(home, "AppData", "Local"), "workit");
|
|
9876
|
-
default:
|
|
9877
|
-
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}`);
|
|
9878
9565
|
}
|
|
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
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
"
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
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()
|
|
9654
|
+
]);
|
|
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();
|
|
9919
9750
|
});
|
|
9920
9751
|
|
|
9921
|
-
// packages/workit-core/src/
|
|
9922
|
-
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9752
|
+
// packages/workit-core/src/core/release-tracks.ts
|
|
9923
9753
|
import { createHash as createHash5 } from "node:crypto";
|
|
9924
|
-
import
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
if (
|
|
9928
|
-
return
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
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;
|
|
9937
9770
|
}
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
try {
|
|
9944
|
-
entry = fs3.statSync(dotGit);
|
|
9945
|
-
} catch {}
|
|
9946
|
-
let gitDir = null;
|
|
9947
|
-
let top = null;
|
|
9948
|
-
if (entry) {
|
|
9949
|
-
top = dir;
|
|
9950
|
-
if (entry.isDirectory())
|
|
9951
|
-
gitDir = dotGit;
|
|
9952
|
-
else if (entry.isFile()) {
|
|
9953
|
-
let text = "";
|
|
9954
|
-
try {
|
|
9955
|
-
text = fs3.readFileSync(dotGit, "utf8");
|
|
9956
|
-
} catch {
|
|
9957
|
-
return null;
|
|
9958
|
-
}
|
|
9959
|
-
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
9960
|
-
if (!match)
|
|
9961
|
-
return null;
|
|
9962
|
-
gitDir = path6.resolve(dir, match[1].trim());
|
|
9963
|
-
} else
|
|
9964
|
-
return null;
|
|
9965
|
-
if (!fs3.existsSync(path6.join(gitDir, "HEAD")))
|
|
9966
|
-
return null;
|
|
9967
|
-
} else if (isGitDir(dir))
|
|
9968
|
-
gitDir = dir;
|
|
9969
|
-
if (gitDir) {
|
|
9970
|
-
let common = gitDir;
|
|
9971
|
-
try {
|
|
9972
|
-
common = path6.resolve(gitDir, fs3.readFileSync(path6.join(gitDir, "commondir"), "utf8").trim());
|
|
9973
|
-
} catch {}
|
|
9974
|
-
if (!fs3.existsSync(path6.join(common, "objects")))
|
|
9975
|
-
return null;
|
|
9976
|
-
return {
|
|
9977
|
-
kind: "repo",
|
|
9978
|
-
common: real(common),
|
|
9979
|
-
gitDir: real(gitDir),
|
|
9980
|
-
top: top === null ? null : real(top),
|
|
9981
|
-
reftable: reftableRefs(common)
|
|
9982
|
-
};
|
|
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;
|
|
9983
9776
|
}
|
|
9984
|
-
|
|
9985
|
-
if (
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
}
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
} catch {}
|
|
9996
|
-
if (!directory)
|
|
9997
|
-
return { dir: path6.join(root, ".workit"), shared: false, top: null };
|
|
9998
|
-
const fast = fastGit(root);
|
|
9999
|
-
if (fast?.kind === "none")
|
|
10000
|
-
return { dir: path6.join(root, ".workit"), shared: false, top: null };
|
|
10001
|
-
if (fast?.kind === "repo")
|
|
10002
|
-
return {
|
|
10003
|
-
dir: path6.join(fast.common, "workit"),
|
|
10004
|
-
shared: true,
|
|
10005
|
-
top: fast.top,
|
|
10006
|
-
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}`);
|
|
10007
9788
|
};
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
const
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
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;
|
|
10016
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
|
+
});
|
|
10017
9846
|
}
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
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;
|
|
10022
9851
|
}
|
|
10023
|
-
function
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
const
|
|
10027
|
-
if (
|
|
10028
|
-
|
|
10029
|
-
try {
|
|
10030
|
-
head = fs3.readFileSync(path6.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
10031
|
-
} catch {}
|
|
10032
|
-
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
10033
|
-
if (ref && ref[1] !== ".invalid")
|
|
10034
|
-
return branchKey(ref[1]);
|
|
10035
|
-
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
10036
|
-
const rebasing = rebasingBranch(location.git.gitDir);
|
|
10037
|
-
if (rebasing)
|
|
10038
|
-
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)
|
|
10039
9858
|
return {
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
9859
|
+
status: "invalid",
|
|
9860
|
+
error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
|
|
9861
|
+
issues
|
|
10043
9862
|
};
|
|
10044
|
-
}
|
|
9863
|
+
return { status: "none", issues };
|
|
10045
9864
|
}
|
|
10046
|
-
const
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
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`);
|
|
10055
9885
|
}
|
|
10056
|
-
const
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
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(", ")})`);
|
|
10067
9919
|
}
|
|
10068
|
-
const
|
|
10069
|
-
if (
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
return fs3.realpathSync(found.top);
|
|
10075
|
-
} catch {
|
|
10076
|
-
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`;
|
|
10077
9926
|
}
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
if (!
|
|
10081
|
-
return [];
|
|
10082
|
-
|
|
10083
|
-
const
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
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;
|
|
10089
9952
|
}
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
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
|
|
10095
10047
|
};
|
|
10096
|
-
|
|
10097
|
-
if (
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
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;
|
|
10116
10074
|
}
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
current = rename.to;
|
|
10125
|
-
since = rename.at;
|
|
10126
|
-
}
|
|
10127
|
-
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;
|
|
10128
10082
|
}
|
|
10129
|
-
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) => {
|
|
10130
10099
|
const run = spawnSync6("git", args, {
|
|
10131
10100
|
cwd,
|
|
10132
10101
|
encoding: "utf8",
|
|
10133
10102
|
timeout: 20000,
|
|
10134
|
-
killSignal: "SIGKILL",
|
|
10135
10103
|
windowsHide: true,
|
|
10136
|
-
|
|
10137
|
-
...process.env,
|
|
10138
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
10139
|
-
GIT_OPTIONAL_LOCKS: "0",
|
|
10140
|
-
LC_ALL: "C",
|
|
10141
|
-
LANGUAGE: "C"
|
|
10142
|
-
}
|
|
10104
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10143
10105
|
});
|
|
10144
|
-
|
|
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");
|
|
10145
10110
|
return {
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
missing: error?.code === "ENOENT"
|
|
10111
|
+
sha,
|
|
10112
|
+
time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
|
|
10113
|
+
subject: subject.join("\t")
|
|
10150
10114
|
};
|
|
10151
|
-
}
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
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;
|
|
10156
10163
|
}
|
|
10157
|
-
|
|
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;
|
|
10158
10239
|
try {
|
|
10159
|
-
|
|
10240
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
10160
10241
|
} catch {
|
|
10161
|
-
return
|
|
10242
|
+
return { status: "missing", path: file, config: {} };
|
|
10162
10243
|
}
|
|
10163
|
-
|
|
10244
|
+
let parsed;
|
|
10164
10245
|
try {
|
|
10165
|
-
|
|
10246
|
+
parsed = JSON.parse(raw);
|
|
10166
10247
|
} catch {
|
|
10167
|
-
return
|
|
10248
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
10168
10249
|
}
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
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");
|
|
10177
10286
|
}
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
"
|
|
10190
|
-
"
|
|
10191
|
-
"
|
|
10192
|
-
"
|
|
10193
|
-
"
|
|
10194
|
-
|
|
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");
|
|
10195
10327
|
});
|
|
10196
10328
|
|
|
10197
10329
|
// packages/workit-core/src/host-session.ts
|
|
10198
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
10199
|
-
import
|
|
10330
|
+
import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10331
|
+
import fs5 from "node:fs";
|
|
10200
10332
|
import path7 from "node:path";
|
|
10201
10333
|
function hostSessionFromEnv(env) {
|
|
10202
10334
|
const value = (key) => env[key]?.trim() || null;
|
|
@@ -10225,7 +10357,7 @@ function migrateRepoIssued(cwd) {
|
|
|
10225
10357
|
if (location instanceof Error)
|
|
10226
10358
|
return;
|
|
10227
10359
|
old = path7.join(location.dir, "hooks", "issued");
|
|
10228
|
-
if (!
|
|
10360
|
+
if (!fs5.existsSync(old))
|
|
10229
10361
|
return;
|
|
10230
10362
|
} catch {
|
|
10231
10363
|
return;
|
|
@@ -10233,13 +10365,18 @@ function migrateRepoIssued(cwd) {
|
|
|
10233
10365
|
for (const lead of safeList(old)) {
|
|
10234
10366
|
try {
|
|
10235
10367
|
const from = path7.join(old, lead);
|
|
10236
|
-
if (!
|
|
10368
|
+
if (!fs5.statSync(from).isDirectory())
|
|
10237
10369
|
continue;
|
|
10238
10370
|
const to = path7.join(userIssuedRoot(), lead);
|
|
10239
|
-
|
|
10240
|
-
for (const id of
|
|
10241
|
-
|
|
10242
|
-
|
|
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 });
|
|
10243
10380
|
}
|
|
10244
10381
|
removeIfEmpty(from);
|
|
10245
10382
|
} catch {}
|
|
@@ -10247,30 +10384,76 @@ function migrateRepoIssued(cwd) {
|
|
|
10247
10384
|
removeIfEmpty(old);
|
|
10248
10385
|
}
|
|
10249
10386
|
function recordIssuedSession(cwd, lead, issued) {
|
|
10387
|
+
const token = randomBytes(18).toString("base64url");
|
|
10250
10388
|
try {
|
|
10251
10389
|
migrateRepoIssued(cwd);
|
|
10252
|
-
const dir = path7.join(userIssuedRoot(), hash(lead));
|
|
10253
|
-
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10254
10390
|
const own = hash(issued);
|
|
10255
|
-
|
|
10256
|
-
|
|
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);
|
|
10257
10408
|
if (entries.length <= ISSUED_PER_LEAD)
|
|
10258
10409
|
return;
|
|
10259
|
-
const byAge = entries.filter((name) => name !== own).map((name) => ({ name, at:
|
|
10260
|
-
for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD))
|
|
10261
|
-
|
|
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
|
+
}
|
|
10262
10415
|
} catch {}
|
|
10263
10416
|
}
|
|
10264
|
-
|
|
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) => {
|
|
10265
10427
|
try {
|
|
10266
|
-
return
|
|
10428
|
+
return fs5.readdirSync(dir);
|
|
10267
10429
|
} catch {
|
|
10268
10430
|
return [];
|
|
10269
10431
|
}
|
|
10270
10432
|
}, removeIfEmpty = (dir) => {
|
|
10271
10433
|
try {
|
|
10272
|
-
|
|
10434
|
+
fs5.rmdirSync(dir);
|
|
10273
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
|
+
}
|
|
10274
10457
|
}, OWN_CHILD_SHELL;
|
|
10275
10458
|
var init_host_session = __esm(() => {
|
|
10276
10459
|
init_logger();
|
|
@@ -10283,13 +10466,14 @@ var init_host_session = __esm(() => {
|
|
|
10283
10466
|
(env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
|
|
10284
10467
|
]
|
|
10285
10468
|
];
|
|
10469
|
+
TOKEN_HASH = /^([0-9a-f]{64})$/mu;
|
|
10286
10470
|
OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
|
|
10287
10471
|
});
|
|
10288
10472
|
|
|
10289
10473
|
// packages/workit-core/src/ledger.ts
|
|
10290
|
-
import { spawnSync as
|
|
10291
|
-
import { createHash as createHash7, randomBytes } from "node:crypto";
|
|
10292
|
-
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";
|
|
10293
10477
|
import path8 from "node:path";
|
|
10294
10478
|
function resolveCommit(cwd, ref) {
|
|
10295
10479
|
if (!safeRef(ref))
|
|
@@ -10365,7 +10549,7 @@ function ledgerPath(cwd) {
|
|
|
10365
10549
|
const root = storeRoot(cwd);
|
|
10366
10550
|
return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
|
|
10367
10551
|
}
|
|
10368
|
-
function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
10552
|
+
function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
|
|
10369
10553
|
const lock = `${file}.lock`;
|
|
10370
10554
|
const deadline = Date.now() + waitMs;
|
|
10371
10555
|
let token = null;
|
|
@@ -10389,13 +10573,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
|
10389
10573
|
}
|
|
10390
10574
|
function appendLine(file, build, options) {
|
|
10391
10575
|
const write = () => {
|
|
10392
|
-
const fd =
|
|
10576
|
+
const fd = fs6.openSync(file, "a+", 384);
|
|
10393
10577
|
try {
|
|
10394
|
-
|
|
10578
|
+
if (process.platform !== "win32")
|
|
10579
|
+
tighten(fd);
|
|
10580
|
+
const size = fs6.fstatSync(fd).size;
|
|
10395
10581
|
let fence = "";
|
|
10396
10582
|
if (size > 0) {
|
|
10397
10583
|
const last = Buffer.alloc(1);
|
|
10398
|
-
|
|
10584
|
+
fs6.readSync(fd, last, 0, 1, size - 1);
|
|
10399
10585
|
if (last[0] !== 10)
|
|
10400
10586
|
fence = `
|
|
10401
10587
|
`;
|
|
@@ -10404,16 +10590,16 @@ function appendLine(file, build, options) {
|
|
|
10404
10590
|
if (!line.ok)
|
|
10405
10591
|
return line;
|
|
10406
10592
|
const bytes = Buffer.from(fence + line.value);
|
|
10407
|
-
const written =
|
|
10593
|
+
const written = fs6.writeSync(fd, bytes);
|
|
10408
10594
|
if (written !== bytes.length)
|
|
10409
10595
|
return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
|
|
10410
10596
|
return { ok: true, value: undefined };
|
|
10411
10597
|
} finally {
|
|
10412
|
-
|
|
10598
|
+
fs6.closeSync(fd);
|
|
10413
10599
|
}
|
|
10414
10600
|
};
|
|
10415
10601
|
try {
|
|
10416
|
-
|
|
10602
|
+
fs6.mkdirSync(path8.dirname(file), { recursive: true });
|
|
10417
10603
|
if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
|
|
10418
10604
|
return write();
|
|
10419
10605
|
const locked = withLedgerLock(file, write);
|
|
@@ -10467,8 +10653,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10467
10653
|
if (!file.ok)
|
|
10468
10654
|
return file;
|
|
10469
10655
|
let text = "";
|
|
10656
|
+
let stat = null;
|
|
10470
10657
|
try {
|
|
10471
|
-
|
|
10658
|
+
stat = fs6.statSync(file.value);
|
|
10659
|
+
text = fs6.readFileSync(file.value, "utf8");
|
|
10472
10660
|
} catch (error) {
|
|
10473
10661
|
if (error.code === "ENOENT")
|
|
10474
10662
|
return {
|
|
@@ -10487,6 +10675,7 @@ function readLedger(cwd, options = {}) {
|
|
|
10487
10675
|
const integrity = { chained: 0, legacy: 0, unverified: [] };
|
|
10488
10676
|
const earlier = new Set([null]);
|
|
10489
10677
|
let chainStarted = false;
|
|
10678
|
+
const ids = new Set;
|
|
10490
10679
|
for (const raw of text.split(`
|
|
10491
10680
|
`)) {
|
|
10492
10681
|
const trimmed = raw.trim();
|
|
@@ -10520,6 +10709,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10520
10709
|
reason = "unsigned";
|
|
10521
10710
|
else
|
|
10522
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);
|
|
10523
10716
|
if (reason)
|
|
10524
10717
|
rowIntegrity = "unverified";
|
|
10525
10718
|
const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
|
|
@@ -10543,7 +10736,8 @@ function readLedger(cwd, options = {}) {
|
|
|
10543
10736
|
seq: rows.length + 1,
|
|
10544
10737
|
superseded: false,
|
|
10545
10738
|
supersedeIgnored: false,
|
|
10546
|
-
...rowIntegrity ? { integrity: rowIntegrity } : {}
|
|
10739
|
+
...rowIntegrity ? { integrity: rowIntegrity } : {},
|
|
10740
|
+
...reason ? { integrityReason: reason } : {}
|
|
10547
10741
|
};
|
|
10548
10742
|
rows.push(row);
|
|
10549
10743
|
if (!options.integrity)
|
|
@@ -10571,18 +10765,44 @@ function readLedger(cwd, options = {}) {
|
|
|
10571
10765
|
const byId = new Map;
|
|
10572
10766
|
for (const row of rows) {
|
|
10573
10767
|
const target = row.supersedes ? byId.get(row.supersedes) : undefined;
|
|
10574
|
-
if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10768
|
+
if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10575
10769
|
target.superseded = true;
|
|
10576
10770
|
else if (row.supersedes)
|
|
10577
10771
|
row.supersedeIgnored = true;
|
|
10578
10772
|
if (!byId.has(row.id))
|
|
10579
10773
|
byId.set(row.id, row);
|
|
10580
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
|
+
});
|
|
10581
10781
|
return {
|
|
10582
10782
|
ok: true,
|
|
10583
10783
|
value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
|
|
10584
10784
|
};
|
|
10585
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
|
+
}
|
|
10586
10806
|
function verdictBasis(verdict, current) {
|
|
10587
10807
|
if (verdict.head && verdict.head === current.head)
|
|
10588
10808
|
return "fresh";
|
|
@@ -10649,15 +10869,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
|
|
|
10649
10869
|
sessions.add(session);
|
|
10650
10870
|
return sessions;
|
|
10651
10871
|
}
|
|
10652
|
-
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) {
|
|
10653
10907
|
let effective = null;
|
|
10654
10908
|
for (const row of rows) {
|
|
10655
10909
|
if (!effective) {
|
|
10656
10910
|
effective = row;
|
|
10657
10911
|
continue;
|
|
10658
10912
|
}
|
|
10659
|
-
|
|
10660
|
-
|
|
10913
|
+
if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
|
|
10914
|
+
continue;
|
|
10915
|
+
const rowIndependent = reasonsOf(row).length === 0;
|
|
10916
|
+
const effIndependent = reasonsOf(effective).length === 0;
|
|
10661
10917
|
if (effIndependent && !rowIndependent && isCurrent(effective))
|
|
10662
10918
|
continue;
|
|
10663
10919
|
if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
|
|
@@ -10671,7 +10927,7 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10671
10927
|
return false;
|
|
10672
10928
|
const edges = new Map;
|
|
10673
10929
|
for (const row of rows)
|
|
10674
|
-
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)
|
|
10675
10931
|
edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
|
|
10676
10932
|
const seen = new Set([from]);
|
|
10677
10933
|
const queue = [from];
|
|
@@ -10687,7 +10943,8 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10687
10943
|
}
|
|
10688
10944
|
return false;
|
|
10689
10945
|
}
|
|
10690
|
-
function checkVerdicts(cwd, branch,
|
|
10946
|
+
function checkVerdicts(cwd, branch, ledgerRows) {
|
|
10947
|
+
const rows = withIntegrity(cwd, ledgerRows);
|
|
10691
10948
|
const head = branchHead(cwd, branch);
|
|
10692
10949
|
const fallbackBase = defaultBase(cwd);
|
|
10693
10950
|
const scope = branchScope(cwd, branch);
|
|
@@ -10717,21 +10974,47 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10717
10974
|
return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
|
|
10718
10975
|
};
|
|
10719
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);
|
|
10720
11003
|
const byKind = new Map;
|
|
10721
11004
|
for (const row of rows)
|
|
10722
|
-
if (isLiveVerdict(row) && scope(row)) {
|
|
11005
|
+
if (isLiveVerdict(row, retired) && scope(row)) {
|
|
10723
11006
|
const kind = typeof row.kind === "string" ? row.kind : "review";
|
|
10724
11007
|
byKind.set(kind, [...byKind.get(kind) ?? [], row]);
|
|
10725
11008
|
}
|
|
10726
11009
|
const verdicts = [];
|
|
10727
11010
|
const warnings = [];
|
|
10728
11011
|
for (const [kind, list] of byKind) {
|
|
10729
|
-
const row = effectiveOf(list,
|
|
11012
|
+
const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
|
|
10730
11013
|
if (!row)
|
|
10731
11014
|
continue;
|
|
10732
11015
|
const basis = basisOf(row);
|
|
10733
11016
|
const current = basis !== "stale";
|
|
10734
|
-
const independence =
|
|
11017
|
+
const independence = reasonsOf(row);
|
|
10735
11018
|
const reasons = [
|
|
10736
11019
|
...current ? [] : ["stale"],
|
|
10737
11020
|
...PASSING.has(String(row.result)) ? [] : ["not_passing"],
|
|
@@ -10748,23 +11031,31 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10748
11031
|
});
|
|
10749
11032
|
if (reasons.length === 0) {
|
|
10750
11033
|
for (const later of list)
|
|
10751
|
-
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")
|
|
10752
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`);
|
|
10753
11036
|
}
|
|
10754
11037
|
}
|
|
10755
11038
|
for (const row of rows)
|
|
10756
11039
|
if (row.integrity === "unverified" && scope(row))
|
|
10757
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
|
+
}
|
|
10758
11049
|
verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
|
|
10759
11050
|
const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
|
|
10760
11051
|
const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
|
|
10761
11052
|
const newestAccepted = verdicts.findLast((entry) => entry.accepted && STRONG_RESULTS.has(String(entry.verdict.result))) ?? null;
|
|
10762
|
-
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: [] } : {
|
|
10763
11054
|
accepted: false,
|
|
10764
11055
|
verdict: failing?.verdict ?? null,
|
|
10765
11056
|
reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
|
|
10766
11057
|
};
|
|
10767
|
-
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)));
|
|
10768
11059
|
return {
|
|
10769
11060
|
branch,
|
|
10770
11061
|
head,
|
|
@@ -10785,7 +11076,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10785
11076
|
error,
|
|
10786
11077
|
...unblock ? { unblock } : {}
|
|
10787
11078
|
}), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
10788
|
-
const run =
|
|
11079
|
+
const run = spawnSync8("git", args, {
|
|
10789
11080
|
cwd,
|
|
10790
11081
|
encoding: "utf8",
|
|
10791
11082
|
timeout: timeoutMs,
|
|
@@ -10819,24 +11110,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10819
11110
|
} catch {
|
|
10820
11111
|
return null;
|
|
10821
11112
|
}
|
|
10822
|
-
}, 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) => {
|
|
10823
11114
|
if (process.platform !== "linux")
|
|
10824
11115
|
return false;
|
|
10825
11116
|
try {
|
|
10826
|
-
return isNetworkFsType(
|
|
11117
|
+
return isNetworkFsType(fs6.statfsSync(dir).type);
|
|
10827
11118
|
} catch {
|
|
10828
11119
|
return false;
|
|
10829
11120
|
}
|
|
10830
11121
|
}, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
|
|
10831
11122
|
try {
|
|
10832
|
-
return
|
|
11123
|
+
return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
|
|
10833
11124
|
} catch {
|
|
10834
11125
|
return null;
|
|
10835
11126
|
}
|
|
10836
11127
|
}, lockAgeMs = (lock) => {
|
|
10837
11128
|
for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
|
|
10838
11129
|
try {
|
|
10839
|
-
return Date.now() -
|
|
11130
|
+
return Date.now() - fs6.statSync(target).mtimeMs;
|
|
10840
11131
|
} catch {}
|
|
10841
11132
|
}
|
|
10842
11133
|
return null;
|
|
@@ -10844,7 +11135,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10844
11135
|
${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
10845
11136
|
for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
|
|
10846
11137
|
const buffer = Buffer.alloc(span);
|
|
10847
|
-
|
|
11138
|
+
fs6.readSync(fd, buffer, 0, span, size - span);
|
|
10848
11139
|
const end = buffer.lastIndexOf(10);
|
|
10849
11140
|
if (end < 0) {
|
|
10850
11141
|
if (span === size)
|
|
@@ -10872,6 +11163,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10872
11163
|
return text || null;
|
|
10873
11164
|
}
|
|
10874
11165
|
return null;
|
|
11166
|
+
}, tighten = (fd) => {
|
|
11167
|
+
try {
|
|
11168
|
+
if ((fs6.fstatSync(fd).mode & 63) !== 0)
|
|
11169
|
+
fs6.fchmodSync(fd, 384);
|
|
11170
|
+
} catch {}
|
|
10875
11171
|
}, sealed = (row, id, at, previous) => {
|
|
10876
11172
|
const { prevHash: _prev, rowHash: _row, ...fields } = row;
|
|
10877
11173
|
const prevHash = previous === null ? null : sha2562(previous);
|
|
@@ -10886,7 +11182,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10886
11182
|
session: str(actor.session),
|
|
10887
11183
|
agentId: str(actor.agentId)
|
|
10888
11184
|
};
|
|
10889
|
-
}, 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) => {
|
|
10890
11186
|
return (target) => {
|
|
10891
11187
|
const own = typeof branch === "string" ? branch : null;
|
|
10892
11188
|
const theirs = typeof target.branch === "string" ? target.branch : null;
|
|
@@ -10903,7 +11199,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10903
11199
|
if (derivesFrom(author, session) || derivesFrom(session, author))
|
|
10904
11200
|
return true;
|
|
10905
11201
|
return false;
|
|
10906
|
-
}, independenceReasons = (row, authors) => {
|
|
11202
|
+
}, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
|
|
10907
11203
|
const reasons = [];
|
|
10908
11204
|
if (!row.actor.session)
|
|
10909
11205
|
reasons.push("no_session");
|
|
@@ -10914,6 +11210,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10914
11210
|
return reasons;
|
|
10915
11211
|
}, OBSERVED_ONLY;
|
|
10916
11212
|
var init_ledger = __esm(() => {
|
|
11213
|
+
init_store_lock();
|
|
10917
11214
|
init_vcs_config();
|
|
10918
11215
|
init_rev();
|
|
10919
11216
|
init_host_session();
|
|
@@ -10937,56 +11234,73 @@ var init_ledger = __esm(() => {
|
|
|
10937
11234
|
ledgerLock = {
|
|
10938
11235
|
acquire(lock) {
|
|
10939
11236
|
try {
|
|
10940
|
-
|
|
11237
|
+
fs6.mkdirSync(lock);
|
|
10941
11238
|
} catch (error) {
|
|
10942
11239
|
if (error.code === "EEXIST")
|
|
10943
11240
|
return null;
|
|
10944
11241
|
throw error;
|
|
10945
11242
|
}
|
|
10946
|
-
const token = `${process.pid}-${
|
|
10947
|
-
|
|
11243
|
+
const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
|
|
11244
|
+
fs6.writeFileSync(path8.join(lock, LOCK_OWNER), token);
|
|
10948
11245
|
return token;
|
|
10949
11246
|
},
|
|
10950
11247
|
release(lock, token) {
|
|
10951
11248
|
if (lockToken(lock) !== token)
|
|
10952
11249
|
return false;
|
|
10953
|
-
const tomb = `${lock}.release-${
|
|
11250
|
+
const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
|
|
10954
11251
|
try {
|
|
10955
|
-
|
|
11252
|
+
fs6.renameSync(lock, tomb);
|
|
10956
11253
|
} catch {
|
|
10957
11254
|
return false;
|
|
10958
11255
|
}
|
|
10959
11256
|
if (lockToken(tomb) !== token) {
|
|
10960
11257
|
try {
|
|
10961
|
-
|
|
11258
|
+
fs6.renameSync(tomb, lock);
|
|
10962
11259
|
} catch {}
|
|
10963
11260
|
return false;
|
|
10964
11261
|
}
|
|
10965
|
-
|
|
11262
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10966
11263
|
return true;
|
|
10967
11264
|
},
|
|
10968
11265
|
token: lockToken,
|
|
10969
11266
|
reap(lock, observed) {
|
|
10970
|
-
const tomb = `${lock}.reap-${
|
|
11267
|
+
const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
|
|
10971
11268
|
try {
|
|
10972
|
-
|
|
11269
|
+
fs6.renameSync(lock, tomb);
|
|
10973
11270
|
} catch {
|
|
10974
11271
|
return false;
|
|
10975
11272
|
}
|
|
10976
11273
|
if (lockToken(tomb) === observed) {
|
|
10977
|
-
|
|
11274
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10978
11275
|
return true;
|
|
10979
11276
|
}
|
|
10980
11277
|
try {
|
|
10981
|
-
|
|
11278
|
+
fs6.renameSync(tomb, lock);
|
|
10982
11279
|
} catch {
|
|
10983
|
-
|
|
11280
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10984
11281
|
}
|
|
10985
11282
|
return false;
|
|
10986
11283
|
}
|
|
10987
11284
|
};
|
|
10988
11285
|
TAIL_BYTES = 64 * 1024;
|
|
11286
|
+
TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
|
|
11287
|
+
tamperCache = new Map;
|
|
10989
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
|
+
]);
|
|
10990
11304
|
OBSERVED_ONLY = new Set([
|
|
10991
11305
|
"check",
|
|
10992
11306
|
"merge.unverified",
|
|
@@ -11936,7 +12250,7 @@ function normalizeMaxBytes(value, options = {}) {
|
|
|
11936
12250
|
}
|
|
11937
12251
|
|
|
11938
12252
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
11939
|
-
import
|
|
12253
|
+
import fs7 from "node:fs";
|
|
11940
12254
|
function createScratchBuffer(maxBytes) {
|
|
11941
12255
|
const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
|
|
11942
12256
|
return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
|
|
@@ -11959,7 +12273,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
11959
12273
|
let total = 0;
|
|
11960
12274
|
while (true) {
|
|
11961
12275
|
const length = nextReadLength(total, maxBytes, scratch.length);
|
|
11962
|
-
const bytesRead =
|
|
12276
|
+
const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
|
|
11963
12277
|
if (bytesRead === 0) {
|
|
11964
12278
|
return Buffer.concat(chunks, total);
|
|
11965
12279
|
}
|
|
@@ -11973,9 +12287,9 @@ var init_bounded_read = __esm(() => {
|
|
|
11973
12287
|
});
|
|
11974
12288
|
|
|
11975
12289
|
// node_modules/@openclaw/fs-safe/dist/read-open-flags.js
|
|
11976
|
-
import
|
|
12290
|
+
import fs8 from "node:fs";
|
|
11977
12291
|
function resolveReadOpenFlags(options) {
|
|
11978
|
-
const constants = options?.constants ??
|
|
12292
|
+
const constants = options?.constants ?? fs8.constants;
|
|
11979
12293
|
const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
11980
12294
|
const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
|
|
11981
12295
|
return constants.O_RDONLY | noFollow | nonBlocking;
|
|
@@ -11983,7 +12297,7 @@ function resolveReadOpenFlags(options) {
|
|
|
11983
12297
|
var init_read_open_flags = () => {};
|
|
11984
12298
|
|
|
11985
12299
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
11986
|
-
import { randomBytes as
|
|
12300
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
11987
12301
|
import fsSync from "node:fs";
|
|
11988
12302
|
import path11 from "node:path";
|
|
11989
12303
|
function parseSidecarLockSnapshot(snapshot, parser) {
|
|
@@ -11991,7 +12305,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
|
|
|
11991
12305
|
}
|
|
11992
12306
|
function createSidecarLockOwnershipToken() {
|
|
11993
12307
|
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
11994
|
-
for (const byte of
|
|
12308
|
+
for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
11995
12309
|
for (let bit = 7;bit >= 0; bit -= 1) {
|
|
11996
12310
|
token += byte & 1 << bit ? "\t" : " ";
|
|
11997
12311
|
}
|
|
@@ -12188,7 +12502,7 @@ function sleepSync(ms) {
|
|
|
12188
12502
|
}
|
|
12189
12503
|
|
|
12190
12504
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
12191
|
-
import
|
|
12505
|
+
import fs9 from "node:fs";
|
|
12192
12506
|
import path12 from "node:path";
|
|
12193
12507
|
function getSyncHeldLocks() {
|
|
12194
12508
|
const globalWithState = globalThis;
|
|
@@ -12206,7 +12520,7 @@ function releaseAllSyncHeldLocks() {
|
|
|
12206
12520
|
}
|
|
12207
12521
|
try {
|
|
12208
12522
|
if (held.fd !== undefined)
|
|
12209
|
-
|
|
12523
|
+
fs9.closeSync(held.fd);
|
|
12210
12524
|
} catch {}
|
|
12211
12525
|
try {
|
|
12212
12526
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
@@ -12241,7 +12555,7 @@ function releaseSyncHeldLock(held) {
|
|
|
12241
12555
|
if (held.fd !== undefined) {
|
|
12242
12556
|
const fd = held.fd;
|
|
12243
12557
|
held.fd = undefined;
|
|
12244
|
-
|
|
12558
|
+
fs9.closeSync(fd);
|
|
12245
12559
|
}
|
|
12246
12560
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
12247
12561
|
heldLocks.delete(held.normalizedTargetPath);
|
|
@@ -12264,11 +12578,11 @@ function createSyncHeldLockHandle(held) {
|
|
|
12264
12578
|
};
|
|
12265
12579
|
}
|
|
12266
12580
|
function canonicalLockParentSync(parent) {
|
|
12267
|
-
return process.platform === "win32" ?
|
|
12581
|
+
return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
|
|
12268
12582
|
}
|
|
12269
12583
|
function normalizeTargetPath(targetPath) {
|
|
12270
12584
|
const resolved = path12.resolve(targetPath);
|
|
12271
|
-
|
|
12585
|
+
fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
|
|
12272
12586
|
try {
|
|
12273
12587
|
return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
|
|
12274
12588
|
} catch {
|
|
@@ -12296,7 +12610,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
|
12296
12610
|
}
|
|
12297
12611
|
function reclaimGuardExists(reclaimGuardPath) {
|
|
12298
12612
|
try {
|
|
12299
|
-
|
|
12613
|
+
fs9.lstatSync(reclaimGuardPath);
|
|
12300
12614
|
return true;
|
|
12301
12615
|
} catch (error) {
|
|
12302
12616
|
if (error.code === "ENOENT")
|
|
@@ -12360,19 +12674,19 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12360
12674
|
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
12361
12675
|
let lockFileCreateDenied = false;
|
|
12362
12676
|
try {
|
|
12363
|
-
const noFollow = process.platform !== "win32" && typeof
|
|
12677
|
+
const noFollow = process.platform !== "win32" && typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
|
|
12364
12678
|
try {
|
|
12365
|
-
fd =
|
|
12679
|
+
fd = fs9.openSync(lockPath, fs9.constants.O_WRONLY | fs9.constants.O_CREAT | fs9.constants.O_EXCL | noFollow, 384);
|
|
12366
12680
|
} catch (error) {
|
|
12367
12681
|
lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
|
|
12368
12682
|
throw error;
|
|
12369
12683
|
}
|
|
12370
|
-
|
|
12371
|
-
|
|
12684
|
+
fs9.writeFileSync(fd, raw, "utf8");
|
|
12685
|
+
fs9.fsyncSync(fd);
|
|
12372
12686
|
const snapshot = {
|
|
12373
12687
|
raw,
|
|
12374
12688
|
payload,
|
|
12375
|
-
stat:
|
|
12689
|
+
stat: fs9.fstatSync(fd),
|
|
12376
12690
|
ownershipToken
|
|
12377
12691
|
};
|
|
12378
12692
|
const createdHeld = {
|
|
@@ -12407,8 +12721,8 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12407
12721
|
return returnedHandle;
|
|
12408
12722
|
} catch (error) {
|
|
12409
12723
|
if (fd !== undefined) {
|
|
12410
|
-
const failed = { payload: null, stat:
|
|
12411
|
-
|
|
12724
|
+
const failed = { payload: null, stat: fs9.fstatSync(fd) };
|
|
12725
|
+
fs9.closeSync(fd);
|
|
12412
12726
|
fd = undefined;
|
|
12413
12727
|
removeSidecarLockIfUnchangedSync(lockPath, failed);
|
|
12414
12728
|
}
|
|
@@ -12457,7 +12771,7 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12457
12771
|
})) {
|
|
12458
12772
|
let ownsReclaimGuard = false;
|
|
12459
12773
|
try {
|
|
12460
|
-
|
|
12774
|
+
fs9.mkdirSync(reclaimGuardPath);
|
|
12461
12775
|
ownsReclaimGuard = true;
|
|
12462
12776
|
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
12463
12777
|
continue;
|
|
@@ -12470,7 +12784,7 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12470
12784
|
} finally {
|
|
12471
12785
|
if (ownsReclaimGuard) {
|
|
12472
12786
|
try {
|
|
12473
|
-
|
|
12787
|
+
fs9.rmdirSync(reclaimGuardPath);
|
|
12474
12788
|
} catch {}
|
|
12475
12789
|
}
|
|
12476
12790
|
}
|
|
@@ -12516,137 +12830,6 @@ var packageRoot = () => {
|
|
|
12516
12830
|
};
|
|
12517
12831
|
var init_package_root = () => {};
|
|
12518
12832
|
|
|
12519
|
-
// packages/workit-core/src/core/store-lock.ts
|
|
12520
|
-
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
12521
|
-
import fs9 from "node:fs";
|
|
12522
|
-
import { hostname } from "node:os";
|
|
12523
|
-
var metadataLockSchema, FOREIGN_LOCK_TTL_MS, UNREADABLE_LOCK_TTL_MS = 30000, RECLAIM_GUARD_TTL_MS = 30000, defaultLockTimeoutMs = 250, defaultLockTimeout = () => defaultLockTimeoutMs, parseMetadataLock = (raw) => {
|
|
12524
|
-
let value;
|
|
12525
|
-
try {
|
|
12526
|
-
value = JSON.parse(raw);
|
|
12527
|
-
} catch {
|
|
12528
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
12529
|
-
}
|
|
12530
|
-
const parsed = metadataLockSchema.safeParse(value);
|
|
12531
|
-
if (!parsed.success)
|
|
12532
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
12533
|
-
return parsed.data;
|
|
12534
|
-
}, parseMetadataLockOrNull = (raw) => {
|
|
12535
|
-
try {
|
|
12536
|
-
return parseMetadataLock(raw);
|
|
12537
|
-
} catch {
|
|
12538
|
-
return null;
|
|
12539
|
-
}
|
|
12540
|
-
}, parseProcStatStart = (stat) => {
|
|
12541
|
-
const close = stat.lastIndexOf(")");
|
|
12542
|
-
if (close < 0)
|
|
12543
|
-
return null;
|
|
12544
|
-
return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
|
|
12545
|
-
}, processStartOf = (pid) => {
|
|
12546
|
-
if (process.platform === "linux") {
|
|
12547
|
-
try {
|
|
12548
|
-
return parseProcStatStart(fs9.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
12549
|
-
} catch {
|
|
12550
|
-
return null;
|
|
12551
|
-
}
|
|
12552
|
-
}
|
|
12553
|
-
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
12554
|
-
try {
|
|
12555
|
-
const run = spawnSync8("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
12556
|
-
encoding: "utf8",
|
|
12557
|
-
timeout: 1000
|
|
12558
|
-
});
|
|
12559
|
-
const value = run.status === 0 ? run.stdout.trim() : "";
|
|
12560
|
-
return value || null;
|
|
12561
|
-
} catch {
|
|
12562
|
-
return null;
|
|
12563
|
-
}
|
|
12564
|
-
}
|
|
12565
|
-
return null;
|
|
12566
|
-
}, readTrimmed = (file) => {
|
|
12567
|
-
try {
|
|
12568
|
-
return fs9.readFileSync(file, "utf8").trim() || null;
|
|
12569
|
-
} catch {
|
|
12570
|
-
return null;
|
|
12571
|
-
}
|
|
12572
|
-
}, cachedLockHost = null, localLockHost = () => {
|
|
12573
|
-
if (cachedLockHost !== null)
|
|
12574
|
-
return cachedLockHost;
|
|
12575
|
-
let pidns = null;
|
|
12576
|
-
try {
|
|
12577
|
-
pidns = /\[(\d+)\]/.exec(fs9.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
12578
|
-
} catch {}
|
|
12579
|
-
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
12580
|
-
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
12581
|
-
return cachedLockHost;
|
|
12582
|
-
}, pidAlive = (pid) => {
|
|
12583
|
-
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
12584
|
-
return false;
|
|
12585
|
-
try {
|
|
12586
|
-
process.kill(pid, 0);
|
|
12587
|
-
return true;
|
|
12588
|
-
} catch (error) {
|
|
12589
|
-
return error.code === "EPERM";
|
|
12590
|
-
}
|
|
12591
|
-
}, TASK_STORE_LOCK, classifyLockOwner = (payload, ageMs, localHost = localLockHost(), options = {}) => {
|
|
12592
|
-
const lock = metadataLockSchema.safeParse(payload);
|
|
12593
|
-
if (!lock.success)
|
|
12594
|
-
return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
|
|
12595
|
-
const { pid, processStart, host } = lock.data;
|
|
12596
|
-
const [lockName, lockSpace] = host.split("#");
|
|
12597
|
-
const [localName, localSpace] = localHost.split("#");
|
|
12598
|
-
if (lockSpace && localSpace && lockName === localName) {
|
|
12599
|
-
const [lockNs, lockBoot] = lockSpace.split(":");
|
|
12600
|
-
const [localNs, localBoot] = localSpace.split(":");
|
|
12601
|
-
if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
|
|
12602
|
-
return { state: "stale", reason: "lock was taken before this machine rebooted" };
|
|
12603
|
-
}
|
|
12604
|
-
if (host !== localHost)
|
|
12605
|
-
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` };
|
|
12606
|
-
if (!pidAlive(pid))
|
|
12607
|
-
return { state: "stale", reason: `pid ${pid} is not running` };
|
|
12608
|
-
const currentStart = processStartOf(pid);
|
|
12609
|
-
if (processStart !== null && currentStart !== null && processStart !== currentStart)
|
|
12610
|
-
return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
|
|
12611
|
-
if (options.reclaimUnverifiedAfterTtl === true && (processStart === null || currentStart === null) && ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS)
|
|
12612
|
-
return {
|
|
12613
|
-
state: "stale",
|
|
12614
|
-
reason: `pid ${pid} is running but the lock is older than its TTL and the pid's start time cannot be checked (reused pid)`
|
|
12615
|
-
};
|
|
12616
|
-
return { state: "live", reason: `held by running pid ${pid}` };
|
|
12617
|
-
}, ageOf = (file, nowMs) => {
|
|
12618
|
-
try {
|
|
12619
|
-
return nowMs - fs9.lstatSync(file).mtimeMs;
|
|
12620
|
-
} catch {
|
|
12621
|
-
return null;
|
|
12622
|
-
}
|
|
12623
|
-
}, clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
12624
|
-
const guard = `${lockPath}.reclaim`;
|
|
12625
|
-
const age = ageOf(guard, nowMs);
|
|
12626
|
-
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
12627
|
-
return false;
|
|
12628
|
-
try {
|
|
12629
|
-
fs9.rmdirSync(guard);
|
|
12630
|
-
return true;
|
|
12631
|
-
} catch {
|
|
12632
|
-
return false;
|
|
12633
|
-
}
|
|
12634
|
-
};
|
|
12635
|
-
var init_store_lock = __esm(() => {
|
|
12636
|
-
init_zod();
|
|
12637
|
-
init_task_contract();
|
|
12638
|
-
init_paths();
|
|
12639
|
-
metadataLockSchema = object({
|
|
12640
|
-
pid: number2().int().nonnegative().safe(),
|
|
12641
|
-
processStart: string2().nullable(),
|
|
12642
|
-
host: string2().min(1),
|
|
12643
|
-
nonce: string2().min(1),
|
|
12644
|
-
externalAction: literal(true).optional()
|
|
12645
|
-
}).strict();
|
|
12646
|
-
FOREIGN_LOCK_TTL_MS = 10 * 60000;
|
|
12647
|
-
TASK_STORE_LOCK = { reclaimUnverifiedAfterTtl: true };
|
|
12648
|
-
});
|
|
12649
|
-
|
|
12650
12833
|
// packages/workit-core/src/store/event-log.ts
|
|
12651
12834
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
12652
12835
|
import fs10 from "node:fs";
|
|
@@ -20056,10 +20239,10 @@ function stopObligation(input, descriptor) {
|
|
|
20056
20239
|
const branch = currentBranch(input.cwd);
|
|
20057
20240
|
if (!branch || guardedBranch(input.cwd, branch))
|
|
20058
20241
|
return null;
|
|
20059
|
-
const ledger = readLedger(input.cwd);
|
|
20242
|
+
const ledger = readLedger(input.cwd, { integrity: true });
|
|
20060
20243
|
if (!ledger.ok)
|
|
20061
20244
|
return null;
|
|
20062
|
-
const all = ledger.value.rows;
|
|
20245
|
+
const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
|
|
20063
20246
|
const rows = all.filter((row) => row.branch === branch);
|
|
20064
20247
|
const mine = rows.filter((row) => sessionOf(row) === session);
|
|
20065
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"));
|
|
@@ -20587,9 +20770,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
|
|
|
20587
20770
|
if (role !== "judge" || !event.agentId)
|
|
20588
20771
|
return readOnly;
|
|
20589
20772
|
const own = subagentSession(session, event.agentId);
|
|
20590
|
-
|
|
20591
|
-
|
|
20592
|
-
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." : ""}`;
|
|
20593
20776
|
}, failureDecision = (descriptor, event, error) => {
|
|
20594
20777
|
if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
|
|
20595
20778
|
return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
|