@brainervirus/workit-cursor 8.13.0 → 8.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +2310 -1722
- package/dist/mcp-server.js +639 -369
- package/dist/workit-hook.js +1587 -963
- package/package.json +4 -4
- package/skills/workit-fanout/SKILL.md +4 -4
- package/skills/workit-fanout/references/orchestration.md +8 -5
- package/skills/workit-review/SKILL.md +1 -1
|
@@ -8638,1565 +8638,1712 @@ 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
|
-
|
|
9354
|
-
|
|
9355
|
-
const
|
|
9356
|
-
const
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
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}`);
|
|
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
|
|
9380
9259
|
};
|
|
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;
|
|
9260
|
+
}
|
|
9261
|
+
if (root === "main") {
|
|
9262
|
+
return {
|
|
9263
|
+
preset: "github-flow",
|
|
9264
|
+
developBranch: null,
|
|
9265
|
+
integration: "pr",
|
|
9266
|
+
protected: [root],
|
|
9267
|
+
allowed: ["*"],
|
|
9268
|
+
prefixes: PREFIXES
|
|
9397
9269
|
};
|
|
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
9270
|
}
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
if (tracks.length === 0) {
|
|
9449
|
-
if (requested)
|
|
9450
|
-
return {
|
|
9451
|
-
status: "invalid",
|
|
9452
|
-
error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
|
|
9453
|
-
issues
|
|
9454
|
-
};
|
|
9455
|
-
return { status: "none", issues };
|
|
9271
|
+
if (root === "master") {
|
|
9272
|
+
return {
|
|
9273
|
+
preset: "trunk-based",
|
|
9274
|
+
developBranch: null,
|
|
9275
|
+
integration: "pr",
|
|
9276
|
+
protected: [root],
|
|
9277
|
+
allowed: ["*"],
|
|
9278
|
+
prefixes: PREFIXES
|
|
9279
|
+
};
|
|
9456
9280
|
}
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
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
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
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
|
|
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
|
+
};
|
|
9438
|
+
}
|
|
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;
|
|
9618
9456
|
};
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
const
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
branch,
|
|
9637
|
-
defaultBranch: options.defaultBranch,
|
|
9638
|
-
issues: read.issues
|
|
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);
|
|
9463
|
+
}
|
|
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));
|
|
9473
|
+
});
|
|
9639
9474
|
};
|
|
9640
|
-
|
|
9641
|
-
if (
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
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;
|
|
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;
|
|
9666
9486
|
}
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
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}`);
|
|
9672
9506
|
}
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
...new Set(tracks.flatMap((track) => [
|
|
9680
|
-
track.productionBranch,
|
|
9681
|
-
track.integrationBranch,
|
|
9682
|
-
track.baseBranch,
|
|
9683
|
-
track.pullRequestTarget,
|
|
9684
|
-
...track.mergeBackBranches
|
|
9685
|
-
]))
|
|
9686
|
-
], 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) => {
|
|
9687
|
-
const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
|
|
9688
|
-
const remote = remotes.find((name) => bare.startsWith(`${name}/`));
|
|
9689
|
-
return remote ? bare.slice(remote.length + 1) : bare;
|
|
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 {
|
|
9713
|
-
};
|
|
9714
|
-
KNOWN_FIELDS = new Set([
|
|
9715
|
-
"strategy",
|
|
9716
|
-
"productionBranch",
|
|
9717
|
-
"integrationBranch",
|
|
9718
|
-
"naming",
|
|
9719
|
-
"baseBranch",
|
|
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;
|
|
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(", ")}`);
|
|
9755
9513
|
}
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
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");
|
|
9541
|
+
let raw;
|
|
9542
|
+
try {
|
|
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 });
|
|
9782
9548
|
}
|
|
9783
|
-
|
|
9784
|
-
|
|
9549
|
+
let parsed;
|
|
9550
|
+
try {
|
|
9551
|
+
parsed = JSON.parse(raw);
|
|
9552
|
+
} catch {
|
|
9553
|
+
throw new WorkspaceConfigError(`${file} is not valid JSON`);
|
|
9785
9554
|
}
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
ok: false,
|
|
9791
|
-
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
9792
|
-
configPath: cfgPath
|
|
9793
|
-
};
|
|
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}`);
|
|
9794
9559
|
}
|
|
9795
|
-
const
|
|
9796
|
-
const
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
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}`);
|
|
9565
|
+
}
|
|
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
|
+
}, inheritedSetting = (cwd, dir, has) => {
|
|
9574
|
+
let matched;
|
|
9575
|
+
try {
|
|
9576
|
+
matched = matchRuntimeWorkspace(cwd, dir);
|
|
9577
|
+
} catch {
|
|
9578
|
+
return null;
|
|
9579
|
+
}
|
|
9580
|
+
if (!matched)
|
|
9581
|
+
return null;
|
|
9582
|
+
const { selected, matches } = matched;
|
|
9583
|
+
const enclosing = matches.filter((entry) => entry !== selected).toSorted((a, b) => globSpecificity(b.glob) - globSpecificity(a.glob));
|
|
9584
|
+
return { workspace: selected.name, source: [selected, ...enclosing].find(has) ?? null };
|
|
9585
|
+
}, workspaceTrailers = (cwd, dir) => {
|
|
9586
|
+
const found = inheritedSetting(cwd, dir, (entry) => typeof entry.trailers === "boolean");
|
|
9587
|
+
return {
|
|
9588
|
+
enabled: found?.source?.trailers !== false,
|
|
9589
|
+
workspace: found?.workspace ?? null,
|
|
9590
|
+
from: found?.source?.name ?? null
|
|
9591
|
+
};
|
|
9592
|
+
}, workspaceLiveSessionGuard = (cwd, dir) => inheritedSetting(cwd, dir, (entry) => typeof entry.liveSessionGuard === "boolean")?.source?.liveSessionGuard !== false, TIER_KEYS, TIER_HOSTS, resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
|
|
9593
|
+
const matched = matchRuntimeWorkspace(cwd);
|
|
9594
|
+
if (!matched)
|
|
9595
|
+
return null;
|
|
9596
|
+
const { file, selected } = matched;
|
|
9597
|
+
const policy = runtimeWorkspacePolicySchema(kind).safeParse(selected);
|
|
9598
|
+
if (!policy.success) {
|
|
9599
|
+
const detail = policy.error.issues.map((issue) => `${issue.path.map(String).join(".") || "workspace"}: ${issue.message}`).join("; ");
|
|
9600
|
+
throw new WorkspaceConfigError(`${file} has invalid ${kind} policy configuration: ${detail}`);
|
|
9601
|
+
}
|
|
9602
|
+
return policy.data;
|
|
9603
|
+
}, resolveRuntimeWorkspacePolicy = (cwd, kind, requestedProfile) => {
|
|
9604
|
+
const candidate = resolveRuntimeWorkspaceCandidate(cwd, kind);
|
|
9605
|
+
if (!candidate)
|
|
9606
|
+
return { policy: undefined, source: "user-default" };
|
|
9607
|
+
const key = kind === "branch" ? "branchPolicy" : "commitPolicy";
|
|
9608
|
+
const profileName = requestedProfile?.trim() || candidate.defaultProfile;
|
|
9609
|
+
if (candidate.defaultProfile && !Object.hasOwn(candidate.profiles ?? {}, candidate.defaultProfile))
|
|
9610
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(candidate.defaultProfile)}`);
|
|
9611
|
+
const profile = profileName ? candidate.profiles?.[profileName] : undefined;
|
|
9612
|
+
if (profileName && !profile)
|
|
9613
|
+
throw new WorkspaceConfigError(`workspace ${JSON.stringify(candidate.name)} has no profile ${JSON.stringify(profileName)}`);
|
|
9614
|
+
const profilePolicy = profile?.[key];
|
|
9615
|
+
const workspacePolicy = candidate[key];
|
|
9616
|
+
return {
|
|
9617
|
+
policy: profilePolicy ?? workspacePolicy,
|
|
9618
|
+
source: profilePolicy ? `profile:${profileName}` : workspacePolicy ? `workspace:${candidate.name}` : "user-default"
|
|
9619
|
+
};
|
|
9620
|
+
};
|
|
9621
|
+
var init_workspaces = __esm(() => {
|
|
9622
|
+
init_zod();
|
|
9623
|
+
init_config();
|
|
9624
|
+
init_branch_policy();
|
|
9625
|
+
nonBlank = string2().refine((value) => value.trim().length > 0, "must not be blank");
|
|
9626
|
+
branchPreset = custom((value) => typeof value === "string" && Object.hasOwn(PRESETS, value), "unsupported branch preset");
|
|
9627
|
+
commitPreset = custom((value) => typeof value === "string" && COMMIT_PRESETS.includes(value), "unsupported commit preset");
|
|
9628
|
+
workspaceBranchPolicySchema = object({
|
|
9629
|
+
preset: branchPreset,
|
|
9630
|
+
developBranch: nonBlank.optional(),
|
|
9631
|
+
prefixes: object({
|
|
9632
|
+
feature: nonBlank,
|
|
9633
|
+
bugfix: nonBlank,
|
|
9634
|
+
release: nonBlank,
|
|
9635
|
+
hotfix: nonBlank
|
|
9636
|
+
}).passthrough().optional(),
|
|
9637
|
+
allowed: array(nonBlank).optional(),
|
|
9638
|
+
protected: array(nonBlank).optional(),
|
|
9639
|
+
integration: _enum(["pr", "merge"]).optional()
|
|
9640
|
+
}).strict();
|
|
9641
|
+
workspaceCommitPolicySchema = object({ preset: commitPreset, pattern: string2().optional() }).strict();
|
|
9642
|
+
workspaceProfileSchema = object({
|
|
9643
|
+
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9644
|
+
commitPolicy: workspaceCommitPolicySchema.optional()
|
|
9645
|
+
}).strict().refine((profile) => profile.branchPolicy !== undefined || profile.commitPolicy !== undefined, {
|
|
9646
|
+
message: "a profile must define branchPolicy or commitPolicy"
|
|
9647
|
+
});
|
|
9648
|
+
versionSourceSchema = discriminatedUnion("kind", [
|
|
9649
|
+
object({
|
|
9650
|
+
kind: literal("package-json"),
|
|
9651
|
+
path: nonBlank,
|
|
9652
|
+
field: nonBlank
|
|
9653
|
+
}).strict(),
|
|
9654
|
+
object({ kind: literal("git-tag") }).strict(),
|
|
9655
|
+
object({ kind: literal("manual") }).strict()
|
|
9656
|
+
]);
|
|
9657
|
+
releaseTrackSchema = object({
|
|
9658
|
+
strategy: branchPreset.optional(),
|
|
9659
|
+
productionBranch: nonBlank,
|
|
9660
|
+
integrationBranch: nonBlank,
|
|
9661
|
+
naming: object({
|
|
9662
|
+
feature: nonBlank.optional(),
|
|
9663
|
+
release: nonBlank.optional(),
|
|
9664
|
+
hotfix: nonBlank.optional()
|
|
9665
|
+
}).passthrough().optional(),
|
|
9666
|
+
baseBranch: nonBlank.optional(),
|
|
9667
|
+
mergeBackBranches: array(nonBlank).optional(),
|
|
9668
|
+
pullRequestTarget: nonBlank.optional(),
|
|
9669
|
+
tagNamespace: string2().optional(),
|
|
9670
|
+
versionSource: versionSourceSchema.optional(),
|
|
9671
|
+
requiredChecks: array(nonBlank).optional(),
|
|
9672
|
+
critical: array(nonBlank).optional()
|
|
9673
|
+
}).passthrough().superRefine((track, context) => {
|
|
9674
|
+
for (const [field, values] of [
|
|
9675
|
+
["mergeBackBranches", track.mergeBackBranches ?? []],
|
|
9676
|
+
["requiredChecks", track.requiredChecks ?? []]
|
|
9677
|
+
]) {
|
|
9678
|
+
if (new Set(values).size !== values.length) {
|
|
9679
|
+
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
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(".."))) {
|
|
9683
|
+
context.addIssue({
|
|
9684
|
+
code: "custom",
|
|
9685
|
+
path: ["versionSource", "path"],
|
|
9686
|
+
message: "must stay inside the repository"
|
|
9687
|
+
});
|
|
9688
|
+
}
|
|
9689
|
+
});
|
|
9690
|
+
workspaceConfigSchema = object({
|
|
9691
|
+
name: nonBlank,
|
|
9692
|
+
glob: nonBlank,
|
|
9693
|
+
vcs: object({
|
|
9694
|
+
provider: _enum(["gitlab", "github"]),
|
|
9695
|
+
defaultTargetBranch: nonBlank.optional(),
|
|
9696
|
+
tokenFile: nonBlank.optional(),
|
|
9697
|
+
account: nonBlank.optional()
|
|
9698
|
+
}).passthrough().optional(),
|
|
9699
|
+
autoApprove: union([boolean2(), array(nonBlank)]).optional(),
|
|
9700
|
+
defaultEndpoint: unknown().optional(),
|
|
9701
|
+
verification: _enum(["self", "independent"]).optional(),
|
|
9702
|
+
trailers: boolean2().optional(),
|
|
9703
|
+
liveSessionGuard: boolean2().optional(),
|
|
9704
|
+
fanout: object({ tierModels: unknown().optional() }).passthrough().optional(),
|
|
9705
|
+
autonomy: object(Object.fromEntries(["push", "pr", "merge", "release", "rerun"].map((kind) => [
|
|
9706
|
+
kind,
|
|
9707
|
+
union([boolean2(), literal("verified")]).optional()
|
|
9708
|
+
]))).passthrough().optional(),
|
|
9709
|
+
youtrack: object({ baseUrl: nonBlank.optional(), link_issues: boolean2().optional() }).passthrough().optional(),
|
|
9710
|
+
issues: object({ provider: literal("github").optional(), link_on_pr: boolean2().optional() }).passthrough().optional(),
|
|
9711
|
+
branchPolicy: workspaceBranchPolicySchema.optional(),
|
|
9712
|
+
commitPolicy: workspaceCommitPolicySchema.optional(),
|
|
9713
|
+
defaultProfile: nonBlank.optional(),
|
|
9714
|
+
profiles: record(nonBlank, workspaceProfileSchema).optional(),
|
|
9715
|
+
releaseTracks: record(nonBlank, releaseTrackSchema).optional()
|
|
9716
|
+
}).passthrough().superRefine((workspace, context) => {
|
|
9717
|
+
if (workspace.issues && workspace.vcs?.provider !== "github") {
|
|
9718
|
+
context.addIssue({
|
|
9719
|
+
code: "custom",
|
|
9720
|
+
path: ["issues"],
|
|
9721
|
+
message: `GitHub issue linking requires the github provider, got ${workspace.vcs?.provider ?? "unset"}`
|
|
9722
|
+
});
|
|
9723
|
+
}
|
|
9724
|
+
if (workspace.defaultProfile !== undefined && !Object.hasOwn(workspace.profiles ?? {}, workspace.defaultProfile)) {
|
|
9725
|
+
context.addIssue({
|
|
9726
|
+
code: "custom",
|
|
9727
|
+
path: ["defaultProfile"],
|
|
9728
|
+
message: `does not name a configured profile`
|
|
9729
|
+
});
|
|
9730
|
+
}
|
|
9731
|
+
});
|
|
9732
|
+
workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).optional() }).passthrough().superRefine((file, context) => {
|
|
9733
|
+
const names = new Set;
|
|
9734
|
+
for (const [index, workspace] of (file.workspaces ?? []).entries()) {
|
|
9735
|
+
if (names.has(workspace.name)) {
|
|
9736
|
+
context.addIssue({
|
|
9737
|
+
code: "custom",
|
|
9738
|
+
path: ["workspaces", index, "name"],
|
|
9739
|
+
message: `duplicate workspace name ${JSON.stringify(workspace.name)}`
|
|
9740
|
+
});
|
|
9741
|
+
}
|
|
9742
|
+
names.add(workspace.name);
|
|
9743
|
+
}
|
|
9744
|
+
});
|
|
9745
|
+
UNSUPPORTED_GLOB = /[?[\]{}]/;
|
|
9746
|
+
UNSUPPORTED_EXTGLOB = /[@+*]\(|!/;
|
|
9747
|
+
WorkspaceConfigError = class WorkspaceConfigError extends Error {
|
|
9748
|
+
name = "WorkspaceConfigError";
|
|
9807
9749
|
};
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
const url = (r.stdout ?? "").trim();
|
|
9823
|
-
if (/github\.com[:/]/.test(url))
|
|
9824
|
-
return "github";
|
|
9825
|
-
if (/gitlab\.com[:/]/.test(url))
|
|
9826
|
-
return "gitlab";
|
|
9827
|
-
return null;
|
|
9828
|
-
}, readVcsConfig = () => {
|
|
9829
|
-
const file = vcsConfigPath();
|
|
9830
|
-
let raw;
|
|
9831
|
-
try {
|
|
9832
|
-
raw = fs2.readFileSync(file, "utf8");
|
|
9833
|
-
} catch {
|
|
9834
|
-
return { status: "missing", path: file, config: {} };
|
|
9835
|
-
}
|
|
9836
|
-
let parsed;
|
|
9837
|
-
try {
|
|
9838
|
-
parsed = JSON.parse(raw);
|
|
9839
|
-
} catch {
|
|
9840
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
9841
|
-
}
|
|
9842
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9843
|
-
return { status: "valid", path: file, config: parsed };
|
|
9844
|
-
}
|
|
9845
|
-
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
9846
|
-
}, trackSummary = (resolution) => resolution.status === "resolved" ? {
|
|
9847
|
-
name: resolution.track?.name ?? null,
|
|
9848
|
-
source: resolution.source,
|
|
9849
|
-
detail: resolution.detail,
|
|
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");
|
|
9878
|
-
}
|
|
9879
|
-
}, homePattern;
|
|
9880
|
-
var init_logger = __esm(() => {
|
|
9881
|
-
SENSITIVE_WORDS = new Set([
|
|
9882
|
-
"token",
|
|
9883
|
-
"secret",
|
|
9884
|
-
"password",
|
|
9885
|
-
"passwd",
|
|
9886
|
-
"authorization",
|
|
9887
|
-
"credential",
|
|
9888
|
-
"credentials",
|
|
9889
|
-
"cookie",
|
|
9890
|
-
"api",
|
|
9891
|
-
"apikey",
|
|
9892
|
-
"apitoken",
|
|
9893
|
-
"accesstoken",
|
|
9894
|
-
"clientsecret",
|
|
9895
|
-
"refreshtoken",
|
|
9896
|
-
"key",
|
|
9897
|
-
"bearer",
|
|
9898
|
-
"prompt",
|
|
9899
|
-
"message",
|
|
9900
|
-
"messages",
|
|
9901
|
-
"content",
|
|
9902
|
-
"body",
|
|
9903
|
-
"args",
|
|
9904
|
-
"argument",
|
|
9905
|
-
"result",
|
|
9906
|
-
"results",
|
|
9907
|
-
"output",
|
|
9908
|
-
"issue",
|
|
9909
|
-
"description",
|
|
9910
|
-
"summary",
|
|
9911
|
-
"payload",
|
|
9912
|
-
"script",
|
|
9913
|
-
"command",
|
|
9914
|
-
"text",
|
|
9915
|
-
"query"
|
|
9750
|
+
mainCheckouts = new Map;
|
|
9751
|
+
runtimeWorkspaceIndexSchema = object({
|
|
9752
|
+
workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
|
|
9753
|
+
}).passthrough();
|
|
9754
|
+
TIER_KEYS = new Set(["mundane", "standard", "hard"]);
|
|
9755
|
+
TIER_HOSTS = new Set([
|
|
9756
|
+
"claude_code",
|
|
9757
|
+
"opencode",
|
|
9758
|
+
"codex",
|
|
9759
|
+
"codex_cli",
|
|
9760
|
+
"codex_desktop",
|
|
9761
|
+
"cursor",
|
|
9762
|
+
"pi",
|
|
9763
|
+
"default"
|
|
9916
9764
|
]);
|
|
9917
|
-
STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
|
|
9918
|
-
homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
9919
9765
|
});
|
|
9920
9766
|
|
|
9921
|
-
// packages/workit-core/src/
|
|
9922
|
-
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9767
|
+
// packages/workit-core/src/core/release-tracks.ts
|
|
9923
9768
|
import { createHash as createHash5 } from "node:crypto";
|
|
9924
|
-
import
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
if (
|
|
9928
|
-
return
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9769
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9770
|
+
function readReleaseTracks(raw) {
|
|
9771
|
+
const out = { tracks: [], issues: [], error: null, protectedBranches: [] };
|
|
9772
|
+
if (raw === undefined || raw === null)
|
|
9773
|
+
return out;
|
|
9774
|
+
if (!isRecord(raw)) {
|
|
9775
|
+
out.issues.push("releaseTracks is not an object; ignored");
|
|
9776
|
+
return out;
|
|
9777
|
+
}
|
|
9778
|
+
const fatal = [];
|
|
9779
|
+
const protectedNames = new Set;
|
|
9780
|
+
for (const [name, entry] of Object.entries(raw).toSorted(([a], [b]) => a < b ? -1 : 1)) {
|
|
9781
|
+
const label = `release track ${JSON.stringify(name)}`;
|
|
9782
|
+
if (!isRecord(entry)) {
|
|
9783
|
+
out.issues.push(`${label} is not an object; ignored`);
|
|
9784
|
+
continue;
|
|
9937
9785
|
}
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
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")))
|
|
9786
|
+
for (const branch of namedBranches(entry))
|
|
9787
|
+
protectedNames.add(branch);
|
|
9788
|
+
if (!name.trim()) {
|
|
9789
|
+
out.issues.push("a release track with a blank name was ignored");
|
|
9790
|
+
continue;
|
|
9791
|
+
}
|
|
9792
|
+
let critical = [];
|
|
9793
|
+
if (Array.isArray(entry.critical))
|
|
9794
|
+
critical = entry.critical.filter((item) => typeof item === "string");
|
|
9795
|
+
else if (entry.critical !== undefined)
|
|
9796
|
+
out.issues.push(`${label} critical: must be a list of field names; ignored`);
|
|
9797
|
+
const isCritical = (field) => critical.some((item) => field === item || field.startsWith(`${item}.`));
|
|
9798
|
+
const problem = (field, message) => {
|
|
9799
|
+
if (isCritical(field))
|
|
9800
|
+
fatal.push(`${label} ${field}: ${message}`);
|
|
9801
|
+
else
|
|
9802
|
+
out.issues.push(`${label} ${field}: ${message}`);
|
|
9803
|
+
};
|
|
9804
|
+
for (const key of Object.keys(entry))
|
|
9805
|
+
if (!KNOWN_FIELDS.has(key))
|
|
9806
|
+
problem(key, "unknown field, ignored");
|
|
9807
|
+
const productionBranch = branchName(entry.productionBranch);
|
|
9808
|
+
const integrationBranch = branchName(entry.integrationBranch);
|
|
9809
|
+
if (!productionBranch)
|
|
9810
|
+
problem("productionBranch", "missing or not a branch name");
|
|
9811
|
+
if (!integrationBranch)
|
|
9812
|
+
problem("integrationBranch", "missing or not a branch name");
|
|
9813
|
+
const optionalBranch = (field) => {
|
|
9814
|
+
if (entry[field] === undefined)
|
|
9975
9815
|
return null;
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9816
|
+
const value = branchName(entry[field]);
|
|
9817
|
+
if (!value)
|
|
9818
|
+
problem(field, "not a branch name; using the integration branch");
|
|
9819
|
+
return value;
|
|
9820
|
+
};
|
|
9821
|
+
const baseBranch = optionalBranch("baseBranch");
|
|
9822
|
+
const pullRequestTarget = optionalBranch("pullRequestTarget");
|
|
9823
|
+
let mergeBackBranches = [];
|
|
9824
|
+
if (entry.mergeBackBranches !== undefined) {
|
|
9825
|
+
const list = Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : [null];
|
|
9826
|
+
if (list.some((item) => item === null))
|
|
9827
|
+
problem("mergeBackBranches", "must be a list of branch names; ignored");
|
|
9828
|
+
else
|
|
9829
|
+
mergeBackBranches = [...new Set(list)];
|
|
9830
|
+
}
|
|
9831
|
+
const naming = {};
|
|
9832
|
+
if (entry.naming !== undefined) {
|
|
9833
|
+
if (!isRecord(entry.naming))
|
|
9834
|
+
problem("naming", "not an object; ignored");
|
|
9835
|
+
else
|
|
9836
|
+
for (const kind of ["feature", "release", "hotfix"]) {
|
|
9837
|
+
const template = entry.naming[kind];
|
|
9838
|
+
if (template === undefined)
|
|
9839
|
+
continue;
|
|
9840
|
+
if (typeof template !== "string" || !template.trim())
|
|
9841
|
+
problem(`naming.${kind}`, "not a template string; ignored");
|
|
9842
|
+
else if (!literalOf(template))
|
|
9843
|
+
problem(`naming.${kind}`, "needs literal text besides placeholders; ignored");
|
|
9844
|
+
else
|
|
9845
|
+
naming[kind] = template.trim();
|
|
9846
|
+
}
|
|
9983
9847
|
}
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9848
|
+
if (!productionBranch || !integrationBranch) {
|
|
9849
|
+
out.issues.push(`${label} was skipped: it needs a productionBranch and integrationBranch`);
|
|
9850
|
+
continue;
|
|
9851
|
+
}
|
|
9852
|
+
out.tracks.push({
|
|
9853
|
+
name,
|
|
9854
|
+
productionBranch,
|
|
9855
|
+
integrationBranch,
|
|
9856
|
+
baseBranch: baseBranch ?? integrationBranch,
|
|
9857
|
+
pullRequestTarget: pullRequestTarget ?? integrationBranch,
|
|
9858
|
+
mergeBackBranches,
|
|
9859
|
+
naming
|
|
9860
|
+
});
|
|
9988
9861
|
}
|
|
9862
|
+
out.protectedBranches = [...protectedNames];
|
|
9863
|
+
if (fatal.length)
|
|
9864
|
+
out.error = `workspace releaseTracks cannot be read by this Workit (${fatal.join("; ")}); upgrade Workit or fix the track`;
|
|
9865
|
+
return out;
|
|
9989
9866
|
}
|
|
9990
|
-
function
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
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 }
|
|
10007
|
-
};
|
|
10008
|
-
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
10009
|
-
const common = run.stdout.trim();
|
|
10010
|
-
if (run.ok && common) {
|
|
10011
|
-
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
10012
|
-
return {
|
|
10013
|
-
dir: path6.join(real(path6.resolve(root, common)), "workit"),
|
|
10014
|
-
shared: true,
|
|
10015
|
-
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
10016
|
-
};
|
|
9867
|
+
function resolveReleaseTrack(input) {
|
|
9868
|
+
const tracks = [...input.tracks];
|
|
9869
|
+
const issues = [...input.issues ?? []];
|
|
9870
|
+
const requested = input.requested?.trim();
|
|
9871
|
+
if (tracks.length === 0) {
|
|
9872
|
+
if (requested)
|
|
9873
|
+
return {
|
|
9874
|
+
status: "invalid",
|
|
9875
|
+
error: `no release tracks configured for this workspace; --track ${requested} cannot apply`,
|
|
9876
|
+
issues
|
|
9877
|
+
};
|
|
9878
|
+
return { status: "none", issues };
|
|
10017
9879
|
}
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
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);
|
|
9880
|
+
const names = tracks.map((track) => track.name).join(", ");
|
|
9881
|
+
let envProblem = null;
|
|
9882
|
+
const done = (track, source, detail, warnings = [], ambiguous = null) => ({
|
|
9883
|
+
status: "resolved",
|
|
9884
|
+
track: track && track.name !== IMPLICIT ? track : null,
|
|
9885
|
+
source,
|
|
9886
|
+
detail,
|
|
9887
|
+
warnings: [...issues, ...envProblem ? [envProblem] : [], ...warnings],
|
|
9888
|
+
tracks,
|
|
9889
|
+
blocking: envProblem ?? ambiguous
|
|
9890
|
+
});
|
|
9891
|
+
if (requested) {
|
|
9892
|
+
const named = tracks.find((track) => track.name === requested);
|
|
9893
|
+
if (!named)
|
|
10039
9894
|
return {
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
9895
|
+
status: "invalid",
|
|
9896
|
+
error: `no release track ${JSON.stringify(requested)}; configured: ${names}`,
|
|
9897
|
+
issues
|
|
10043
9898
|
};
|
|
10044
|
-
}
|
|
9899
|
+
return done(named, "flag", `track ${named.name} was requested explicitly`);
|
|
10045
9900
|
}
|
|
10046
|
-
const
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
9901
|
+
const owned = (branch) => branch !== null && tracks.some((track) => [
|
|
9902
|
+
track.integrationBranch,
|
|
9903
|
+
track.productionBranch,
|
|
9904
|
+
track.baseBranch,
|
|
9905
|
+
track.pullRequestTarget
|
|
9906
|
+
].includes(branch));
|
|
9907
|
+
const implicit = input.defaultBranch && !owned(input.defaultBranch) ? {
|
|
9908
|
+
name: IMPLICIT,
|
|
9909
|
+
productionBranch: input.defaultBranch,
|
|
9910
|
+
integrationBranch: input.defaultBranch,
|
|
9911
|
+
baseBranch: input.defaultBranch,
|
|
9912
|
+
pullRequestTarget: input.defaultBranch,
|
|
9913
|
+
mergeBackBranches: [],
|
|
9914
|
+
naming: {}
|
|
9915
|
+
} : null;
|
|
9916
|
+
const candidates = implicit ? [...tracks, implicit] : tracks;
|
|
9917
|
+
const label = (track) => track.name === IMPLICIT ? `the workspace default line (${track.integrationBranch})` : `track ${track.name}`;
|
|
9918
|
+
const fallback = (why) => {
|
|
9919
|
+
const byDefault = input.defaultBranch ? tracks.filter((track) => track.integrationBranch === input.defaultBranch || track.pullRequestTarget === input.defaultBranch) : [];
|
|
9920
|
+
const track = byDefault.length === 1 ? byDefault[0] : null;
|
|
9921
|
+
const target = track ? `track ${track.name}` : `default target ${input.defaultBranch ?? "(none)"}`;
|
|
9922
|
+
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;
|
|
9923
|
+
return done(track, "default", `${why}; using the workspace ${target}`, [
|
|
9924
|
+
`release track not determined: ${why}; using the workspace ${target} (pass --track <name> to choose)`
|
|
9925
|
+
], ambiguous);
|
|
9926
|
+
};
|
|
9927
|
+
const branch = input.branch;
|
|
9928
|
+
if (branch) {
|
|
9929
|
+
const owners = ownedBy(candidates, branch);
|
|
9930
|
+
if (owners.length === 1)
|
|
9931
|
+
return done(owners[0], "branch", `${branch} is a branch of ${label(owners[0])}`);
|
|
9932
|
+
if (owners.length > 1)
|
|
9933
|
+
return fallback(`${branch} belongs to more than one track (${owners.map((track) => track.name).join(", ")})`);
|
|
10055
9934
|
}
|
|
10056
|
-
const
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
let canonical = resolved;
|
|
10063
|
-
try {
|
|
10064
|
-
canonical = fs3.realpathSync(resolved);
|
|
10065
|
-
} catch {
|
|
10066
|
-
return resolved;
|
|
9935
|
+
const env = input.envRequested?.trim();
|
|
9936
|
+
if (env) {
|
|
9937
|
+
const named = tracks.find((track) => track.name === env);
|
|
9938
|
+
if (named)
|
|
9939
|
+
return done(named, "env", `WORKFLOW_RELEASE_TRACK selects track ${named.name}`);
|
|
9940
|
+
envProblem = `WORKFLOW_RELEASE_TRACK=${env} names no release track of this workspace (${names}); ignored`;
|
|
10067
9941
|
}
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
return
|
|
9942
|
+
if (!branch)
|
|
9943
|
+
return fallback("HEAD is detached");
|
|
9944
|
+
if (tracks.length === 1 && !implicit)
|
|
9945
|
+
return done(tracks[0], "only-track", `${tracks[0].name} is the only track`);
|
|
9946
|
+
let remotes = null;
|
|
9947
|
+
const short = (ref) => shortRef(ref, remotes ??= input.probe.remotes());
|
|
9948
|
+
const ownerOf = (ref) => {
|
|
9949
|
+
const owner = ownedBy(candidates, short(ref));
|
|
9950
|
+
return owner.length === 1 ? owner[0] : null;
|
|
9951
|
+
};
|
|
9952
|
+
const followRecorded = (start) => {
|
|
9953
|
+
let cursor = start;
|
|
9954
|
+
const seen = new Set([start]);
|
|
9955
|
+
for (let hop = 0;hop < MAX_RECORDED_HOPS; hop += 1) {
|
|
9956
|
+
const recorded = input.probe.recordedBase(cursor);
|
|
9957
|
+
if (!recorded)
|
|
9958
|
+
return null;
|
|
9959
|
+
const base = short(recorded);
|
|
9960
|
+
const owner = ownerOf(base);
|
|
9961
|
+
if (owner)
|
|
9962
|
+
return owner;
|
|
9963
|
+
if (ownedBy(candidates, base).length > 1 || seen.has(base))
|
|
9964
|
+
return null;
|
|
9965
|
+
seen.add(base);
|
|
9966
|
+
cursor = base;
|
|
9967
|
+
}
|
|
9968
|
+
return null;
|
|
9969
|
+
};
|
|
9970
|
+
const recordedOwner = followRecorded(branch);
|
|
9971
|
+
if (recordedOwner)
|
|
9972
|
+
return done(recordedOwner, "recorded-base", `${branch} was created on ${label(recordedOwner)}'s line (recorded base)`);
|
|
9973
|
+
for (const source of ["reflog", "checkout"]) {
|
|
9974
|
+
const ref = source === "reflog" ? input.probe.createdFrom(branch) : input.probe.checkedOutFrom(branch);
|
|
9975
|
+
if (!ref)
|
|
9976
|
+
continue;
|
|
9977
|
+
const owner = ownerOf(ref) ?? followRecorded(short(ref));
|
|
9978
|
+
if (owner)
|
|
9979
|
+
return done(owner, source, `${branch} was created from ${short(ref)} (${source === "reflog" ? "branch reflog" : "HEAD reflog"})`);
|
|
9980
|
+
}
|
|
9981
|
+
const forks = [];
|
|
9982
|
+
for (const track of candidates)
|
|
9983
|
+
for (const ref of new Set([track.integrationBranch, track.baseBranch])) {
|
|
9984
|
+
const sha = input.probe.mergeBase(branch, ref);
|
|
9985
|
+
if (sha)
|
|
9986
|
+
forks.push({ track, ref, sha });
|
|
9987
|
+
}
|
|
9988
|
+
if (forks.length === 0)
|
|
9989
|
+
return fallback(`no track branch could be compared with ${branch} (fetch the track branches)`);
|
|
9990
|
+
const ancestry = new Map;
|
|
9991
|
+
const descends = (older, newer) => {
|
|
9992
|
+
if (older === newer)
|
|
9993
|
+
return false;
|
|
9994
|
+
const key = `${older}>${newer}`;
|
|
9995
|
+
if (!ancestry.has(key))
|
|
9996
|
+
ancestry.set(key, input.probe.isAncestor(older, newer));
|
|
9997
|
+
return ancestry.get(key);
|
|
9998
|
+
};
|
|
9999
|
+
const winners = forks.filter((fork) => forks.every((other) => other.track === fork.track || descends(other.sha, fork.sha)));
|
|
10000
|
+
const winningTracks = [...new Set(winners.map((fork) => fork.track))];
|
|
10001
|
+
if (winningTracks.length === 1 && forks.some((fork) => fork.track !== winningTracks[0])) {
|
|
10002
|
+
const win = winners[0];
|
|
10003
|
+
return done(win.track, "ancestry", `${branch} forked from ${win.ref} at ${win.sha.slice(0, 12)}, after every other line's fork point`);
|
|
10077
10004
|
}
|
|
10005
|
+
if (winningTracks.length === 1)
|
|
10006
|
+
return fallback(`only ${label(winningTracks[0])} could be compared with ${branch}`);
|
|
10007
|
+
const newest = forks.filter((fork) => !forks.some((other) => descends(fork.sha, other.sha)));
|
|
10008
|
+
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 ")}`);
|
|
10009
|
+
}
|
|
10010
|
+
function gitTrackProbe(cwd) {
|
|
10011
|
+
let remote;
|
|
10012
|
+
let remotes;
|
|
10013
|
+
const tip = (branch) => {
|
|
10014
|
+
remote ??= pushRemoteName(cwd) ?? "origin";
|
|
10015
|
+
return resolveRef(cwd, `refs/remotes/${remote}/${branch}`) ?? resolveRef(cwd, `refs/heads/${branch}`);
|
|
10016
|
+
};
|
|
10017
|
+
return {
|
|
10018
|
+
remotes: () => remotes ??= remoteNames(cwd),
|
|
10019
|
+
recordedBase: (branch) => git2(cwd, ["config", "--get", recordedBaseKey(branch)]) || null,
|
|
10020
|
+
createdFrom: (branch) => {
|
|
10021
|
+
const log = git2(cwd, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`, "--"]);
|
|
10022
|
+
const oldest = log?.split(`
|
|
10023
|
+
`).findLast(Boolean) ?? "";
|
|
10024
|
+
const match = /^branch: Created from (\S+)$/u.exec(oldest);
|
|
10025
|
+
return match && match[1] !== "HEAD" ? match[1] : null;
|
|
10026
|
+
},
|
|
10027
|
+
checkedOutFrom: (branch) => {
|
|
10028
|
+
const own = reflog(cwd, `refs/heads/${branch}`);
|
|
10029
|
+
const created = own.at(-1);
|
|
10030
|
+
if (!created || created.subject !== "branch: Created from HEAD")
|
|
10031
|
+
return null;
|
|
10032
|
+
const pattern = new RegExp(`^checkout: moving from (\\S+) to ${escapeRegExp(branch)}$`, "u");
|
|
10033
|
+
const entry = reflog(cwd, "HEAD").findLast((item) => item.time >= created.time && item.sha === created.sha && pattern.test(item.subject));
|
|
10034
|
+
return entry ? pattern.exec(entry.subject)?.[1] ?? null : null;
|
|
10035
|
+
},
|
|
10036
|
+
mergeBase: (branch, trackBranch) => {
|
|
10037
|
+
const base = tip(trackBranch);
|
|
10038
|
+
return base && resolveRef(cwd, `refs/heads/${branch}`) ? mergeBase(cwd, base, `refs/heads/${branch}`) : null;
|
|
10039
|
+
},
|
|
10040
|
+
isAncestor: (ancestor, descendant) => git2(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]) !== null
|
|
10041
|
+
};
|
|
10078
10042
|
}
|
|
10079
|
-
function
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10043
|
+
function workspaceReleaseTracks(cwd) {
|
|
10044
|
+
const raw = resolveRuntimeReleaseTracks(cwd);
|
|
10045
|
+
return { workspace: raw?.workspace ?? null, ...readReleaseTracks(raw?.releaseTracks) };
|
|
10046
|
+
}
|
|
10047
|
+
function resolveTrackFor(cwd, options) {
|
|
10048
|
+
const read = workspaceReleaseTracks(cwd);
|
|
10049
|
+
if (read.error)
|
|
10050
|
+
return { status: "invalid", error: read.error, issues: read.issues };
|
|
10051
|
+
if (read.tracks.length === 0 && !options.track?.trim())
|
|
10052
|
+
return { status: "none", issues: read.issues };
|
|
10053
|
+
const branch = options.branch === undefined ? currentBranch(cwd) : options.branch;
|
|
10054
|
+
const envRequested = process.env.WORKFLOW_RELEASE_TRACK?.trim() || null;
|
|
10055
|
+
const input = {
|
|
10056
|
+
tracks: read.tracks,
|
|
10057
|
+
requested: options.track ?? null,
|
|
10058
|
+
envRequested,
|
|
10059
|
+
branch,
|
|
10060
|
+
defaultBranch: options.defaultBranch,
|
|
10061
|
+
issues: read.issues
|
|
10095
10062
|
};
|
|
10096
|
-
|
|
10097
|
-
if (
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10063
|
+
let key = null;
|
|
10064
|
+
if (!options.probe) {
|
|
10065
|
+
const refs = git2(cwd, [
|
|
10066
|
+
"for-each-ref",
|
|
10067
|
+
"--format=%(refname) %(objectname)",
|
|
10068
|
+
"refs/heads",
|
|
10069
|
+
"refs/remotes"
|
|
10070
|
+
]) ?? "";
|
|
10071
|
+
const head = git2(cwd, ["reflog", "show", "-n", "1", "--format=%H %gs", "HEAD", "--"]) ?? "";
|
|
10072
|
+
const own = branch ? [
|
|
10073
|
+
git2(cwd, [
|
|
10074
|
+
"log",
|
|
10075
|
+
"-g",
|
|
10076
|
+
"-n",
|
|
10077
|
+
"1",
|
|
10078
|
+
"--date=unix",
|
|
10079
|
+
"--format=%H %gd %gs",
|
|
10080
|
+
`refs/heads/${branch}`,
|
|
10081
|
+
"--"
|
|
10082
|
+
]) ?? "",
|
|
10083
|
+
git2(cwd, ["config", "--get", recordedBaseKey(branch)]) ?? ""
|
|
10084
|
+
] : [];
|
|
10085
|
+
key = createHash5("sha256").update(JSON.stringify({ cwd, input, refs, head, own })).digest("hex");
|
|
10086
|
+
const hit = memo2.get(key);
|
|
10087
|
+
if (hit)
|
|
10088
|
+
return hit;
|
|
10116
10089
|
}
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
current = rename.to;
|
|
10125
|
-
since = rename.at;
|
|
10126
|
-
}
|
|
10127
|
-
return current;
|
|
10090
|
+
const resolution = resolveReleaseTrack({ ...input, probe: options.probe ?? gitTrackProbe(cwd) });
|
|
10091
|
+
if (key) {
|
|
10092
|
+
if (memo2.size >= MEMO_LIMIT)
|
|
10093
|
+
memo2.delete(memo2.keys().next().value);
|
|
10094
|
+
memo2.set(key, resolution);
|
|
10095
|
+
}
|
|
10096
|
+
return resolution;
|
|
10128
10097
|
}
|
|
10129
|
-
var
|
|
10098
|
+
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) => [
|
|
10099
|
+
...BRANCH_FIELDS.map((field) => branchName(entry[field])),
|
|
10100
|
+
...Array.isArray(entry.mergeBackBranches) ? entry.mergeBackBranches.map(branchName) : []
|
|
10101
|
+
].filter((name) => name !== null), literalOf = (template) => template.replace(PLACEHOLDER, "").replace(/[*/]/gu, "").trim(), trackBranches = (tracks) => [
|
|
10102
|
+
...new Set(tracks.flatMap((track) => [
|
|
10103
|
+
track.productionBranch,
|
|
10104
|
+
track.integrationBranch,
|
|
10105
|
+
track.baseBranch,
|
|
10106
|
+
track.pullRequestTarget,
|
|
10107
|
+
...track.mergeBackBranches
|
|
10108
|
+
]))
|
|
10109
|
+
], 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) => {
|
|
10110
|
+
const bare = ref.replace(/^refs\/heads\//u, "").replace(/^refs\/remotes\/[^/]+\//u, "");
|
|
10111
|
+
const remote = remotes.find((name) => bare.startsWith(`${name}/`));
|
|
10112
|
+
return remote ? bare.slice(remote.length + 1) : bare;
|
|
10113
|
+
}, MAX_RECORDED_HOPS = 10, git2 = (cwd, args) => {
|
|
10130
10114
|
const run = spawnSync6("git", args, {
|
|
10131
10115
|
cwd,
|
|
10132
10116
|
encoding: "utf8",
|
|
10133
10117
|
timeout: 20000,
|
|
10134
|
-
killSignal: "SIGKILL",
|
|
10135
10118
|
windowsHide: true,
|
|
10136
|
-
|
|
10137
|
-
...process.env,
|
|
10138
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
10139
|
-
GIT_OPTIONAL_LOCKS: "0",
|
|
10140
|
-
LC_ALL: "C",
|
|
10141
|
-
LANGUAGE: "C"
|
|
10142
|
-
}
|
|
10119
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10143
10120
|
});
|
|
10144
|
-
|
|
10121
|
+
return run.status === 0 ? (run.stdout ?? "").trim() : null;
|
|
10122
|
+
}, 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(`
|
|
10123
|
+
`).map((line) => {
|
|
10124
|
+
const [sha = "", selector = "", ...subject] = line.split("\t");
|
|
10145
10125
|
return {
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10126
|
+
sha,
|
|
10127
|
+
time: Number(/@\{(\d+)\}$/u.exec(selector)?.[1] ?? Number.NaN),
|
|
10128
|
+
subject: subject.join("\t")
|
|
10129
|
+
};
|
|
10130
|
+
}).filter((entry) => entry.sha && Number.isFinite(entry.time)), memo2, MEMO_LIMIT = 64;
|
|
10131
|
+
var init_release_tracks = __esm(() => {
|
|
10132
|
+
init_rev();
|
|
10133
|
+
init_config();
|
|
10134
|
+
init_workspaces();
|
|
10135
|
+
ReleaseTrackError = class ReleaseTrackError extends Error {
|
|
10136
|
+
};
|
|
10137
|
+
KNOWN_FIELDS = new Set([
|
|
10138
|
+
"strategy",
|
|
10139
|
+
"productionBranch",
|
|
10140
|
+
"integrationBranch",
|
|
10141
|
+
"naming",
|
|
10142
|
+
"baseBranch",
|
|
10143
|
+
"mergeBackBranches",
|
|
10144
|
+
"pullRequestTarget",
|
|
10145
|
+
"tagNamespace",
|
|
10146
|
+
"versionSource",
|
|
10147
|
+
"requiredChecks",
|
|
10148
|
+
"critical"
|
|
10149
|
+
]);
|
|
10150
|
+
BRANCH_FIELDS = ["productionBranch", "integrationBranch", "baseBranch", "pullRequestTarget"];
|
|
10151
|
+
PLACEHOLDER = /\{[^}]*\}/gu;
|
|
10152
|
+
HAS_PLACEHOLDER = /\{[^}]*\}/u;
|
|
10153
|
+
memo2 = new Map;
|
|
10154
|
+
});
|
|
10155
|
+
|
|
10156
|
+
// packages/workit-core/src/core/vcs-config.ts
|
|
10157
|
+
import fs4 from "node:fs";
|
|
10158
|
+
import path5 from "node:path";
|
|
10159
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
10160
|
+
function vcsConfig(mode, cwd, options = {}) {
|
|
10161
|
+
const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
|
|
10162
|
+
const wsVcs = ws?.vcs ?? {};
|
|
10163
|
+
const wsYt = ws?.youtrack ?? {};
|
|
10164
|
+
const wsIssues = ws?.issues ?? {};
|
|
10165
|
+
const { status: cfgStatus, path: cfgPath, config: cfg, error: cfgError } = readVcsConfig();
|
|
10166
|
+
const root = vcsCwd(cwd);
|
|
10167
|
+
const determinateRoot = cwd !== undefined || process.env.WORKFLOW_WORKSPACE_ROOT !== undefined;
|
|
10168
|
+
const rawProvider = wsVcs.provider ?? (determinateRoot ? remoteProvider(root) : null) ?? cfg.provider ?? null;
|
|
10169
|
+
const provider = rawProvider === null || rawProvider === undefined ? null : String(rawProvider).toLowerCase() || null;
|
|
10170
|
+
const workspaceDefault = workspaceDefaultTarget(root, cfg.defaultTargetBranch);
|
|
10171
|
+
const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
|
|
10172
|
+
const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
|
|
10173
|
+
let issuesProvider = null;
|
|
10174
|
+
let linkOnPr = null;
|
|
10175
|
+
if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
|
|
10176
|
+
issuesProvider = "github";
|
|
10177
|
+
linkOnPr = typeof wsIssues.link_on_pr === "boolean" ? wsIssues.link_on_pr : null;
|
|
10178
|
+
}
|
|
10179
|
+
if (mode === "resolve") {
|
|
10180
|
+
if (cfgStatus === "malformed")
|
|
10181
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10182
|
+
const trackResolution = resolveTrackFor(root, {
|
|
10183
|
+
track: options.track,
|
|
10184
|
+
...options.branch !== undefined ? { branch: options.branch } : {},
|
|
10185
|
+
defaultBranch: workspaceDefault
|
|
10186
|
+
});
|
|
10187
|
+
if (trackResolution.status === "invalid")
|
|
10188
|
+
return { ok: false, error: trackResolution.error, configPath: workspacesPath() };
|
|
10189
|
+
const releaseTrack = trackSummary(trackResolution);
|
|
10190
|
+
const defaultTarget = releaseTrack?.track?.pullRequestTarget ?? workspaceDefault;
|
|
10191
|
+
const baseBranch = releaseTrack?.track?.baseBranch ?? defaultTarget;
|
|
10192
|
+
return {
|
|
10193
|
+
ok: true,
|
|
10194
|
+
workspace_name: ws?.name ?? null,
|
|
10195
|
+
provider,
|
|
10196
|
+
defaultTargetBranch: defaultTarget,
|
|
10197
|
+
workspaceDefaultTargetBranch: workspaceDefault,
|
|
10198
|
+
baseBranch,
|
|
10199
|
+
releaseTrack,
|
|
10200
|
+
link_issues: linkIssues,
|
|
10201
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10202
|
+
issues_provider: issuesProvider,
|
|
10203
|
+
link_on_pr: linkOnPr
|
|
10204
|
+
};
|
|
10205
|
+
}
|
|
10206
|
+
if (cfgStatus === "malformed") {
|
|
10207
|
+
return { ok: false, error: cfgError, configPath: cfgPath };
|
|
10208
|
+
}
|
|
10209
|
+
if (cfgStatus === "missing" && provider === null)
|
|
10210
|
+
return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
|
|
10211
|
+
if (provider === null) {
|
|
10212
|
+
return {
|
|
10213
|
+
ok: false,
|
|
10214
|
+
error: `no vcs provider configured (set vcs.provider in ${cfgPath}, match a workspace entry, or run inside a checkout with a recognized origin remote)`,
|
|
10215
|
+
configPath: cfgPath
|
|
10216
|
+
};
|
|
10217
|
+
}
|
|
10218
|
+
const prov = cfg[provider] ?? {};
|
|
10219
|
+
const out = {
|
|
10220
|
+
ok: true,
|
|
10221
|
+
configPath: path5.resolve(cfgPath),
|
|
10222
|
+
provider,
|
|
10223
|
+
defaultTargetBranch: workspaceDefault,
|
|
10224
|
+
pr: cfg.pr ?? {},
|
|
10225
|
+
workspace_name: ws?.name ?? null,
|
|
10226
|
+
link_issues: linkIssues,
|
|
10227
|
+
youtrack_base_url: youtrackBaseUrl,
|
|
10228
|
+
issues_provider: issuesProvider,
|
|
10229
|
+
link_on_pr: linkOnPr
|
|
10150
10230
|
};
|
|
10151
|
-
|
|
10231
|
+
if (provider === "gitlab") {
|
|
10232
|
+
out.gitlab = {
|
|
10233
|
+
host: prov.host ?? "gitlab.com",
|
|
10234
|
+
apiUrl: prov.apiUrl ?? "https://gitlab.com/api/v4"
|
|
10235
|
+
};
|
|
10236
|
+
} else if (provider === "github") {
|
|
10237
|
+
out.github = { host: prov.host ?? "github.com" };
|
|
10238
|
+
}
|
|
10239
|
+
return out;
|
|
10240
|
+
}
|
|
10241
|
+
var workspacesPath = () => path5.join(configDir(), "workspaces.json"), vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), remoteProvider = (cwd) => {
|
|
10242
|
+
const r = spawnSync7("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
10243
|
+
if (r.status !== 0)
|
|
10244
|
+
return null;
|
|
10245
|
+
const url = (r.stdout ?? "").trim();
|
|
10246
|
+
if (/github\.com[:/]/.test(url))
|
|
10247
|
+
return "github";
|
|
10248
|
+
if (/gitlab\.com[:/]/.test(url))
|
|
10249
|
+
return "gitlab";
|
|
10250
|
+
return null;
|
|
10251
|
+
}, readVcsConfig = () => {
|
|
10252
|
+
const file = vcsConfigPath();
|
|
10253
|
+
let raw;
|
|
10152
10254
|
try {
|
|
10153
|
-
|
|
10255
|
+
raw = fs4.readFileSync(file, "utf8");
|
|
10154
10256
|
} catch {
|
|
10155
|
-
return file;
|
|
10257
|
+
return { status: "missing", path: file, config: {} };
|
|
10156
10258
|
}
|
|
10157
|
-
|
|
10259
|
+
let parsed;
|
|
10158
10260
|
try {
|
|
10159
|
-
|
|
10261
|
+
parsed = JSON.parse(raw);
|
|
10160
10262
|
} catch {
|
|
10161
|
-
return
|
|
10263
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not valid JSON` };
|
|
10162
10264
|
}
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs3.readFileSync(path6.join(common, "config"), "utf8"));
|
|
10166
|
-
} catch {
|
|
10167
|
-
return false;
|
|
10265
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10266
|
+
return { status: "valid", path: file, config: parsed };
|
|
10168
10267
|
}
|
|
10169
|
-
},
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10268
|
+
return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
|
|
10269
|
+
}, trackSummary = (resolution) => resolution.status === "resolved" ? {
|
|
10270
|
+
name: resolution.track?.name ?? null,
|
|
10271
|
+
source: resolution.source,
|
|
10272
|
+
detail: resolution.detail,
|
|
10273
|
+
warnings: resolution.warnings,
|
|
10274
|
+
track: resolution.track,
|
|
10275
|
+
tracks: resolution.tracks,
|
|
10276
|
+
blocking: resolution.blocking
|
|
10277
|
+
} : null;
|
|
10278
|
+
var init_vcs_config = __esm(() => {
|
|
10279
|
+
init_config();
|
|
10280
|
+
init_workspaces();
|
|
10281
|
+
init_release_tracks();
|
|
10282
|
+
});
|
|
10283
|
+
|
|
10284
|
+
// packages/workit-core/src/core/logger.ts
|
|
10285
|
+
import os3 from "node:os";
|
|
10286
|
+
import path6 from "node:path";
|
|
10287
|
+
var SENSITIVE_WORDS, STACK_WORDS, escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), resolveStateDir = () => {
|
|
10288
|
+
const override = process.env.WORKFLOW_TOOLKIT_STATE;
|
|
10289
|
+
if (override)
|
|
10290
|
+
return override;
|
|
10291
|
+
const home = os3.homedir();
|
|
10292
|
+
if (process.env.XDG_STATE_HOME)
|
|
10293
|
+
return path6.join(process.env.XDG_STATE_HOME, "workit");
|
|
10294
|
+
switch (os3.platform()) {
|
|
10295
|
+
case "darwin":
|
|
10296
|
+
return path6.join(home, "Library", "Application Support", "workit");
|
|
10297
|
+
case "win32":
|
|
10298
|
+
return path6.join(process.env.LOCALAPPDATA ?? path6.join(home, "AppData", "Local"), "workit");
|
|
10299
|
+
default:
|
|
10300
|
+
return path6.join(process.env.HOME ?? home, ".local", "state", "workit");
|
|
10177
10301
|
}
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
"
|
|
10190
|
-
"
|
|
10191
|
-
"
|
|
10192
|
-
"
|
|
10193
|
-
"
|
|
10194
|
-
|
|
10302
|
+
}, homePattern;
|
|
10303
|
+
var init_logger = __esm(() => {
|
|
10304
|
+
SENSITIVE_WORDS = new Set([
|
|
10305
|
+
"token",
|
|
10306
|
+
"secret",
|
|
10307
|
+
"password",
|
|
10308
|
+
"passwd",
|
|
10309
|
+
"authorization",
|
|
10310
|
+
"credential",
|
|
10311
|
+
"credentials",
|
|
10312
|
+
"cookie",
|
|
10313
|
+
"api",
|
|
10314
|
+
"apikey",
|
|
10315
|
+
"apitoken",
|
|
10316
|
+
"accesstoken",
|
|
10317
|
+
"clientsecret",
|
|
10318
|
+
"refreshtoken",
|
|
10319
|
+
"key",
|
|
10320
|
+
"bearer",
|
|
10321
|
+
"prompt",
|
|
10322
|
+
"message",
|
|
10323
|
+
"messages",
|
|
10324
|
+
"content",
|
|
10325
|
+
"body",
|
|
10326
|
+
"args",
|
|
10327
|
+
"argument",
|
|
10328
|
+
"result",
|
|
10329
|
+
"results",
|
|
10330
|
+
"output",
|
|
10331
|
+
"issue",
|
|
10332
|
+
"description",
|
|
10333
|
+
"summary",
|
|
10334
|
+
"payload",
|
|
10335
|
+
"script",
|
|
10336
|
+
"command",
|
|
10337
|
+
"text",
|
|
10338
|
+
"query"
|
|
10339
|
+
]);
|
|
10340
|
+
STACK_WORDS = new Set(["stack", "stacktrace", "trace"]);
|
|
10341
|
+
homePattern = new RegExp(`${escapeRegExp2(os3.homedir())}(?=[/\\\\]|$)|\\$HOME(?=[/\\\\]|$)`, "g");
|
|
10195
10342
|
});
|
|
10196
10343
|
|
|
10197
10344
|
// packages/workit-core/src/host-session.ts
|
|
10198
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
10199
|
-
import
|
|
10345
|
+
import { createHash as createHash6, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10346
|
+
import fs5 from "node:fs";
|
|
10200
10347
|
import path7 from "node:path";
|
|
10201
10348
|
function hostSessionFromEnv(env) {
|
|
10202
10349
|
const value = (key) => env[key]?.trim() || null;
|
|
@@ -10225,7 +10372,7 @@ function migrateRepoIssued(cwd) {
|
|
|
10225
10372
|
if (location instanceof Error)
|
|
10226
10373
|
return;
|
|
10227
10374
|
old = path7.join(location.dir, "hooks", "issued");
|
|
10228
|
-
if (!
|
|
10375
|
+
if (!fs5.existsSync(old))
|
|
10229
10376
|
return;
|
|
10230
10377
|
} catch {
|
|
10231
10378
|
return;
|
|
@@ -10233,13 +10380,18 @@ function migrateRepoIssued(cwd) {
|
|
|
10233
10380
|
for (const lead of safeList(old)) {
|
|
10234
10381
|
try {
|
|
10235
10382
|
const from = path7.join(old, lead);
|
|
10236
|
-
if (!
|
|
10383
|
+
if (!fs5.statSync(from).isDirectory())
|
|
10237
10384
|
continue;
|
|
10238
10385
|
const to = path7.join(userIssuedRoot(), lead);
|
|
10239
|
-
|
|
10240
|
-
for (const id of
|
|
10241
|
-
|
|
10242
|
-
|
|
10386
|
+
fs5.mkdirSync(to, { recursive: true, mode: 448 });
|
|
10387
|
+
for (const id of fs5.readdirSync(from)) {
|
|
10388
|
+
try {
|
|
10389
|
+
fs5.copyFileSync(path7.join(from, id), path7.join(to, id), fs5.constants.COPYFILE_EXCL);
|
|
10390
|
+
} catch (error) {
|
|
10391
|
+
if (error.code !== "EEXIST")
|
|
10392
|
+
throw error;
|
|
10393
|
+
}
|
|
10394
|
+
fs5.rmSync(path7.join(from, id), { force: true });
|
|
10243
10395
|
}
|
|
10244
10396
|
removeIfEmpty(from);
|
|
10245
10397
|
} catch {}
|
|
@@ -10247,30 +10399,76 @@ function migrateRepoIssued(cwd) {
|
|
|
10247
10399
|
removeIfEmpty(old);
|
|
10248
10400
|
}
|
|
10249
10401
|
function recordIssuedSession(cwd, lead, issued) {
|
|
10402
|
+
const token = randomBytes(18).toString("base64url");
|
|
10250
10403
|
try {
|
|
10251
10404
|
migrateRepoIssued(cwd);
|
|
10252
|
-
const dir = path7.join(userIssuedRoot(), hash(lead));
|
|
10253
|
-
fs4.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10254
10405
|
const own = hash(issued);
|
|
10255
|
-
|
|
10256
|
-
|
|
10406
|
+
const tokens = path7.join(userTokenRoot(), hash(lead));
|
|
10407
|
+
fs5.mkdirSync(tokens, { recursive: true, mode: 448 });
|
|
10408
|
+
fs5.writeFileSync(path7.join(tokens, own), `${tokenHash(token)}
|
|
10409
|
+
`, { mode: 384, flag: "wx" });
|
|
10410
|
+
const dir = path7.join(userIssuedRoot(), hash(lead));
|
|
10411
|
+
fs5.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10412
|
+
fs5.writeFileSync(path7.join(dir, own), `${ISSUED_SESSIONS_MARKER}
|
|
10413
|
+
`, { mode: 384 });
|
|
10414
|
+
pruneIssued(dir, own, tokens);
|
|
10415
|
+
return token;
|
|
10416
|
+
} catch {
|
|
10417
|
+
return null;
|
|
10418
|
+
}
|
|
10419
|
+
}
|
|
10420
|
+
function pruneIssued(dir, own, tokens) {
|
|
10421
|
+
try {
|
|
10422
|
+
const entries = fs5.readdirSync(dir);
|
|
10257
10423
|
if (entries.length <= ISSUED_PER_LEAD)
|
|
10258
10424
|
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
|
-
|
|
10425
|
+
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);
|
|
10426
|
+
for (const { name } of byAge.slice(0, entries.length - ISSUED_PER_LEAD)) {
|
|
10427
|
+
fs5.rmSync(path7.join(dir, name), { force: true });
|
|
10428
|
+
fs5.rmSync(path7.join(tokens, name), { force: true });
|
|
10429
|
+
}
|
|
10262
10430
|
} catch {}
|
|
10263
10431
|
}
|
|
10264
|
-
|
|
10432
|
+
function issuedBinding(cwd, claimed) {
|
|
10433
|
+
const record = issuedRecord(cwd, claimed);
|
|
10434
|
+
const { stored, leadBound } = issuedToken(claimed);
|
|
10435
|
+
if (stored)
|
|
10436
|
+
return "token";
|
|
10437
|
+
if (record === null)
|
|
10438
|
+
return null;
|
|
10439
|
+
return leadBound ? "token" : "legacy";
|
|
10440
|
+
}
|
|
10441
|
+
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
10442
|
try {
|
|
10266
|
-
return
|
|
10443
|
+
return fs5.readdirSync(dir);
|
|
10267
10444
|
} catch {
|
|
10268
10445
|
return [];
|
|
10269
10446
|
}
|
|
10270
10447
|
}, removeIfEmpty = (dir) => {
|
|
10271
10448
|
try {
|
|
10272
|
-
|
|
10449
|
+
fs5.rmdirSync(dir);
|
|
10273
10450
|
} catch {}
|
|
10451
|
+
}, tokenHash = (token) => createHash6("sha256").update(token).digest("hex"), issuedRecord = (cwd, claimed) => {
|
|
10452
|
+
const cut = claimed.lastIndexOf(":");
|
|
10453
|
+
if (cut <= 0)
|
|
10454
|
+
return null;
|
|
10455
|
+
migrateRepoIssued(cwd);
|
|
10456
|
+
try {
|
|
10457
|
+
return fs5.readFileSync(path7.join(userIssuedRoot(), hash(claimed.slice(0, cut)), hash(claimed)), "utf8");
|
|
10458
|
+
} catch {
|
|
10459
|
+
return null;
|
|
10460
|
+
}
|
|
10461
|
+
}, TOKEN_HASH, issuedToken = (claimed) => {
|
|
10462
|
+
const cut = claimed.lastIndexOf(":");
|
|
10463
|
+
if (cut <= 0)
|
|
10464
|
+
return { stored: null, leadBound: false };
|
|
10465
|
+
const dir = path7.join(userTokenRoot(), hash(claimed.slice(0, cut)));
|
|
10466
|
+
try {
|
|
10467
|
+
const text = fs5.readFileSync(path7.join(dir, hash(claimed)), "utf8");
|
|
10468
|
+
return { stored: TOKEN_HASH.exec(text)?.[1] ?? null, leadBound: true };
|
|
10469
|
+
} catch {
|
|
10470
|
+
return { stored: null, leadBound: safeList(dir).length > 0 };
|
|
10471
|
+
}
|
|
10274
10472
|
}, OWN_CHILD_SHELL;
|
|
10275
10473
|
var init_host_session = __esm(() => {
|
|
10276
10474
|
init_logger();
|
|
@@ -10283,13 +10481,14 @@ var init_host_session = __esm(() => {
|
|
|
10283
10481
|
(env) => env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop" || env.CODEX_ELECTRON_RESOURCES_PATH ? "codex_desktop" : "codex_cli"
|
|
10284
10482
|
]
|
|
10285
10483
|
];
|
|
10484
|
+
TOKEN_HASH = /^([0-9a-f]{64})$/mu;
|
|
10286
10485
|
OWN_CHILD_SHELL = new Set(["opencode", "pi"]);
|
|
10287
10486
|
});
|
|
10288
10487
|
|
|
10289
10488
|
// packages/workit-core/src/ledger.ts
|
|
10290
|
-
import { spawnSync as
|
|
10291
|
-
import { createHash as createHash7, randomBytes } from "node:crypto";
|
|
10292
|
-
import
|
|
10489
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10490
|
+
import { createHash as createHash7, randomBytes as randomBytes2 } from "node:crypto";
|
|
10491
|
+
import fs6 from "node:fs";
|
|
10293
10492
|
import path8 from "node:path";
|
|
10294
10493
|
function resolveCommit(cwd, ref) {
|
|
10295
10494
|
if (!safeRef(ref))
|
|
@@ -10365,7 +10564,7 @@ function ledgerPath(cwd) {
|
|
|
10365
10564
|
const root = storeRoot(cwd);
|
|
10366
10565
|
return root.ok ? { ok: true, value: path8.join(root.value.root, "ledger", "ledger.jsonl") } : root;
|
|
10367
10566
|
}
|
|
10368
|
-
function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
10567
|
+
function withLedgerLock(file, fn, waitMs = hostAwareLockTimeout(LOCK_WAIT_MS)) {
|
|
10369
10568
|
const lock = `${file}.lock`;
|
|
10370
10569
|
const deadline = Date.now() + waitMs;
|
|
10371
10570
|
let token = null;
|
|
@@ -10389,13 +10588,15 @@ function withLedgerLock(file, fn, waitMs = LOCK_WAIT_MS) {
|
|
|
10389
10588
|
}
|
|
10390
10589
|
function appendLine(file, build, options) {
|
|
10391
10590
|
const write = () => {
|
|
10392
|
-
const fd =
|
|
10591
|
+
const fd = fs6.openSync(file, "a+", 384);
|
|
10393
10592
|
try {
|
|
10394
|
-
|
|
10593
|
+
if (process.platform !== "win32")
|
|
10594
|
+
tighten(fd);
|
|
10595
|
+
const size = fs6.fstatSync(fd).size;
|
|
10395
10596
|
let fence = "";
|
|
10396
10597
|
if (size > 0) {
|
|
10397
10598
|
const last = Buffer.alloc(1);
|
|
10398
|
-
|
|
10599
|
+
fs6.readSync(fd, last, 0, 1, size - 1);
|
|
10399
10600
|
if (last[0] !== 10)
|
|
10400
10601
|
fence = `
|
|
10401
10602
|
`;
|
|
@@ -10404,16 +10605,16 @@ function appendLine(file, build, options) {
|
|
|
10404
10605
|
if (!line.ok)
|
|
10405
10606
|
return line;
|
|
10406
10607
|
const bytes = Buffer.from(fence + line.value);
|
|
10407
|
-
const written =
|
|
10608
|
+
const written = fs6.writeSync(fd, bytes);
|
|
10408
10609
|
if (written !== bytes.length)
|
|
10409
10610
|
return err("unavailable", `short write to ${file}: ${written} of ${bytes.length} bytes; the row is torn and will be skipped`);
|
|
10410
10611
|
return { ok: true, value: undefined };
|
|
10411
10612
|
} finally {
|
|
10412
|
-
|
|
10613
|
+
fs6.closeSync(fd);
|
|
10413
10614
|
}
|
|
10414
10615
|
};
|
|
10415
10616
|
try {
|
|
10416
|
-
|
|
10617
|
+
fs6.mkdirSync(path8.dirname(file), { recursive: true });
|
|
10417
10618
|
if (!options.forceLock && !onNetworkFs(path8.dirname(file)))
|
|
10418
10619
|
return write();
|
|
10419
10620
|
const locked = withLedgerLock(file, write);
|
|
@@ -10467,8 +10668,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10467
10668
|
if (!file.ok)
|
|
10468
10669
|
return file;
|
|
10469
10670
|
let text = "";
|
|
10671
|
+
let stat = null;
|
|
10470
10672
|
try {
|
|
10471
|
-
|
|
10673
|
+
stat = fs6.statSync(file.value);
|
|
10674
|
+
text = fs6.readFileSync(file.value, "utf8");
|
|
10472
10675
|
} catch (error) {
|
|
10473
10676
|
if (error.code === "ENOENT")
|
|
10474
10677
|
return {
|
|
@@ -10487,6 +10690,7 @@ function readLedger(cwd, options = {}) {
|
|
|
10487
10690
|
const integrity = { chained: 0, legacy: 0, unverified: [] };
|
|
10488
10691
|
const earlier = new Set([null]);
|
|
10489
10692
|
let chainStarted = false;
|
|
10693
|
+
const ids = new Set;
|
|
10490
10694
|
for (const raw of text.split(`
|
|
10491
10695
|
`)) {
|
|
10492
10696
|
const trimmed = raw.trim();
|
|
@@ -10520,6 +10724,10 @@ function readLedger(cwd, options = {}) {
|
|
|
10520
10724
|
reason = "unsigned";
|
|
10521
10725
|
else
|
|
10522
10726
|
rowIntegrity = "legacy";
|
|
10727
|
+
if (options.integrity && !reason && ids.has(parsed.id))
|
|
10728
|
+
reason = "duplicate_id";
|
|
10729
|
+
if (options.integrity)
|
|
10730
|
+
ids.add(parsed.id);
|
|
10523
10731
|
if (reason)
|
|
10524
10732
|
rowIntegrity = "unverified";
|
|
10525
10733
|
const pr = typeof parsed.pr === "number" && Number.isInteger(parsed.pr) ? parsed.pr : undefined;
|
|
@@ -10543,7 +10751,8 @@ function readLedger(cwd, options = {}) {
|
|
|
10543
10751
|
seq: rows.length + 1,
|
|
10544
10752
|
superseded: false,
|
|
10545
10753
|
supersedeIgnored: false,
|
|
10546
|
-
...rowIntegrity ? { integrity: rowIntegrity } : {}
|
|
10754
|
+
...rowIntegrity ? { integrity: rowIntegrity } : {},
|
|
10755
|
+
...reason ? { integrityReason: reason } : {}
|
|
10547
10756
|
};
|
|
10548
10757
|
rows.push(row);
|
|
10549
10758
|
if (!options.integrity)
|
|
@@ -10571,18 +10780,44 @@ function readLedger(cwd, options = {}) {
|
|
|
10571
10780
|
const byId = new Map;
|
|
10572
10781
|
for (const row of rows) {
|
|
10573
10782
|
const target = row.supersedes ? byId.get(row.supersedes) : undefined;
|
|
10574
|
-
if (target && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10783
|
+
if (target && row.integrity !== "unverified" && supersedeAllowed(row, target, renameAwareBranch(scopeOf, row.branch)) === null)
|
|
10575
10784
|
target.superseded = true;
|
|
10576
10785
|
else if (row.supersedes)
|
|
10577
10786
|
row.supersedeIgnored = true;
|
|
10578
10787
|
if (!byId.has(row.id))
|
|
10579
10788
|
byId.set(row.id, row);
|
|
10580
10789
|
}
|
|
10790
|
+
if (options.integrity && stat && Buffer.byteLength(text) === stat.size)
|
|
10791
|
+
tamperCache.set(file.value, {
|
|
10792
|
+
size: stat.size,
|
|
10793
|
+
stamp: fileStamp(stat),
|
|
10794
|
+
breaks: integrity.unverified.filter((row) => TAMPER_REASONS.has(row.reason))
|
|
10795
|
+
});
|
|
10581
10796
|
return {
|
|
10582
10797
|
ok: true,
|
|
10583
10798
|
value: { path: file.value, rows, skipped, integrity: options.integrity ? integrity : null }
|
|
10584
10799
|
};
|
|
10585
10800
|
}
|
|
10801
|
+
function ledgerTampering(cwd) {
|
|
10802
|
+
const file = ledgerPath(cwd);
|
|
10803
|
+
if (!file.ok)
|
|
10804
|
+
return [];
|
|
10805
|
+
try {
|
|
10806
|
+
const stat = fs6.statSync(file.value);
|
|
10807
|
+
const cached = tamperCache.get(file.value);
|
|
10808
|
+
if (cached && cached.size === stat.size && cached.stamp === fileStamp(stat))
|
|
10809
|
+
return cached.breaks;
|
|
10810
|
+
} catch {
|
|
10811
|
+
return [];
|
|
10812
|
+
}
|
|
10813
|
+
const read = readLedger(cwd, { integrity: true });
|
|
10814
|
+
return read.ok ? (read.value.integrity?.unverified ?? []).filter((row) => TAMPER_REASONS.has(row.reason)) : [];
|
|
10815
|
+
}
|
|
10816
|
+
function tamperedUnblock(cwd) {
|
|
10817
|
+
const file = ledgerPath(cwd);
|
|
10818
|
+
const where = file.ok ? file.value : "the ledger file";
|
|
10819
|
+
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`;
|
|
10820
|
+
}
|
|
10586
10821
|
function verdictBasis(verdict, current) {
|
|
10587
10822
|
if (verdict.head && verdict.head === current.head)
|
|
10588
10823
|
return "fresh";
|
|
@@ -10649,15 +10884,51 @@ function authorSessions(cwd, rows, branch, range = { base: null, head: null }, s
|
|
|
10649
10884
|
sessions.add(session);
|
|
10650
10885
|
return sessions;
|
|
10651
10886
|
}
|
|
10652
|
-
function
|
|
10887
|
+
function sessionTrust(cwd, row) {
|
|
10888
|
+
const session = row.actor.session;
|
|
10889
|
+
if (!session || row.self === true)
|
|
10890
|
+
return null;
|
|
10891
|
+
const source = typeof row.sessionSource === "string" ? row.sessionSource : "env";
|
|
10892
|
+
const host = row.actor.host;
|
|
10893
|
+
const issuedOnly = source === "flag" ? host !== "cli" : source === "env" && session.includes(":") && !SHELL_VOUCHED_HOSTS.has(host);
|
|
10894
|
+
if (!issuedOnly)
|
|
10895
|
+
return null;
|
|
10896
|
+
const binding = issuedBinding(cwd, session);
|
|
10897
|
+
if (binding === null)
|
|
10898
|
+
return "unissued_session";
|
|
10899
|
+
return binding === "token" && row.sessionBound !== true ? "unbound_session" : null;
|
|
10900
|
+
}
|
|
10901
|
+
function trustReasons(cwd, row) {
|
|
10902
|
+
if (row.integrity === "unverified")
|
|
10903
|
+
return [row.integrityReason ?? "unsigned"];
|
|
10904
|
+
if (row.integrity !== "verified")
|
|
10905
|
+
return ["unsigned"];
|
|
10906
|
+
const session = sessionTrust(cwd, row);
|
|
10907
|
+
return session ? [session] : [];
|
|
10908
|
+
}
|
|
10909
|
+
function withIntegrity(cwd, rows) {
|
|
10910
|
+
if (rows.every((row) => row.integrity !== undefined))
|
|
10911
|
+
return rows;
|
|
10912
|
+
const read = readLedger(cwd, { integrity: true });
|
|
10913
|
+
const bySeq = new Map(read.ok ? read.value.rows.map((row) => [row.seq, row]) : []);
|
|
10914
|
+
return rows.map((row) => {
|
|
10915
|
+
if (row.integrity !== undefined)
|
|
10916
|
+
return row;
|
|
10917
|
+
const fresh = bySeq.get(row.seq);
|
|
10918
|
+
return fresh && fresh.id === row.id && fresh.at === row.at ? fresh : row;
|
|
10919
|
+
});
|
|
10920
|
+
}
|
|
10921
|
+
function effectiveOf(rows, reasonsOf, sameCode, isCurrent) {
|
|
10653
10922
|
let effective = null;
|
|
10654
10923
|
for (const row of rows) {
|
|
10655
10924
|
if (!effective) {
|
|
10656
10925
|
effective = row;
|
|
10657
10926
|
continue;
|
|
10658
10927
|
}
|
|
10659
|
-
|
|
10660
|
-
|
|
10928
|
+
if (untrusted(reasonsOf(row)) && !untrusted(reasonsOf(effective)))
|
|
10929
|
+
continue;
|
|
10930
|
+
const rowIndependent = reasonsOf(row).length === 0;
|
|
10931
|
+
const effIndependent = reasonsOf(effective).length === 0;
|
|
10661
10932
|
if (effIndependent && !rowIndependent && isCurrent(effective))
|
|
10662
10933
|
continue;
|
|
10663
10934
|
if (effIndependent && FAILING.has(String(effective.result)) && row.actor.session === effective.actor.session && sameCode(effective, row))
|
|
@@ -10671,7 +10942,7 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10671
10942
|
return false;
|
|
10672
10943
|
const edges = new Map;
|
|
10673
10944
|
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)
|
|
10945
|
+
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
10946
|
edges.set(row.fromHead, [...edges.get(row.fromHead) ?? [], row.head]);
|
|
10676
10947
|
const seen = new Set([from]);
|
|
10677
10948
|
const queue = [from];
|
|
@@ -10687,7 +10958,8 @@ function restackCarries(rows, branch, from, to) {
|
|
|
10687
10958
|
}
|
|
10688
10959
|
return false;
|
|
10689
10960
|
}
|
|
10690
|
-
function checkVerdicts(cwd, branch,
|
|
10961
|
+
function checkVerdicts(cwd, branch, ledgerRows) {
|
|
10962
|
+
const rows = withIntegrity(cwd, ledgerRows);
|
|
10691
10963
|
const head = branchHead(cwd, branch);
|
|
10692
10964
|
const fallbackBase = defaultBase(cwd);
|
|
10693
10965
|
const scope = branchScope(cwd, branch);
|
|
@@ -10717,21 +10989,47 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10717
10989
|
return verdictBasis(failed, { head: row.head, ...keys }) !== "stale" || restackCarries(rows, branch, failed.head, row.head);
|
|
10718
10990
|
};
|
|
10719
10991
|
const authors = authorSessions(cwd, rows, branch, { base: fallbackBase, head }, scope);
|
|
10992
|
+
const failureAuthorCache = new Map;
|
|
10993
|
+
const failureAuthors = (row) => {
|
|
10994
|
+
let set = failureAuthorCache.get(row.seq);
|
|
10995
|
+
if (!set) {
|
|
10996
|
+
set = authorSessions(cwd, rows.filter((other) => other.integrity === "verified" && other.seq < row.seq), branch, { base: null, head: null }, scope);
|
|
10997
|
+
failureAuthorCache.set(row.seq, set);
|
|
10998
|
+
}
|
|
10999
|
+
return set;
|
|
11000
|
+
};
|
|
11001
|
+
const reasonCache = new Map;
|
|
11002
|
+
const reasonsOf = (row) => {
|
|
11003
|
+
let reasons = reasonCache.get(row);
|
|
11004
|
+
if (!reasons) {
|
|
11005
|
+
const failing = FAILING.has(String(row.result));
|
|
11006
|
+
reasons = [
|
|
11007
|
+
...trustReasons(cwd, row),
|
|
11008
|
+
...independenceReasons(row, failing ? failureAuthors(row) : authors)
|
|
11009
|
+
];
|
|
11010
|
+
reasonCache.set(row, reasons);
|
|
11011
|
+
}
|
|
11012
|
+
return reasons;
|
|
11013
|
+
};
|
|
11014
|
+
const retired = new Set;
|
|
11015
|
+
for (const row of rows)
|
|
11016
|
+
if (row.type === "verdict" && row.supersedes && !row.supersedeIgnored && trustReasons(cwd, row).length === 0)
|
|
11017
|
+
retired.add(row.supersedes);
|
|
10720
11018
|
const byKind = new Map;
|
|
10721
11019
|
for (const row of rows)
|
|
10722
|
-
if (isLiveVerdict(row) && scope(row)) {
|
|
11020
|
+
if (isLiveVerdict(row, retired) && scope(row)) {
|
|
10723
11021
|
const kind = typeof row.kind === "string" ? row.kind : "review";
|
|
10724
11022
|
byKind.set(kind, [...byKind.get(kind) ?? [], row]);
|
|
10725
11023
|
}
|
|
10726
11024
|
const verdicts = [];
|
|
10727
11025
|
const warnings = [];
|
|
10728
11026
|
for (const [kind, list] of byKind) {
|
|
10729
|
-
const row = effectiveOf(list,
|
|
11027
|
+
const row = effectiveOf(list, reasonsOf, sameCode, (candidate) => basisOf(candidate) !== "stale");
|
|
10730
11028
|
if (!row)
|
|
10731
11029
|
continue;
|
|
10732
11030
|
const basis = basisOf(row);
|
|
10733
11031
|
const current = basis !== "stale";
|
|
10734
|
-
const independence =
|
|
11032
|
+
const independence = reasonsOf(row);
|
|
10735
11033
|
const reasons = [
|
|
10736
11034
|
...current ? [] : ["stale"],
|
|
10737
11035
|
...PASSING.has(String(row.result)) ? [] : ["not_passing"],
|
|
@@ -10748,23 +11046,31 @@ function checkVerdicts(cwd, branch, rows) {
|
|
|
10748
11046
|
});
|
|
10749
11047
|
if (reasons.length === 0) {
|
|
10750
11048
|
for (const later of list)
|
|
10751
|
-
if (later.seq > row.seq &&
|
|
11049
|
+
if (later.seq > row.seq && reasonsOf(later).length > 0 && !untrusted(reasonsOf(later)) && FAILING.has(String(later.result)) && basisOf(later) !== "stale")
|
|
10752
11050
|
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
11051
|
}
|
|
10754
11052
|
}
|
|
10755
11053
|
for (const row of rows)
|
|
10756
11054
|
if (row.integrity === "unverified" && scope(row))
|
|
10757
11055
|
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)`);
|
|
11056
|
+
const tampered = ledgerTampering(cwd);
|
|
11057
|
+
if (tampered.length) {
|
|
11058
|
+
warnings.push(tamperedWarning(cwd, tampered));
|
|
11059
|
+
for (const entry of verdicts) {
|
|
11060
|
+
entry.accepted = false;
|
|
11061
|
+
entry.reasons = [...entry.reasons, "ledger_tampered"];
|
|
11062
|
+
}
|
|
11063
|
+
}
|
|
10758
11064
|
verdicts.sort((a, b) => a.verdict.seq - b.verdict.seq);
|
|
10759
11065
|
const newestCurrent = verdicts.findLast((entry) => entry.current) ?? verdicts.at(-1) ?? null;
|
|
10760
11066
|
const failing = verdicts.find((entry) => entry.current && entry.independent && FAILING.has(String(entry.verdict.result)));
|
|
10761
11067
|
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: [] } : {
|
|
11068
|
+
const accepted = tampered.length ? { accepted: false, verdict: null, reasons: ["ledger_tampered"] } : newestAccepted && !failing ? { accepted: true, verdict: newestAccepted.verdict, reasons: [] } : {
|
|
10763
11069
|
accepted: false,
|
|
10764
11070
|
verdict: failing?.verdict ?? null,
|
|
10765
11071
|
reasons: failing ? ["failing_verdict"] : !newestCurrent ? ["no_verdict"] : newestCurrent.reasons.length ? newestCurrent.reasons : ["type_check_only"]
|
|
10766
11072
|
};
|
|
10767
|
-
const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && STRONG_RESULTS.has(String(entry.verdict.result)));
|
|
11073
|
+
const selfEntry = failing ? undefined : verdicts.findLast((entry) => entry.current && !entry.independent && !untrusted(entry.reasons) && STRONG_RESULTS.has(String(entry.verdict.result)));
|
|
10768
11074
|
return {
|
|
10769
11075
|
branch,
|
|
10770
11076
|
head,
|
|
@@ -10785,7 +11091,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10785
11091
|
error,
|
|
10786
11092
|
...unblock ? { unblock } : {}
|
|
10787
11093
|
}), gitRun2 = (cwd, args, timeoutMs = GIT_TIMEOUTS.local) => {
|
|
10788
|
-
const run =
|
|
11094
|
+
const run = spawnSync8("git", args, {
|
|
10789
11095
|
cwd,
|
|
10790
11096
|
encoding: "utf8",
|
|
10791
11097
|
timeout: timeoutMs,
|
|
@@ -10819,24 +11125,24 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10819
11125
|
} catch {
|
|
10820
11126
|
return null;
|
|
10821
11127
|
}
|
|
10822
|
-
}, newId2 = () => `${Date.now().toString(36)}-${
|
|
11128
|
+
}, newId2 = () => `${Date.now().toString(36)}-${randomBytes2(6).toString("hex")}`, NETWORK_FS_TYPES, isNetworkFsType = (type) => NETWORK_FS_TYPES.has(type >>> 0), onNetworkFs = (dir) => {
|
|
10823
11129
|
if (process.platform !== "linux")
|
|
10824
11130
|
return false;
|
|
10825
11131
|
try {
|
|
10826
|
-
return isNetworkFsType(
|
|
11132
|
+
return isNetworkFsType(fs6.statfsSync(dir).type);
|
|
10827
11133
|
} catch {
|
|
10828
11134
|
return false;
|
|
10829
11135
|
}
|
|
10830
11136
|
}, LOCK_WAIT_MS = 5000, LOCK_STALE_MS = 30000, LOCK_OWNER = "owner", lockToken = (lock) => {
|
|
10831
11137
|
try {
|
|
10832
|
-
return
|
|
11138
|
+
return fs6.readFileSync(path8.join(lock, LOCK_OWNER), "utf8");
|
|
10833
11139
|
} catch {
|
|
10834
11140
|
return null;
|
|
10835
11141
|
}
|
|
10836
11142
|
}, lockAgeMs = (lock) => {
|
|
10837
11143
|
for (const target of [path8.join(lock, LOCK_OWNER), lock]) {
|
|
10838
11144
|
try {
|
|
10839
|
-
return Date.now() -
|
|
11145
|
+
return Date.now() - fs6.statSync(target).mtimeMs;
|
|
10840
11146
|
} catch {}
|
|
10841
11147
|
}
|
|
10842
11148
|
return null;
|
|
@@ -10844,7 +11150,7 @@ var LEDGER_VERSION = 1, MAX_LINE_BYTES = 4096, PASSING, FAILING, STRONG_RESULTS,
|
|
|
10844
11150
|
${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
10845
11151
|
for (let span = Math.min(size, TAIL_BYTES);span > 0; span = Math.min(size, span * 2)) {
|
|
10846
11152
|
const buffer = Buffer.alloc(span);
|
|
10847
|
-
|
|
11153
|
+
fs6.readSync(fd, buffer, 0, span, size - span);
|
|
10848
11154
|
const end = buffer.lastIndexOf(10);
|
|
10849
11155
|
if (end < 0) {
|
|
10850
11156
|
if (span === size)
|
|
@@ -10872,6 +11178,11 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10872
11178
|
return text || null;
|
|
10873
11179
|
}
|
|
10874
11180
|
return null;
|
|
11181
|
+
}, tighten = (fd) => {
|
|
11182
|
+
try {
|
|
11183
|
+
if ((fs6.fstatSync(fd).mode & 63) !== 0)
|
|
11184
|
+
fs6.fchmodSync(fd, 384);
|
|
11185
|
+
} catch {}
|
|
10875
11186
|
}, sealed = (row, id, at, previous) => {
|
|
10876
11187
|
const { prevHash: _prev, rowHash: _row, ...fields } = row;
|
|
10877
11188
|
const prevHash = previous === null ? null : sha2562(previous);
|
|
@@ -10886,7 +11197,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10886
11197
|
session: str(actor.session),
|
|
10887
11198
|
agentId: str(actor.agentId)
|
|
10888
11199
|
};
|
|
10889
|
-
}, isLiveVerdict = (row) => row.type === "verdict" && typeof row.result === "string" && !row.
|
|
11200
|
+
}, 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
11201
|
return (target) => {
|
|
10891
11202
|
const own = typeof branch === "string" ? branch : null;
|
|
10892
11203
|
const theirs = typeof target.branch === "string" ? target.branch : null;
|
|
@@ -10903,7 +11214,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10903
11214
|
if (derivesFrom(author, session) || derivesFrom(session, author))
|
|
10904
11215
|
return true;
|
|
10905
11216
|
return false;
|
|
10906
|
-
}, independenceReasons = (row, authors) => {
|
|
11217
|
+
}, TRUST_REASONS, SHELL_VOUCHED_HOSTS, untrusted = (reasons) => reasons.some((reason) => TRUST_REASONS.has(reason)), independenceReasons = (row, authors) => {
|
|
10907
11218
|
const reasons = [];
|
|
10908
11219
|
if (!row.actor.session)
|
|
10909
11220
|
reasons.push("no_session");
|
|
@@ -10914,6 +11225,7 @@ ${body}`), TAIL_BYTES, lastLine = (fd, size) => {
|
|
|
10914
11225
|
return reasons;
|
|
10915
11226
|
}, OBSERVED_ONLY;
|
|
10916
11227
|
var init_ledger = __esm(() => {
|
|
11228
|
+
init_store_lock();
|
|
10917
11229
|
init_vcs_config();
|
|
10918
11230
|
init_rev();
|
|
10919
11231
|
init_host_session();
|
|
@@ -10937,56 +11249,73 @@ var init_ledger = __esm(() => {
|
|
|
10937
11249
|
ledgerLock = {
|
|
10938
11250
|
acquire(lock) {
|
|
10939
11251
|
try {
|
|
10940
|
-
|
|
11252
|
+
fs6.mkdirSync(lock);
|
|
10941
11253
|
} catch (error) {
|
|
10942
11254
|
if (error.code === "EEXIST")
|
|
10943
11255
|
return null;
|
|
10944
11256
|
throw error;
|
|
10945
11257
|
}
|
|
10946
|
-
const token = `${process.pid}-${
|
|
10947
|
-
|
|
11258
|
+
const token = `${process.pid}-${randomBytes2(8).toString("hex")}`;
|
|
11259
|
+
fs6.writeFileSync(path8.join(lock, LOCK_OWNER), token);
|
|
10948
11260
|
return token;
|
|
10949
11261
|
},
|
|
10950
11262
|
release(lock, token) {
|
|
10951
11263
|
if (lockToken(lock) !== token)
|
|
10952
11264
|
return false;
|
|
10953
|
-
const tomb = `${lock}.release-${
|
|
11265
|
+
const tomb = `${lock}.release-${randomBytes2(6).toString("hex")}`;
|
|
10954
11266
|
try {
|
|
10955
|
-
|
|
11267
|
+
fs6.renameSync(lock, tomb);
|
|
10956
11268
|
} catch {
|
|
10957
11269
|
return false;
|
|
10958
11270
|
}
|
|
10959
11271
|
if (lockToken(tomb) !== token) {
|
|
10960
11272
|
try {
|
|
10961
|
-
|
|
11273
|
+
fs6.renameSync(tomb, lock);
|
|
10962
11274
|
} catch {}
|
|
10963
11275
|
return false;
|
|
10964
11276
|
}
|
|
10965
|
-
|
|
11277
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10966
11278
|
return true;
|
|
10967
11279
|
},
|
|
10968
11280
|
token: lockToken,
|
|
10969
11281
|
reap(lock, observed) {
|
|
10970
|
-
const tomb = `${lock}.reap-${
|
|
11282
|
+
const tomb = `${lock}.reap-${randomBytes2(6).toString("hex")}`;
|
|
10971
11283
|
try {
|
|
10972
|
-
|
|
11284
|
+
fs6.renameSync(lock, tomb);
|
|
10973
11285
|
} catch {
|
|
10974
11286
|
return false;
|
|
10975
11287
|
}
|
|
10976
11288
|
if (lockToken(tomb) === observed) {
|
|
10977
|
-
|
|
11289
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10978
11290
|
return true;
|
|
10979
11291
|
}
|
|
10980
11292
|
try {
|
|
10981
|
-
|
|
11293
|
+
fs6.renameSync(tomb, lock);
|
|
10982
11294
|
} catch {
|
|
10983
|
-
|
|
11295
|
+
fs6.rmSync(tomb, { recursive: true, force: true });
|
|
10984
11296
|
}
|
|
10985
11297
|
return false;
|
|
10986
11298
|
}
|
|
10987
11299
|
};
|
|
10988
11300
|
TAIL_BYTES = 64 * 1024;
|
|
11301
|
+
TAMPER_REASONS = new Set(["hash_mismatch", "broken_link"]);
|
|
11302
|
+
tamperCache = new Map;
|
|
10989
11303
|
RENAMED = /^branch: renamed refs\/heads\/(.+) to refs\/heads\/(.+)$/iu;
|
|
11304
|
+
TRUST_REASONS = new Set([
|
|
11305
|
+
"unsigned",
|
|
11306
|
+
"hash_mismatch",
|
|
11307
|
+
"broken_link",
|
|
11308
|
+
"duplicate_id",
|
|
11309
|
+
"unissued_session",
|
|
11310
|
+
"unbound_session"
|
|
11311
|
+
]);
|
|
11312
|
+
SHELL_VOUCHED_HOSTS = new Set([
|
|
11313
|
+
"cli",
|
|
11314
|
+
"opencode",
|
|
11315
|
+
"pi",
|
|
11316
|
+
"codex_cli",
|
|
11317
|
+
"codex_desktop"
|
|
11318
|
+
]);
|
|
10990
11319
|
OBSERVED_ONLY = new Set([
|
|
10991
11320
|
"check",
|
|
10992
11321
|
"merge.unverified",
|
|
@@ -11936,7 +12265,7 @@ function normalizeMaxBytes(value, options = {}) {
|
|
|
11936
12265
|
}
|
|
11937
12266
|
|
|
11938
12267
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
11939
|
-
import
|
|
12268
|
+
import fs7 from "node:fs";
|
|
11940
12269
|
function createScratchBuffer(maxBytes) {
|
|
11941
12270
|
const initialReadBytes = Number.isFinite(maxBytes) ? Math.min(READ_CHUNK_BYTES, maxBytes + 1) : READ_CHUNK_BYTES;
|
|
11942
12271
|
return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
|
|
@@ -11959,7 +12288,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
11959
12288
|
let total = 0;
|
|
11960
12289
|
while (true) {
|
|
11961
12290
|
const length = nextReadLength(total, maxBytes, scratch.length);
|
|
11962
|
-
const bytesRead =
|
|
12291
|
+
const bytesRead = fs7.readSync(fd, scratch, 0, length, null);
|
|
11963
12292
|
if (bytesRead === 0) {
|
|
11964
12293
|
return Buffer.concat(chunks, total);
|
|
11965
12294
|
}
|
|
@@ -11973,9 +12302,9 @@ var init_bounded_read = __esm(() => {
|
|
|
11973
12302
|
});
|
|
11974
12303
|
|
|
11975
12304
|
// node_modules/@openclaw/fs-safe/dist/read-open-flags.js
|
|
11976
|
-
import
|
|
12305
|
+
import fs8 from "node:fs";
|
|
11977
12306
|
function resolveReadOpenFlags(options) {
|
|
11978
|
-
const constants = options?.constants ??
|
|
12307
|
+
const constants = options?.constants ?? fs8.constants;
|
|
11979
12308
|
const noFollow = process.platform !== "win32" && options?.followSymlinks !== true && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
11980
12309
|
const nonBlocking = process.platform !== "win32" && typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
|
|
11981
12310
|
return constants.O_RDONLY | noFollow | nonBlocking;
|
|
@@ -11983,7 +12312,7 @@ function resolveReadOpenFlags(options) {
|
|
|
11983
12312
|
var init_read_open_flags = () => {};
|
|
11984
12313
|
|
|
11985
12314
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
11986
|
-
import { randomBytes as
|
|
12315
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
11987
12316
|
import fsSync from "node:fs";
|
|
11988
12317
|
import path11 from "node:path";
|
|
11989
12318
|
function parseSidecarLockSnapshot(snapshot, parser) {
|
|
@@ -11991,7 +12320,7 @@ function parseSidecarLockSnapshot(snapshot, parser) {
|
|
|
11991
12320
|
}
|
|
11992
12321
|
function createSidecarLockOwnershipToken() {
|
|
11993
12322
|
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
11994
|
-
for (const byte of
|
|
12323
|
+
for (const byte of randomBytes3(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
11995
12324
|
for (let bit = 7;bit >= 0; bit -= 1) {
|
|
11996
12325
|
token += byte & 1 << bit ? "\t" : " ";
|
|
11997
12326
|
}
|
|
@@ -12188,7 +12517,7 @@ function sleepSync(ms) {
|
|
|
12188
12517
|
}
|
|
12189
12518
|
|
|
12190
12519
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
12191
|
-
import
|
|
12520
|
+
import fs9 from "node:fs";
|
|
12192
12521
|
import path12 from "node:path";
|
|
12193
12522
|
function getSyncHeldLocks() {
|
|
12194
12523
|
const globalWithState = globalThis;
|
|
@@ -12206,7 +12535,7 @@ function releaseAllSyncHeldLocks() {
|
|
|
12206
12535
|
}
|
|
12207
12536
|
try {
|
|
12208
12537
|
if (held.fd !== undefined)
|
|
12209
|
-
|
|
12538
|
+
fs9.closeSync(held.fd);
|
|
12210
12539
|
} catch {}
|
|
12211
12540
|
try {
|
|
12212
12541
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
@@ -12241,7 +12570,7 @@ function releaseSyncHeldLock(held) {
|
|
|
12241
12570
|
if (held.fd !== undefined) {
|
|
12242
12571
|
const fd = held.fd;
|
|
12243
12572
|
held.fd = undefined;
|
|
12244
|
-
|
|
12573
|
+
fs9.closeSync(fd);
|
|
12245
12574
|
}
|
|
12246
12575
|
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
12247
12576
|
heldLocks.delete(held.normalizedTargetPath);
|
|
@@ -12264,11 +12593,11 @@ function createSyncHeldLockHandle(held) {
|
|
|
12264
12593
|
};
|
|
12265
12594
|
}
|
|
12266
12595
|
function canonicalLockParentSync(parent) {
|
|
12267
|
-
return process.platform === "win32" ?
|
|
12596
|
+
return process.platform === "win32" ? fs9.realpathSync.native(parent) : fs9.realpathSync(parent);
|
|
12268
12597
|
}
|
|
12269
12598
|
function normalizeTargetPath(targetPath) {
|
|
12270
12599
|
const resolved = path12.resolve(targetPath);
|
|
12271
|
-
|
|
12600
|
+
fs9.mkdirSync(path12.dirname(resolved), { recursive: true });
|
|
12272
12601
|
try {
|
|
12273
12602
|
return path12.join(canonicalLockParentSync(path12.dirname(resolved)), path12.basename(resolved));
|
|
12274
12603
|
} catch {
|
|
@@ -12296,7 +12625,7 @@ function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
|
12296
12625
|
}
|
|
12297
12626
|
function reclaimGuardExists(reclaimGuardPath) {
|
|
12298
12627
|
try {
|
|
12299
|
-
|
|
12628
|
+
fs9.lstatSync(reclaimGuardPath);
|
|
12300
12629
|
return true;
|
|
12301
12630
|
} catch (error) {
|
|
12302
12631
|
if (error.code === "ENOENT")
|
|
@@ -12360,19 +12689,19 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12360
12689
|
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
12361
12690
|
let lockFileCreateDenied = false;
|
|
12362
12691
|
try {
|
|
12363
|
-
const noFollow = process.platform !== "win32" && typeof
|
|
12692
|
+
const noFollow = process.platform !== "win32" && typeof fs9.constants.O_NOFOLLOW === "number" ? fs9.constants.O_NOFOLLOW : 0;
|
|
12364
12693
|
try {
|
|
12365
|
-
fd =
|
|
12694
|
+
fd = fs9.openSync(lockPath, fs9.constants.O_WRONLY | fs9.constants.O_CREAT | fs9.constants.O_EXCL | noFollow, 384);
|
|
12366
12695
|
} catch (error) {
|
|
12367
12696
|
lockFileCreateDenied = isTransientLockFileDenial(error, lockPath);
|
|
12368
12697
|
throw error;
|
|
12369
12698
|
}
|
|
12370
|
-
|
|
12371
|
-
|
|
12699
|
+
fs9.writeFileSync(fd, raw, "utf8");
|
|
12700
|
+
fs9.fsyncSync(fd);
|
|
12372
12701
|
const snapshot = {
|
|
12373
12702
|
raw,
|
|
12374
12703
|
payload,
|
|
12375
|
-
stat:
|
|
12704
|
+
stat: fs9.fstatSync(fd),
|
|
12376
12705
|
ownershipToken
|
|
12377
12706
|
};
|
|
12378
12707
|
const createdHeld = {
|
|
@@ -12407,8 +12736,8 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12407
12736
|
return returnedHandle;
|
|
12408
12737
|
} catch (error) {
|
|
12409
12738
|
if (fd !== undefined) {
|
|
12410
|
-
const failed = { payload: null, stat:
|
|
12411
|
-
|
|
12739
|
+
const failed = { payload: null, stat: fs9.fstatSync(fd) };
|
|
12740
|
+
fs9.closeSync(fd);
|
|
12412
12741
|
fd = undefined;
|
|
12413
12742
|
removeSidecarLockIfUnchangedSync(lockPath, failed);
|
|
12414
12743
|
}
|
|
@@ -12457,7 +12786,7 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12457
12786
|
})) {
|
|
12458
12787
|
let ownsReclaimGuard = false;
|
|
12459
12788
|
try {
|
|
12460
|
-
|
|
12789
|
+
fs9.mkdirSync(reclaimGuardPath);
|
|
12461
12790
|
ownsReclaimGuard = true;
|
|
12462
12791
|
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
12463
12792
|
continue;
|
|
@@ -12470,182 +12799,51 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
12470
12799
|
} finally {
|
|
12471
12800
|
if (ownsReclaimGuard) {
|
|
12472
12801
|
try {
|
|
12473
|
-
|
|
12802
|
+
fs9.rmdirSync(reclaimGuardPath);
|
|
12474
12803
|
} catch {}
|
|
12475
12804
|
}
|
|
12476
12805
|
}
|
|
12477
12806
|
}
|
|
12478
12807
|
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
12479
|
-
code: "file_lock_stale",
|
|
12480
|
-
lockPath,
|
|
12481
|
-
normalizedTargetPath
|
|
12482
|
-
});
|
|
12483
|
-
}
|
|
12484
|
-
waitForRetry();
|
|
12485
|
-
}
|
|
12486
|
-
}
|
|
12487
|
-
}
|
|
12488
|
-
var SYNC_HELD_LOCKS_KEY, SYNC_CLEANUP_REGISTERED_KEY, SYNC_CLEANUP_HANDLER_KEY;
|
|
12489
|
-
var init_file_lock_sync = __esm(() => {
|
|
12490
|
-
init_errors3();
|
|
12491
|
-
init_sidecar_lock_reclaim();
|
|
12492
|
-
init_sidecar_lock_policy();
|
|
12493
|
-
init_lock_config();
|
|
12494
|
-
SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
12495
|
-
SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
12496
|
-
SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
12497
|
-
});
|
|
12498
|
-
|
|
12499
|
-
// node_modules/@openclaw/fs-safe/dist/file-lock.js
|
|
12500
|
-
var init_file_lock = __esm(() => {
|
|
12501
|
-
init_file_lock_sync();
|
|
12502
|
-
});
|
|
12503
|
-
|
|
12504
|
-
// packages/workit-core/src/core/package-root.ts
|
|
12505
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
12506
|
-
import path13 from "node:path";
|
|
12507
|
-
import { fileURLToPath } from "node:url";
|
|
12508
|
-
var packageRoot = () => {
|
|
12509
|
-
let dir = path13.dirname(fileURLToPath(import.meta.url));
|
|
12510
|
-
while (true) {
|
|
12511
|
-
const parent = path13.dirname(dir);
|
|
12512
|
-
if (existsSync3(path13.join(dir, "package.json")) || parent === dir)
|
|
12513
|
-
return dir;
|
|
12514
|
-
dir = parent;
|
|
12515
|
-
}
|
|
12516
|
-
};
|
|
12517
|
-
var init_package_root = () => {};
|
|
12518
|
-
|
|
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;
|
|
12808
|
+
code: "file_lock_stale",
|
|
12809
|
+
lockPath,
|
|
12810
|
+
normalizedTargetPath
|
|
12811
|
+
});
|
|
12812
|
+
}
|
|
12813
|
+
waitForRetry();
|
|
12563
12814
|
}
|
|
12564
12815
|
}
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
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;
|
|
12816
|
+
}
|
|
12817
|
+
var SYNC_HELD_LOCKS_KEY, SYNC_CLEANUP_REGISTERED_KEY, SYNC_CLEANUP_HANDLER_KEY;
|
|
12818
|
+
var init_file_lock_sync = __esm(() => {
|
|
12819
|
+
init_errors3();
|
|
12820
|
+
init_sidecar_lock_reclaim();
|
|
12821
|
+
init_sidecar_lock_policy();
|
|
12822
|
+
init_lock_config();
|
|
12823
|
+
SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
12824
|
+
SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
12825
|
+
SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
12826
|
+
});
|
|
12827
|
+
|
|
12828
|
+
// node_modules/@openclaw/fs-safe/dist/file-lock.js
|
|
12829
|
+
var init_file_lock = __esm(() => {
|
|
12830
|
+
init_file_lock_sync();
|
|
12831
|
+
});
|
|
12832
|
+
|
|
12833
|
+
// packages/workit-core/src/core/package-root.ts
|
|
12834
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
12835
|
+
import path13 from "node:path";
|
|
12836
|
+
import { fileURLToPath } from "node:url";
|
|
12837
|
+
var packageRoot = () => {
|
|
12838
|
+
let dir = path13.dirname(fileURLToPath(import.meta.url));
|
|
12839
|
+
while (true) {
|
|
12840
|
+
const parent = path13.dirname(dir);
|
|
12841
|
+
if (existsSync3(path13.join(dir, "package.json")) || parent === dir)
|
|
12842
|
+
return dir;
|
|
12843
|
+
dir = parent;
|
|
12633
12844
|
}
|
|
12634
12845
|
};
|
|
12635
|
-
var
|
|
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
|
-
});
|
|
12846
|
+
var init_package_root = () => {};
|
|
12649
12847
|
|
|
12650
12848
|
// packages/workit-core/src/store/event-log.ts
|
|
12651
12849
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -19869,10 +20067,238 @@ var init_policy = __esm(() => {
|
|
|
19869
20067
|
init_shell_words();
|
|
19870
20068
|
});
|
|
19871
20069
|
|
|
20070
|
+
// packages/workit-core/src/core/live-session.ts
|
|
20071
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
20072
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
20073
|
+
import fs17 from "node:fs";
|
|
20074
|
+
import os6 from "node:os";
|
|
20075
|
+
import path22 from "node:path";
|
|
20076
|
+
var LIVE_SESSION_TTL_MS, HEARTBEAT_EVERY_MS = 30000, liveProbe, leadOf = (session) => session.split(":")[0].replace(/-w-.*$/su, "") || session, familyOf = (host) => host.replace(/_(?:cli|desktop)$/u, ""), processAlive = (pid) => {
|
|
20077
|
+
try {
|
|
20078
|
+
process.kill(pid, 0);
|
|
20079
|
+
return true;
|
|
20080
|
+
} catch (error) {
|
|
20081
|
+
return error.code === "EPERM";
|
|
20082
|
+
}
|
|
20083
|
+
}, processInfo = (pid) => {
|
|
20084
|
+
try {
|
|
20085
|
+
if (liveProbe.platform === "linux") {
|
|
20086
|
+
const stat = fs17.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
20087
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
20088
|
+
const command = fs17.readFileSync(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ");
|
|
20089
|
+
return Number.isInteger(ppid) ? { ppid, command: command.trim() } : null;
|
|
20090
|
+
}
|
|
20091
|
+
if (liveProbe.platform === "win32")
|
|
20092
|
+
return null;
|
|
20093
|
+
const match = /^\s*(\d+)\s+(.*)$/su.exec(liveProbe.run(["-o", "ppid=,command=", "-p", String(pid)]));
|
|
20094
|
+
return match ? { ppid: Number(match[1]), command: match[2].trim() } : null;
|
|
20095
|
+
} catch {
|
|
20096
|
+
return null;
|
|
20097
|
+
}
|
|
20098
|
+
}, WRAPPER, PIDS, hostPid = (transport) => {
|
|
20099
|
+
if (transport !== "hook-process")
|
|
20100
|
+
return process.pid;
|
|
20101
|
+
const key = `${liveProbe.platform}\x00${process.ppid}`;
|
|
20102
|
+
if (PIDS.has(key))
|
|
20103
|
+
return PIDS.get(key) ?? null;
|
|
20104
|
+
if (liveProbe.platform === "win32") {
|
|
20105
|
+
PIDS.set(key, null);
|
|
20106
|
+
return null;
|
|
20107
|
+
}
|
|
20108
|
+
let pid = process.ppid;
|
|
20109
|
+
for (let hop = 0;hop < 4; hop++) {
|
|
20110
|
+
const info = processInfo(pid);
|
|
20111
|
+
if (!info || !WRAPPER.test(info.command) || info.ppid <= 1)
|
|
20112
|
+
break;
|
|
20113
|
+
pid = info.ppid;
|
|
20114
|
+
}
|
|
20115
|
+
PIDS.set(key, pid);
|
|
20116
|
+
return pid;
|
|
20117
|
+
}, sessionSafe = (value) => value.replace(/[^A-Za-z0-9_.:@/+-]/gu, "-"), liveOwner = (input, transport, env = process.env) => {
|
|
20118
|
+
const session = input.session.id?.trim();
|
|
20119
|
+
if (!session)
|
|
20120
|
+
return null;
|
|
20121
|
+
const leads = new Set([leadOf(sessionSafe(session))]);
|
|
20122
|
+
if (input.session.parentId)
|
|
20123
|
+
leads.add(leadOf(sessionSafe(input.session.parentId)));
|
|
20124
|
+
if (env.WORKIT_PI_WORKER_SESSION && env.WORKIT_SESSION_ID)
|
|
20125
|
+
leads.add(leadOf(sessionSafe(env.WORKIT_SESSION_ID)));
|
|
20126
|
+
return {
|
|
20127
|
+
host: input.host,
|
|
20128
|
+
session: sessionSafe(session),
|
|
20129
|
+
leads: [...leads],
|
|
20130
|
+
pid: () => hostPid(transport)
|
|
20131
|
+
};
|
|
20132
|
+
}, checkoutOf = (target) => {
|
|
20133
|
+
let dir = path22.resolve(target);
|
|
20134
|
+
while (!fs17.existsSync(dir) && path22.dirname(dir) !== dir)
|
|
20135
|
+
dir = path22.dirname(dir);
|
|
20136
|
+
if (fs17.existsSync(dir) && !fs17.statSync(dir).isDirectory())
|
|
20137
|
+
dir = path22.dirname(dir);
|
|
20138
|
+
const parts = dir.split(path22.sep);
|
|
20139
|
+
const dotGit = parts.lastIndexOf(".git");
|
|
20140
|
+
if (dotGit > 0)
|
|
20141
|
+
dir = parts.slice(0, dotGit).join(path22.sep) || path22.sep;
|
|
20142
|
+
const location = resolveStore(dir);
|
|
20143
|
+
if (location instanceof Error || !location.shared || !location.top)
|
|
20144
|
+
return null;
|
|
20145
|
+
let top = location.top;
|
|
20146
|
+
try {
|
|
20147
|
+
top = fs17.realpathSync.native(top);
|
|
20148
|
+
} catch {}
|
|
20149
|
+
return top;
|
|
20150
|
+
}, markerFile2 = (top) => path22.join(resolveStateDir(), "live-sessions", `${createHash15("sha256").update(checkoutKey(top)).digest("hex").slice(0, 32)}.json`), checkoutKey = (top, platform = process.platform) => {
|
|
20151
|
+
let key = path22.resolve(top);
|
|
20152
|
+
try {
|
|
20153
|
+
key = fs17.realpathSync.native(key);
|
|
20154
|
+
} catch {}
|
|
20155
|
+
return platform === "win32" ? key.replaceAll("/", "\\").toLowerCase() : key;
|
|
20156
|
+
}, readMarker = (file) => {
|
|
20157
|
+
try {
|
|
20158
|
+
const value = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
20159
|
+
if (value?.version !== 1 || typeof value.session !== "string" || typeof value.host !== "string" || typeof value.heartbeatAt !== "string" || Number.isNaN(Date.parse(value.heartbeatAt)))
|
|
20160
|
+
return null;
|
|
20161
|
+
return {
|
|
20162
|
+
version: 1,
|
|
20163
|
+
session: value.session,
|
|
20164
|
+
leads: Array.isArray(value.leads) ? value.leads.filter((item) => typeof item === "string") : [],
|
|
20165
|
+
host: value.host,
|
|
20166
|
+
pid: typeof value.pid === "number" ? value.pid : null,
|
|
20167
|
+
pidStart: typeof value.pidStart === "string" ? value.pidStart : null,
|
|
20168
|
+
machine: typeof value.machine === "string" ? value.machine : "",
|
|
20169
|
+
checkout: typeof value.checkout === "string" ? value.checkout : "",
|
|
20170
|
+
heartbeatAt: value.heartbeatAt
|
|
20171
|
+
};
|
|
20172
|
+
} catch {
|
|
20173
|
+
return null;
|
|
20174
|
+
}
|
|
20175
|
+
}, ageOf2 = (marker, now) => now.getTime() - Date.parse(marker.heartbeatAt), isLive = (marker, deps) => {
|
|
20176
|
+
const age = ageOf2(marker, deps.now ?? new Date);
|
|
20177
|
+
if (age > (deps.ttlMs ?? LIVE_SESSION_TTL_MS))
|
|
20178
|
+
return false;
|
|
20179
|
+
if (marker.machine !== (deps.machine ?? os6.hostname()) || marker.pid === null)
|
|
20180
|
+
return true;
|
|
20181
|
+
if (!(deps.alive ?? processAlive)(marker.pid))
|
|
20182
|
+
return false;
|
|
20183
|
+
if (marker.pidStart === null)
|
|
20184
|
+
return true;
|
|
20185
|
+
const start = liveProbe.start(marker.pid);
|
|
20186
|
+
return start === null || start === marker.pidStart;
|
|
20187
|
+
}, sameLead = (marker, owner) => marker.session === owner.session || owner.leads.some((lead) => lead === marker.session || marker.leads.includes(lead)), sameOwner = (marker, owner, machine) => {
|
|
20188
|
+
if (sameLead(marker, owner))
|
|
20189
|
+
return true;
|
|
20190
|
+
if (marker.machine !== machine)
|
|
20191
|
+
return false;
|
|
20192
|
+
const pid = owner.pid();
|
|
20193
|
+
if (pid === null || marker.pid === null || marker.pidStart === null)
|
|
20194
|
+
return familyOf(marker.host) === familyOf(owner.host);
|
|
20195
|
+
return marker.pid === pid;
|
|
20196
|
+
}, liveConflict = (target, owner, deps = {}) => {
|
|
20197
|
+
try {
|
|
20198
|
+
const checkout = checkoutOf(target);
|
|
20199
|
+
if (!checkout)
|
|
20200
|
+
return null;
|
|
20201
|
+
const file = markerFile2(checkout);
|
|
20202
|
+
const marker = readMarker(file);
|
|
20203
|
+
if (!marker || sameLead(marker, owner) || !isLive(marker, deps))
|
|
20204
|
+
return null;
|
|
20205
|
+
if (sameOwner(marker, owner, deps.machine ?? os6.hostname()))
|
|
20206
|
+
return null;
|
|
20207
|
+
return { marker, file, ageMs: ageOf2(marker, deps.now ?? new Date), checkout };
|
|
20208
|
+
} catch {
|
|
20209
|
+
return null;
|
|
20210
|
+
}
|
|
20211
|
+
}, claimLiveSession = (target, owner, deps = {}) => {
|
|
20212
|
+
try {
|
|
20213
|
+
const checkout = checkoutOf(target);
|
|
20214
|
+
if (!checkout)
|
|
20215
|
+
return;
|
|
20216
|
+
const file = markerFile2(checkout);
|
|
20217
|
+
const now = deps.now ?? new Date;
|
|
20218
|
+
const machine = deps.machine ?? os6.hostname();
|
|
20219
|
+
const current = readMarker(file);
|
|
20220
|
+
if (current) {
|
|
20221
|
+
const age = ageOf2(current, now);
|
|
20222
|
+
if (sameLead(current, owner) && age >= 0 && age < HEARTBEAT_EVERY_MS)
|
|
20223
|
+
return;
|
|
20224
|
+
if (!sameLead(current, owner) && isLive(current, deps) && !sameOwner(current, owner, machine))
|
|
20225
|
+
return;
|
|
20226
|
+
}
|
|
20227
|
+
const pid = owner.pid();
|
|
20228
|
+
const marker = {
|
|
20229
|
+
version: 1,
|
|
20230
|
+
session: owner.session,
|
|
20231
|
+
leads: owner.leads,
|
|
20232
|
+
host: owner.host,
|
|
20233
|
+
pid,
|
|
20234
|
+
pidStart: pid === null ? null : liveProbe.start(pid),
|
|
20235
|
+
machine,
|
|
20236
|
+
checkout,
|
|
20237
|
+
heartbeatAt: now.toISOString()
|
|
20238
|
+
};
|
|
20239
|
+
fs17.mkdirSync(path22.dirname(file), { recursive: true });
|
|
20240
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
20241
|
+
fs17.writeFileSync(temp, JSON.stringify(marker));
|
|
20242
|
+
fs17.renameSync(temp, file);
|
|
20243
|
+
} catch {}
|
|
20244
|
+
};
|
|
20245
|
+
var init_live_session = __esm(() => {
|
|
20246
|
+
init_paths();
|
|
20247
|
+
init_logger();
|
|
20248
|
+
init_store_lock();
|
|
20249
|
+
LIVE_SESSION_TTL_MS = 15 * 60000;
|
|
20250
|
+
liveProbe = {
|
|
20251
|
+
platform: process.platform,
|
|
20252
|
+
run: (args) => spawnSync13("ps", args, { encoding: "utf8", timeout: 2000 }).stdout ?? "",
|
|
20253
|
+
start: (pid) => processStartOf(pid)
|
|
20254
|
+
};
|
|
20255
|
+
WRAPPER = /^(?:\S*\/)?(?:sh|bash|dash|zsh|ash|ksh|fish|env|timeout|npx|npm|bunx|cmd\.exe|pwsh|powershell)(?:\s|$)|launch\.mjs|npx-cli\.js/u;
|
|
20256
|
+
PIDS = new Map;
|
|
20257
|
+
});
|
|
20258
|
+
|
|
19872
20259
|
// packages/workit-core/src/hooks/write-gate.ts
|
|
19873
20260
|
import { realpathSync as realpathSync5 } from "node:fs";
|
|
19874
|
-
import
|
|
19875
|
-
|
|
20261
|
+
import os7 from "node:os";
|
|
20262
|
+
import path23 from "node:path";
|
|
20263
|
+
function writeGate(cwd, paths, owner, deps) {
|
|
20264
|
+
if (!owner)
|
|
20265
|
+
return beforeWriteGate(cwd, paths);
|
|
20266
|
+
const targets = liveTargets(cwd, paths);
|
|
20267
|
+
const live = liveGate(owner, targets, deps);
|
|
20268
|
+
if (live.kind !== "none")
|
|
20269
|
+
return live;
|
|
20270
|
+
const decision = beforeWriteGate(cwd, paths);
|
|
20271
|
+
if (decision.kind === "none")
|
|
20272
|
+
claimAll(owner, targets, deps);
|
|
20273
|
+
return decision;
|
|
20274
|
+
}
|
|
20275
|
+
function shellGate(cwd, command, owner, dialect = "posix", deps) {
|
|
20276
|
+
const writes = shellWrites(command);
|
|
20277
|
+
let targets = [];
|
|
20278
|
+
if (owner) {
|
|
20279
|
+
try {
|
|
20280
|
+
targets = [
|
|
20281
|
+
...segmentTargets(cwd, command, dialect),
|
|
20282
|
+
...gitMutationDirs(cwd, command, dialect).map((dir) => path23.join(dir, DIR_TARGET))
|
|
20283
|
+
];
|
|
20284
|
+
} catch {
|
|
20285
|
+
targets = [];
|
|
20286
|
+
}
|
|
20287
|
+
}
|
|
20288
|
+
if (owner && targets.length) {
|
|
20289
|
+
const live = liveGate(owner, targets, deps);
|
|
20290
|
+
if (live.kind !== "none") {
|
|
20291
|
+
if (!overridden(command, dialect))
|
|
20292
|
+
return live;
|
|
20293
|
+
noteOverride(cwd, owner, command);
|
|
20294
|
+
}
|
|
20295
|
+
}
|
|
20296
|
+
const decision = writes.writes ? beforeWriteGate(cwd, writes.targets) : NONE3;
|
|
20297
|
+
if (owner && targets.length && decision.kind === "none")
|
|
20298
|
+
claimAll(owner, targets, deps);
|
|
20299
|
+
return decision;
|
|
20300
|
+
}
|
|
20301
|
+
function beforeWriteGate(cwd, paths) {
|
|
19876
20302
|
try {
|
|
19877
20303
|
const gate = gateFor(cwd);
|
|
19878
20304
|
if (!gate || gate.blockers.length === 0)
|
|
@@ -19891,21 +20317,21 @@ function writeGate(cwd, paths) {
|
|
|
19891
20317
|
}
|
|
19892
20318
|
var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
|
|
19893
20319
|
let rest = "";
|
|
19894
|
-
for (let dir =
|
|
20320
|
+
for (let dir = path23.resolve(file);; dir = path23.dirname(dir)) {
|
|
19895
20321
|
try {
|
|
19896
|
-
return
|
|
20322
|
+
return path23.join(realpathSync5(dir), rest);
|
|
19897
20323
|
} catch {
|
|
19898
|
-
if (
|
|
19899
|
-
return
|
|
19900
|
-
rest = rest ?
|
|
20324
|
+
if (path23.dirname(dir) === dir)
|
|
20325
|
+
return path23.resolve(file);
|
|
20326
|
+
rest = rest ? path23.join(path23.basename(dir), rest) : path23.basename(dir);
|
|
19901
20327
|
}
|
|
19902
20328
|
}
|
|
19903
20329
|
}, exempt = (cwd, root, file, plans) => {
|
|
19904
20330
|
const fold = (value) => CASE_INSENSITIVE ? value.toLowerCase() : value;
|
|
19905
|
-
const relative =
|
|
19906
|
-
if (relative.startsWith("..") ||
|
|
20331
|
+
const relative = path23.relative(fold(canonical2(root)), fold(canonical2(path23.resolve(cwd, file))));
|
|
20332
|
+
if (relative.startsWith("..") || path23.isAbsolute(relative))
|
|
19907
20333
|
return true;
|
|
19908
|
-
const posix = relative.split(
|
|
20334
|
+
const posix = relative.split(path23.sep).join("/");
|
|
19909
20335
|
const segments = posix.split("/");
|
|
19910
20336
|
return /\.mdx?$/i.test(posix) || segments[0] === "docs" || segments.includes("plans") || [...plans].some((plan) => fold(plan) === posix);
|
|
19911
20337
|
}, PREFIXES2, operands = (words) => words.filter((item) => !item.startsWith("-")), editsOf = (input) => {
|
|
@@ -19997,7 +20423,7 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
|
|
|
19997
20423
|
if (!unknown && targets.length === 0)
|
|
19998
20424
|
return { writes: false, targets: null };
|
|
19999
20425
|
return { writes: true, targets: unknown ? null : targets };
|
|
20000
|
-
}, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(
|
|
20426
|
+
}, cache2, planKeyOf = (root, plans) => [...plans].map((file) => fileSignature(path23.resolve(root, file)) ?? "-").join("\x00"), gateFor = (cwd) => {
|
|
20001
20427
|
const store = new TaskStore(cwd);
|
|
20002
20428
|
const listed = store.listTaskIndex();
|
|
20003
20429
|
if (!listed.ok)
|
|
@@ -20033,21 +20459,182 @@ var NONE3, CASE_INSENSITIVE, canonical2 = (file) => {
|
|
|
20033
20459
|
if (cache2.size > 32)
|
|
20034
20460
|
cache2.delete(cache2.keys().next().value);
|
|
20035
20461
|
return gate;
|
|
20462
|
+
}, MUTATING_GIT, DRY_N, MUTATING_WORKIT_GIT, DIR_TARGET = ".workit-live-target", OVERRIDE = "WORKIT_LIVE_SESSION=override", inert = (args, sub) => args.some((arg) => arg === "--help" || arg === "-h" || arg === "--dry-run" || DRY_N.has(sub) && /^-[a-zA-Z]*n[a-zA-Z]*$/.test(arg) && !arg.startsWith("--")), gitMutationDirs = (cwd, command, dialect) => {
|
|
20463
|
+
let invocations;
|
|
20464
|
+
try {
|
|
20465
|
+
invocations = rawInvocations(command, dialect);
|
|
20466
|
+
} catch {
|
|
20467
|
+
return [];
|
|
20468
|
+
}
|
|
20469
|
+
return invocations.flatMap((invocation) => {
|
|
20470
|
+
if (invocation.tool !== "git" || invocation.dir === undefined)
|
|
20471
|
+
return [];
|
|
20472
|
+
const at = invocation.args.findIndex((arg) => !arg.startsWith("-"));
|
|
20473
|
+
const sub = invocation.args[at] ?? "";
|
|
20474
|
+
const rest = invocation.args.slice(at + 1);
|
|
20475
|
+
if (!MUTATING_GIT.has(sub) || inert(rest, sub))
|
|
20476
|
+
return [];
|
|
20477
|
+
if (sub === "stash" && ["list", "show"].includes(rest[0] ?? ""))
|
|
20478
|
+
return [];
|
|
20479
|
+
return [path23.resolve(cwd, invocation.dir ?? ".")];
|
|
20480
|
+
});
|
|
20481
|
+
}, CD2, PUSHD2, POPD2, cdInto = (dir, words) => {
|
|
20482
|
+
const target = words.slice(1).find((word) => !word.startsWith("-") || word === "-");
|
|
20483
|
+
if (dir === undefined || target === "-" || target && /^~|[$`*?]/.test(target))
|
|
20484
|
+
return;
|
|
20485
|
+
return target === undefined ? os7.homedir() : path23.resolve(dir, target);
|
|
20486
|
+
}, quoteWord = (word) => /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`, segmentTargets = (cwd, command, dialect) => {
|
|
20487
|
+
const targets = [];
|
|
20488
|
+
let dir = cwd;
|
|
20489
|
+
let stack = [];
|
|
20490
|
+
const groups = [];
|
|
20491
|
+
const at = (file) => path23.isAbsolute(file) ? file : dir === undefined ? null : path23.resolve(dir, file);
|
|
20492
|
+
for (const segment of segmentsOf(command, dialect)) {
|
|
20493
|
+
if (segment.group === "open") {
|
|
20494
|
+
groups.push({ dir, stack: [...stack] });
|
|
20495
|
+
continue;
|
|
20496
|
+
}
|
|
20497
|
+
if (segment.group === "close") {
|
|
20498
|
+
const saved = groups.pop();
|
|
20499
|
+
if (saved)
|
|
20500
|
+
({ dir, stack } = saved);
|
|
20501
|
+
continue;
|
|
20502
|
+
}
|
|
20503
|
+
const lead = segment.words.findIndex((word) => !/^\w+=/.test(word));
|
|
20504
|
+
const words = lead < 0 ? [] : segment.words.slice(lead);
|
|
20505
|
+
const name = (words[0]?.split(/[\\/]/).at(-1) ?? "").toLowerCase();
|
|
20506
|
+
if (CD2.has(name)) {
|
|
20507
|
+
dir = cdInto(dir, words);
|
|
20508
|
+
continue;
|
|
20509
|
+
}
|
|
20510
|
+
if (PUSHD2.has(name)) {
|
|
20511
|
+
stack.push(dir);
|
|
20512
|
+
dir = cdInto(dir, words);
|
|
20513
|
+
continue;
|
|
20514
|
+
}
|
|
20515
|
+
if (POPD2.has(name)) {
|
|
20516
|
+
dir = stack.length ? stack.pop() : undefined;
|
|
20517
|
+
continue;
|
|
20518
|
+
}
|
|
20519
|
+
for (const file of segment.redirects)
|
|
20520
|
+
if (!/^\/dev\/(?:null|stdout|stderr|tty)$/.test(file)) {
|
|
20521
|
+
const resolved = at(file);
|
|
20522
|
+
if (resolved)
|
|
20523
|
+
targets.push(resolved);
|
|
20524
|
+
}
|
|
20525
|
+
const edits = editsOf(segment.words);
|
|
20526
|
+
if (edits === null) {
|
|
20527
|
+
if (dir !== undefined)
|
|
20528
|
+
targets.push(path23.join(dir, DIR_TARGET));
|
|
20529
|
+
} else
|
|
20530
|
+
for (const file of edits) {
|
|
20531
|
+
const resolved = at(file);
|
|
20532
|
+
if (resolved)
|
|
20533
|
+
targets.push(resolved);
|
|
20534
|
+
}
|
|
20535
|
+
if (dir !== undefined && workitInvocations(segment.words.map(quoteWord).join(" "), "posix").some((args) => args[0] === "git" && MUTATING_WORKIT_GIT.has(args[1] ?? "") && !inert(args.slice(2), "")))
|
|
20536
|
+
targets.push(path23.join(dir, DIR_TARGET));
|
|
20537
|
+
}
|
|
20538
|
+
return targets;
|
|
20539
|
+
}, overridden = (command, dialect) => {
|
|
20540
|
+
const segments = segmentsOf(command, dialect);
|
|
20541
|
+
if (segments.length !== 1 || segments[0].group || segments[0].subs?.length)
|
|
20542
|
+
return false;
|
|
20543
|
+
const words = segments[0].words;
|
|
20544
|
+
const lead = words.findIndex((word) => !/^\w+=/.test(word));
|
|
20545
|
+
return lead > 0 && words.slice(0, lead).includes(OVERRIDE);
|
|
20546
|
+
}, HOST_LABELS, ageText = (ms) => {
|
|
20547
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
20548
|
+
return seconds < 120 ? `${seconds}s` : `${Math.round(seconds / 60)} min`;
|
|
20549
|
+
}, liveDeny = ({ marker, checkout, ageMs, file }) => {
|
|
20550
|
+
const unblock = `Work in your own worktree (\`git worktree add <path> -b <branch>\`, or \`workit fanout worktree create <slice>\`). Only if the user confirms that session is finished: release it with \`rm '${file}'\`, or prefix one command with ${OVERRIDE}.`;
|
|
20551
|
+
return {
|
|
20552
|
+
kind: "deny",
|
|
20553
|
+
reason: `workit live-session guard: ${checkout} belongs to another live agent session, ${HOST_LABELS[marker.host] ?? marker.host} session ${marker.session}${marker.pid === null ? "" : ` (pid ${marker.pid})`}, last active ${ageText(ageMs)} ago. Do not edit a checkout another session is working in; read-only commands stay allowed. ${unblock}`,
|
|
20554
|
+
unblock
|
|
20555
|
+
};
|
|
20556
|
+
}, guarded = (targets) => {
|
|
20557
|
+
const seen = new Set;
|
|
20558
|
+
return targets.filter((target) => {
|
|
20559
|
+
const dir = path23.dirname(target);
|
|
20560
|
+
if (seen.has(dir))
|
|
20561
|
+
return false;
|
|
20562
|
+
seen.add(dir);
|
|
20563
|
+
return true;
|
|
20564
|
+
});
|
|
20565
|
+
}, liveGate = (owner, targets, deps) => {
|
|
20566
|
+
try {
|
|
20567
|
+
for (const target of guarded(targets)) {
|
|
20568
|
+
const conflict = liveConflict(target, owner, deps);
|
|
20569
|
+
if (conflict && workspaceLiveSessionGuard(conflict.checkout))
|
|
20570
|
+
return liveDeny(conflict);
|
|
20571
|
+
}
|
|
20572
|
+
} catch {}
|
|
20573
|
+
return NONE3;
|
|
20574
|
+
}, claimAll = (owner, targets, deps) => {
|
|
20575
|
+
for (const target of guarded(targets))
|
|
20576
|
+
claimLiveSession(target, owner, deps);
|
|
20577
|
+
}, liveTargets = (cwd, paths) => paths?.length ? paths.map((file) => path23.resolve(cwd, file)) : [path23.join(cwd, DIR_TARGET)], noteOverride = (cwd, owner, command) => {
|
|
20578
|
+
try {
|
|
20579
|
+
if (!inWorkitWorkspace(cwd))
|
|
20580
|
+
return;
|
|
20581
|
+
appendHookObserved(cwd, {
|
|
20582
|
+
type: "live_session.override",
|
|
20583
|
+
actor: { host: owner.host, session: owner.session, agentId: null },
|
|
20584
|
+
session: owner.session,
|
|
20585
|
+
command: command.slice(0, 200)
|
|
20586
|
+
});
|
|
20587
|
+
} catch {}
|
|
20036
20588
|
};
|
|
20037
20589
|
var init_write_gate = __esm(() => {
|
|
20038
20590
|
init_task_store();
|
|
20039
20591
|
init_derive();
|
|
20040
20592
|
init_task_evaluation();
|
|
20593
|
+
init_live_session();
|
|
20594
|
+
init_workspaces();
|
|
20041
20595
|
init_ledger();
|
|
20596
|
+
init_raw_git();
|
|
20042
20597
|
init_shell_words();
|
|
20043
20598
|
NONE3 = { kind: "none" };
|
|
20044
20599
|
CASE_INSENSITIVE = process.platform === "darwin" || process.platform === "win32";
|
|
20045
20600
|
PREFIXES2 = new Set(["sudo", "command", "env", "nohup", "time", "exec", "xargs"]);
|
|
20046
20601
|
cache2 = new Map;
|
|
20602
|
+
MUTATING_GIT = new Set([
|
|
20603
|
+
"add",
|
|
20604
|
+
"am",
|
|
20605
|
+
"apply",
|
|
20606
|
+
"checkout",
|
|
20607
|
+
"cherry-pick",
|
|
20608
|
+
"clean",
|
|
20609
|
+
"commit",
|
|
20610
|
+
"merge",
|
|
20611
|
+
"mv",
|
|
20612
|
+
"pull",
|
|
20613
|
+
"rebase",
|
|
20614
|
+
"reset",
|
|
20615
|
+
"restore",
|
|
20616
|
+
"revert",
|
|
20617
|
+
"rm",
|
|
20618
|
+
"stash",
|
|
20619
|
+
"switch"
|
|
20620
|
+
]);
|
|
20621
|
+
DRY_N = new Set(["add", "clean", "rm", "mv"]);
|
|
20622
|
+
MUTATING_WORKIT_GIT = new Set(["commit", "branch"]);
|
|
20623
|
+
CD2 = new Set(["cd", "chdir", "set-location", "sl"]);
|
|
20624
|
+
PUSHD2 = new Set(["pushd", "push-location"]);
|
|
20625
|
+
POPD2 = new Set(["popd", "pop-location"]);
|
|
20626
|
+
HOST_LABELS = {
|
|
20627
|
+
claude_code: "Claude Code",
|
|
20628
|
+
opencode: "OpenCode",
|
|
20629
|
+
codex_cli: "Codex CLI",
|
|
20630
|
+
codex_desktop: "Codex Desktop",
|
|
20631
|
+
cursor: "Cursor",
|
|
20632
|
+
pi: "Pi"
|
|
20633
|
+
};
|
|
20047
20634
|
});
|
|
20048
20635
|
|
|
20049
20636
|
// packages/workit-core/src/hooks/stop.ts
|
|
20050
|
-
import { spawnSync as
|
|
20637
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
20051
20638
|
function stopObligation(input, descriptor) {
|
|
20052
20639
|
try {
|
|
20053
20640
|
const session = input.session.id;
|
|
@@ -20056,10 +20643,10 @@ function stopObligation(input, descriptor) {
|
|
|
20056
20643
|
const branch = currentBranch(input.cwd);
|
|
20057
20644
|
if (!branch || guardedBranch(input.cwd, branch))
|
|
20058
20645
|
return null;
|
|
20059
|
-
const ledger = readLedger(input.cwd);
|
|
20646
|
+
const ledger = readLedger(input.cwd, { integrity: true });
|
|
20060
20647
|
if (!ledger.ok)
|
|
20061
20648
|
return null;
|
|
20062
|
-
const all = ledger.value.rows;
|
|
20649
|
+
const all = ledger.value.rows.filter((row) => row.integrity !== "unverified");
|
|
20063
20650
|
const rows = all.filter((row) => row.branch === branch);
|
|
20064
20651
|
const mine = rows.filter((row) => sessionOf(row) === session);
|
|
20065
20652
|
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"));
|
|
@@ -20111,7 +20698,7 @@ var NONE4, QUESTION, OFFER, YES_NO, LIST_ITEM, asksUser = (message) => {
|
|
|
20111
20698
|
`);
|
|
20112
20699
|
return QUESTION.test(tail) || OFFER.test(tail) || YES_NO.test(tail);
|
|
20113
20700
|
}, short = (sha) => (sha ?? "?").slice(0, 12), gitList = (cwd, args) => {
|
|
20114
|
-
const run =
|
|
20701
|
+
const run = spawnSync14("git", args, {
|
|
20115
20702
|
cwd,
|
|
20116
20703
|
encoding: "utf8",
|
|
20117
20704
|
timeout: 3000,
|
|
@@ -20423,9 +21010,9 @@ var init_skill_nudge = __esm(() => {
|
|
|
20423
21010
|
|
|
20424
21011
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
20425
21012
|
import { existsSync as existsSync7, realpathSync as realpathSync6, statSync as statSync2 } from "node:fs";
|
|
20426
|
-
import
|
|
21013
|
+
import path24 from "node:path";
|
|
20427
21014
|
var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value), nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "", optionalText = (value) => typeof value === "string" ? value : null, existingDirectory = (value) => {
|
|
20428
|
-
if (!nonEmpty2(value) || !
|
|
21015
|
+
if (!nonEmpty2(value) || !path24.isAbsolute(value) || !existsSync7(value))
|
|
20429
21016
|
return null;
|
|
20430
21017
|
try {
|
|
20431
21018
|
return statSync2(value).isDirectory() ? realpathSync6(value) : null;
|
|
@@ -20530,8 +21117,7 @@ function handleHook(input, deps) {
|
|
|
20530
21117
|
const raw = canDeny ? rawGitPre(input, event.command, { nudge: false }) : NONE5;
|
|
20531
21118
|
if (raw.kind === "deny")
|
|
20532
21119
|
return raw;
|
|
20533
|
-
const
|
|
20534
|
-
const gate = writes.writes ? writeGate(input.cwd, writes.targets) : NONE5;
|
|
21120
|
+
const gate = usable2(descriptor.events["write.pre"].support) ? shellGate(input.cwd, event.command, liveOwner(input, descriptor.transport), event.dialect) : NONE5;
|
|
20535
21121
|
if (gate.kind === "deny")
|
|
20536
21122
|
return gate;
|
|
20537
21123
|
noteRawCommit(input, event.command, postTool ? callKey(event.toolUseId, event.command) : NEXT_COMMAND);
|
|
@@ -20544,7 +21130,7 @@ function handleHook(input, deps) {
|
|
|
20544
21130
|
rawGitPost(input, event.command, callKey(event.toolUseId, event.command));
|
|
20545
21131
|
return NONE5;
|
|
20546
21132
|
case "write.pre":
|
|
20547
|
-
return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths) : NONE5;
|
|
21133
|
+
return usable2(descriptor.events["write.pre"].support) ? writeGate(input.cwd, event.paths, liveOwner(input, descriptor.transport)) : NONE5;
|
|
20548
21134
|
case "subagent.start":
|
|
20549
21135
|
return {
|
|
20550
21136
|
kind: "context",
|
|
@@ -20587,9 +21173,9 @@ ${subagentStartText({ ...input, cwd: "" }, descriptor, event)}`;
|
|
|
20587
21173
|
if (role !== "judge" || !event.agentId)
|
|
20588
21174
|
return readOnly;
|
|
20589
21175
|
const own = subagentSession(session, event.agentId);
|
|
20590
|
-
|
|
20591
|
-
|
|
20592
|
-
return `${readOnly} Its own Workit session is ${own}: record verdicts with \`workit ledger verdict <result>
|
|
21176
|
+
const token = session ? recordIssuedSession(input.cwd || process.cwd(), session, own) : null;
|
|
21177
|
+
const flags = token ? `--session ${own} --token ${token}` : `--session ${own}`;
|
|
21178
|
+
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
21179
|
}, failureDecision = (descriptor, event, error) => {
|
|
20594
21180
|
if (event === "shell.pre" || event === "tool.pre" || event === "write.pre")
|
|
20595
21181
|
return descriptor.shellPolicy.failClosed ? { kind: "deny", reason: error, unblock: null } : NONE5;
|
|
@@ -20627,6 +21213,7 @@ var init_handle = __esm(() => {
|
|
|
20627
21213
|
init_raw_git();
|
|
20628
21214
|
init_skill_nudge();
|
|
20629
21215
|
init_stop();
|
|
21216
|
+
init_live_session();
|
|
20630
21217
|
init_write_gate();
|
|
20631
21218
|
NONE5 = { kind: "none" };
|
|
20632
21219
|
offered = new Set;
|
|
@@ -21165,8 +21752,8 @@ var init_codex = __esm(() => {
|
|
|
21165
21752
|
});
|
|
21166
21753
|
|
|
21167
21754
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
21168
|
-
import
|
|
21169
|
-
import
|
|
21755
|
+
import fs18 from "node:fs";
|
|
21756
|
+
import path25 from "node:path";
|
|
21170
21757
|
var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (value) => {
|
|
21171
21758
|
if (!isRecord4(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
|
|
21172
21759
|
return { ok: false, error: "hook_event_name is required" };
|
|
@@ -21247,17 +21834,17 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
|
|
|
21247
21834
|
};
|
|
21248
21835
|
}
|
|
21249
21836
|
}, TRANSCRIPT_TAIL_BYTES, lastAssistantText = (file) => {
|
|
21250
|
-
if (!file || !
|
|
21837
|
+
if (!file || !path25.isAbsolute(file))
|
|
21251
21838
|
return null;
|
|
21252
21839
|
try {
|
|
21253
|
-
const size =
|
|
21840
|
+
const size = fs18.statSync(file).size;
|
|
21254
21841
|
const start = Math.max(0, size - TRANSCRIPT_TAIL_BYTES);
|
|
21255
21842
|
const buffer = Buffer.alloc(size - start);
|
|
21256
|
-
const fd =
|
|
21843
|
+
const fd = fs18.openSync(file, "r");
|
|
21257
21844
|
try {
|
|
21258
|
-
|
|
21845
|
+
fs18.readSync(fd, buffer, 0, buffer.length, start);
|
|
21259
21846
|
} finally {
|
|
21260
|
-
|
|
21847
|
+
fs18.closeSync(fd);
|
|
21261
21848
|
}
|
|
21262
21849
|
const lines = buffer.toString("utf8").split(`
|
|
21263
21850
|
`);
|
|
@@ -21283,7 +21870,7 @@ var undocumented, none, CURSOR_DESCRIPTOR, EVENTS3, parseCursorHookInput = (valu
|
|
|
21283
21870
|
} catch {
|
|
21284
21871
|
return null;
|
|
21285
21872
|
}
|
|
21286
|
-
}, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
21873
|
+
}, within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path25.sep) ? root : `${root}${path25.sep}`), hookCwd = (input) => {
|
|
21287
21874
|
const cwd = existingDirectory(input.cwd);
|
|
21288
21875
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|
|
21289
21876
|
return cwd;
|
|
@@ -21509,6 +22096,7 @@ var init_hooks = __esm(async () => {
|
|
|
21509
22096
|
init_context();
|
|
21510
22097
|
init_policy();
|
|
21511
22098
|
init_write_gate();
|
|
22099
|
+
init_live_session();
|
|
21512
22100
|
init_stop();
|
|
21513
22101
|
init_raw_git();
|
|
21514
22102
|
init_skill_nudge();
|