@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
package/src/ship.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { envNumber, timerMs } from "./config.js";
|
|
5
|
+
import {
|
|
6
|
+
assertWorktree,
|
|
7
|
+
classifyGitFailure,
|
|
8
|
+
describeGitFailure,
|
|
9
|
+
git,
|
|
10
|
+
gitAtLeast,
|
|
11
|
+
gitError,
|
|
12
|
+
withRepoLock,
|
|
13
|
+
worktreeRoot
|
|
14
|
+
} from "./git.js";
|
|
15
|
+
import { createPullRequest } from "./github.js";
|
|
16
|
+
import { logWarn } from "./log.js";
|
|
17
|
+
const MAX_PATCH = 2e5;
|
|
18
|
+
async function branchDiff(repo, base, branch) {
|
|
19
|
+
const range = `${base}..${branch}`;
|
|
20
|
+
const safeDiff = ["-c", "core.fsmonitor=false", "diff", "--no-ext-diff", "--no-textconv"];
|
|
21
|
+
const numstat = await gitOrThrow([...safeDiff, "--numstat", "-M", "-z", range], repo);
|
|
22
|
+
const status = await gitOrThrow([...safeDiff, "--name-status", "-M", "-z", range], repo);
|
|
23
|
+
const patch = await gitOrThrow([...safeDiff, "--no-color", "-M", range], repo);
|
|
24
|
+
const statusTokens = status.split("\0").filter((t) => t !== "");
|
|
25
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
26
|
+
for (let i = 0; i < statusTokens.length; i++) {
|
|
27
|
+
const code = statusTokens[i][0];
|
|
28
|
+
if (code === "R" || code === "C") {
|
|
29
|
+
kinds.set(statusTokens[i + 2], { status: "renamed", oldPath: statusTokens[i + 1] });
|
|
30
|
+
i += 2;
|
|
31
|
+
} else {
|
|
32
|
+
kinds.set(statusTokens[i + 1], { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified" });
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const counts = /* @__PURE__ */ new Map();
|
|
37
|
+
const numTokens = numstat.split("\0");
|
|
38
|
+
for (let i = 0; i < numTokens.length; i++) {
|
|
39
|
+
const token = numTokens[i];
|
|
40
|
+
if (!token) continue;
|
|
41
|
+
const [add = "", del = "", path = ""] = token.split(" ");
|
|
42
|
+
let file = path;
|
|
43
|
+
if (!file) {
|
|
44
|
+
file = numTokens[i + 2] ?? "";
|
|
45
|
+
i += 2;
|
|
46
|
+
}
|
|
47
|
+
counts.set(file, { additions: Number(add) || 0, deletions: Number(del) || 0, binary: add === "-" });
|
|
48
|
+
}
|
|
49
|
+
const patches = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const chunk of patch.split(/^diff --git /m).slice(1)) {
|
|
51
|
+
const header = chunk.slice(0, chunk.indexOf("\n"));
|
|
52
|
+
const target = header.match(/ b\/(.+)$/)?.[1];
|
|
53
|
+
if (target) patches.set(target, chunk.slice(chunk.indexOf("\n") + 1));
|
|
54
|
+
}
|
|
55
|
+
return [...kinds.entries()].map(([path, kind]) => {
|
|
56
|
+
const full = patches.get(path) ?? "";
|
|
57
|
+
const count = counts.get(path) ?? { additions: 0, deletions: 0, binary: false };
|
|
58
|
+
return {
|
|
59
|
+
path,
|
|
60
|
+
oldPath: kind.oldPath,
|
|
61
|
+
status: kind.status,
|
|
62
|
+
...count,
|
|
63
|
+
patch: full.length > MAX_PATCH ? full.slice(0, MAX_PATCH) : full,
|
|
64
|
+
truncated: full.length > MAX_PATCH
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function checkedOutAt(repo, branch) {
|
|
69
|
+
const out = await gitOrThrow(["worktree", "list", "--porcelain"], repo);
|
|
70
|
+
let path;
|
|
71
|
+
for (const line of out.split("\n")) {
|
|
72
|
+
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
73
|
+
else if (line === `branch refs/heads/${branch}`) return path;
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
const CHECKOUT_TIMEOUT_MS = timerMs(envNumber("CODEFLOW_MERGE_TIMEOUT_SEC", 1800, { min: 1 }) * 1e3);
|
|
78
|
+
async function mergeInto(repo, branch, into, message, expected) {
|
|
79
|
+
if ((await git(["show-ref", "--verify", "--quiet", `refs/heads/${into}`], repo)).code !== 0) {
|
|
80
|
+
throw new Error(`Branch "${into}" doesn't exist`);
|
|
81
|
+
}
|
|
82
|
+
return withRepoLock(repo, async () => {
|
|
83
|
+
if (expected) {
|
|
84
|
+
const [source, target] = await Promise.all([gitOrThrow(["rev-parse", branch], repo), gitOrThrow(["rev-parse", `refs/heads/${into}`], repo)]);
|
|
85
|
+
if (source.trim() !== expected.source || target.trim() !== expected.target) throw new Error("The result or merge target moved after validation. Validate the current merge target again.");
|
|
86
|
+
}
|
|
87
|
+
const checkout = await checkedOutAt(repo, into);
|
|
88
|
+
if (!checkout) await refuseIfBusy(repo, into);
|
|
89
|
+
return checkout ? mergeInCheckout(repo, checkout, expected?.source ?? branch, into, message, expected) : mergeByRef(repo, expected?.source ?? branch, into, message, expected);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function refuseIfBusy(repo, into) {
|
|
93
|
+
const out = await gitOrThrow(["worktree", "list", "--porcelain"], repo);
|
|
94
|
+
const detached = [];
|
|
95
|
+
let path;
|
|
96
|
+
for (const line of out.split("\n")) {
|
|
97
|
+
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
98
|
+
else if (line === "detached" && path) detached.push(path);
|
|
99
|
+
}
|
|
100
|
+
const ref = `refs/heads/${into}`;
|
|
101
|
+
for (const wt of detached.filter((p) => existsSync(p))) {
|
|
102
|
+
const res = await git(["rev-parse", "--git-path", "rebase-merge/head-name", "--git-path", "rebase-apply/head-name", "--git-path", "BISECT_START"], wt);
|
|
103
|
+
if (res.code !== 0) continue;
|
|
104
|
+
const [merge = "", apply = "", bisect = ""] = res.stdout.split("\n").map((p) => resolve(wt, p.trim()));
|
|
105
|
+
const read = (file) => existsSync(file) ? readFileSync(file, "utf8").trim() : "";
|
|
106
|
+
if (read(merge) === ref || read(apply) === ref) {
|
|
107
|
+
throw new Error(`"${into}" is being rebased in ${wt}. Finish the rebase (\`git rebase --continue\`) or abort it (\`git rebase --abort\`) there, then merge again.`);
|
|
108
|
+
}
|
|
109
|
+
const started = read(bisect);
|
|
110
|
+
if (started === into || started === ref) {
|
|
111
|
+
throw new Error(`"${into}" is being bisected in ${wt}. End the bisect there (\`git bisect reset\`), then merge again.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function mergeInCheckout(repo, checkout, branch, into, message, expected) {
|
|
116
|
+
if (!existsSync(checkout)) throw new Error(`"${into}" is checked out at ${checkout}, which no longer exists`);
|
|
117
|
+
const at = await assertWorktree(repo, { path: checkout, branch: into });
|
|
118
|
+
const status = await git(["status", "--porcelain", "--untracked-files=no"], at);
|
|
119
|
+
if (status.code !== 0) throw new Error(`Couldn't read the state of ${checkout}: ${gitError(["status"], status)}`);
|
|
120
|
+
if (status.stdout.trim()) {
|
|
121
|
+
throw new Error(`"${into}" is checked out at ${checkout} and has uncommitted changes. Commit or stash them, then merge again.`);
|
|
122
|
+
}
|
|
123
|
+
if (await midMerge(at)) throw new Error(`${checkout} is in the middle of another merge. Finish it, or run \`git merge --abort\` there, then merge again.`);
|
|
124
|
+
const lockPath = (await git(["rev-parse", "--git-path", "index.lock"], at)).stdout.trim();
|
|
125
|
+
const lock = lockPath && resolve(checkout, lockPath);
|
|
126
|
+
if (lock && existsSync(lock)) throw new Error(`Couldn't merge into ${into}: ${describeGitFailure("locked")} (${lock})`);
|
|
127
|
+
const before = (await gitOrThrow(["rev-parse", "HEAD"], at)).trim();
|
|
128
|
+
const merging = (await gitOrThrow(["rev-parse", branch], at)).trim();
|
|
129
|
+
if (expected && (before !== expected.target || merging !== expected.source)) throw new Error("The checkout moved after merge validation; validate the current target again");
|
|
130
|
+
const options = { timeoutMs: CHECKOUT_TIMEOUT_MS, hooks: !isInside(worktreeRoot(repo), checkout) };
|
|
131
|
+
const finish = async (commit, warning) => {
|
|
132
|
+
if (expected?.tree && (await gitOrThrow(["rev-parse", `${commit}^{tree}`], at)).trim() !== expected.tree) {
|
|
133
|
+
const status2 = await git(["status", "--porcelain", "--untracked-files=no"], at);
|
|
134
|
+
const stillOurCommit = (await gitOrThrow(["rev-parse", "HEAD"], at)).trim() === commit;
|
|
135
|
+
const rollback = status2.code === 0 && !status2.stdout.trim() && stillOurCommit ? await git(["reset", "--merge", expected.target], at) : void 0;
|
|
136
|
+
throw new Error(rollback?.code === 0 ? "A commit hook changed the verified merge tree. The merge was undone; review the hook and validate again." : "A commit hook changed the verified merge tree and the checkout changed afterwards. It was retained; inspect the checkout before proceeding.");
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, commit, method: "checkout", ...warning ? { warning } : {} };
|
|
139
|
+
};
|
|
140
|
+
let res = await git(["merge", "--no-ff", ...expected?.tree ? ["--no-commit"] : [], "-m", message, branch], at, options);
|
|
141
|
+
if (res.code === 0 && expected?.tree) {
|
|
142
|
+
const tree = (await gitOrThrow(["write-tree"], at)).trim();
|
|
143
|
+
if (tree !== expected.tree) {
|
|
144
|
+
if (await midMerge(at)) {
|
|
145
|
+
const abort = await git(["merge", "--abort"], at, { timeoutMs: CHECKOUT_TIMEOUT_MS });
|
|
146
|
+
if (abort.code) throw new Error("The merge tree differed from the verified tree and abort failed. Inspect the checkout before proceeding.");
|
|
147
|
+
}
|
|
148
|
+
throw new Error("The merge produced a different tree than validation. No merge was committed; validate the current result again.");
|
|
149
|
+
}
|
|
150
|
+
if (await midMerge(at)) res = await git(["commit", "-m", message], at, options);
|
|
151
|
+
}
|
|
152
|
+
if (res.code === 0) return finish((await gitOrThrow(["rev-parse", "HEAD"], at)).trim());
|
|
153
|
+
if (res.timedOut) {
|
|
154
|
+
const head = (await git(["rev-parse", "HEAD"], at)).stdout.trim();
|
|
155
|
+
const parents = (await git(["log", "-1", "--format=%P", head], at)).stdout.trim();
|
|
156
|
+
if (head !== before && parents === `${before} ${merging}`) {
|
|
157
|
+
const after = CHECKOUT_TIMEOUT_MS >= 6e4 ? `${Math.round(CHECKOUT_TIMEOUT_MS / 6e4)} min` : `${Math.round(CHECKOUT_TIMEOUT_MS / 1e3)} s`;
|
|
158
|
+
const left2 = lock && existsSync(lock) ? `, and it left ${lock} behind; remove it once no git is running there` : "";
|
|
159
|
+
const warning = `Merged into ${into}, but git was stopped after ${after}, once the merge commit was made: a post-merge hook (an install, a download) may not have finished in ${checkout}${left2}.`;
|
|
160
|
+
logWarn(warning, { checkout, commit: head });
|
|
161
|
+
return finish(head, warning);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let kind = mergeFailure(res);
|
|
165
|
+
const conflicts = kind === "conflict" ? (await git(["diff", "--name-only", "--no-ext-diff", "--diff-filter=U"], at)).stdout.trim().split("\n").filter(Boolean) : [];
|
|
166
|
+
if (kind === "conflict" && !conflicts.length) kind = "other";
|
|
167
|
+
if (await midMerge(at)) {
|
|
168
|
+
const abort = await git(["merge", "--abort"], at, { timeoutMs: CHECKOUT_TIMEOUT_MS });
|
|
169
|
+
if (abort.code !== 0) {
|
|
170
|
+
throw new Error(`Merging into ${into} failed (${describeGitFailure(kind)}), and undoing it failed too: ${gitError(["merge", "--abort"], abort)}. Run \`git merge --abort\` in ${checkout}.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (kind === "conflict") return { ok: false, conflicts, message: `Merging into ${into} would conflict` };
|
|
174
|
+
const left = res.timedOut && lock && existsSync(lock) ? ` It left ${lock} behind; remove it once no git is running there.` : "";
|
|
175
|
+
throw new Error(`Couldn't merge into ${into} at ${checkout}: ${describeGitFailure(kind)}${details(kind, res)}${left}`);
|
|
176
|
+
}
|
|
177
|
+
async function mergeByRef(repo, branch, into, message, expected) {
|
|
178
|
+
if (!await gitAtLeast(2, 38)) {
|
|
179
|
+
throw new Error(`Merging into "${into}" without checking it out needs git 2.38 or later. Check out ${into} somewhere and merge there, or upgrade git.`);
|
|
180
|
+
}
|
|
181
|
+
const intoSha = (await gitOrThrow(["rev-parse", `refs/heads/${into}`], repo)).trim();
|
|
182
|
+
const branchSha = (await gitOrThrow(["rev-parse", branch], repo)).trim();
|
|
183
|
+
if (expected && (intoSha !== expected.target || branchSha !== expected.source)) throw new Error("The result or merge target moved after validation; validate again");
|
|
184
|
+
const tree = await git(["merge-tree", "--write-tree", "--name-only", intoSha, branchSha], repo);
|
|
185
|
+
if (tree.code === 1) {
|
|
186
|
+
const lines = tree.stdout.split("\n");
|
|
187
|
+
const conflicts = lines.slice(1, lines.indexOf("") === -1 ? void 0 : lines.indexOf("")).filter(Boolean);
|
|
188
|
+
return { ok: false, conflicts, message: `Merging into ${into} would conflict` };
|
|
189
|
+
}
|
|
190
|
+
if (tree.code !== 0) throw new Error(gitError(["merge-tree", "--write-tree", into, branch], tree));
|
|
191
|
+
const treeId = tree.stdout.split("\n")[0].trim();
|
|
192
|
+
if (expected?.tree && treeId !== expected.tree) throw new Error("The merge produced a different tree than validation. No branch was changed; validate again.");
|
|
193
|
+
const commit = await git(["commit-tree", treeId, "-p", intoSha, "-p", branchSha, "-m", message], repo);
|
|
194
|
+
if (commit.code !== 0) throw new Error(`Couldn't make the merge commit: ${describeGitFailure(classifyGitFailure(commit))}${details("other", commit)}`);
|
|
195
|
+
const update = await git(["update-ref", `refs/heads/${into}`, commit.stdout.trim(), intoSha], repo);
|
|
196
|
+
if (update.code !== 0) {
|
|
197
|
+
const kind = classifyGitFailure(update);
|
|
198
|
+
throw new Error(kind === "locked" ? `Couldn't move ${into}: ${describeGitFailure(kind)}` : `${into} moved while merging, or couldn't be updated; merge again. ${gitError(["update-ref"], update)}`);
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, commit: commit.stdout.trim(), method: "ref" };
|
|
201
|
+
}
|
|
202
|
+
const isInside = (dir, path) => {
|
|
203
|
+
const rel = relative(resolve(dir), resolve(path));
|
|
204
|
+
return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
205
|
+
};
|
|
206
|
+
async function midMerge(checkout) {
|
|
207
|
+
return (await git(["rev-parse", "-q", "--verify", "MERGE_HEAD"], checkout)).code === 0;
|
|
208
|
+
}
|
|
209
|
+
function mergeFailure(res) {
|
|
210
|
+
const text = `${res.stderr}
|
|
211
|
+
${res.stdout}`;
|
|
212
|
+
if (/Unable to write (?:new )?index/i.test(text)) return "locked";
|
|
213
|
+
const kind = classifyGitFailure(res);
|
|
214
|
+
return kind === "other" && /Not committing merge/i.test(text) ? "hook" : kind;
|
|
215
|
+
}
|
|
216
|
+
function details(kind, res) {
|
|
217
|
+
const text = `${res.stderr}
|
|
218
|
+
${res.stdout}`;
|
|
219
|
+
const files = text.split("\n").filter((l) => /^\t\S/.test(l)).map((l) => l.trim());
|
|
220
|
+
if ((kind === "untracked" || kind === "dirty") && files.length) return `: ${files.slice(0, 10).join(", ")}${files.length > 10 ? ` and ${files.length - 10} more` : ""}. Move or commit them, then merge again.`;
|
|
221
|
+
const last = text.split("\n").map((l) => l.trim()).filter(Boolean).pop();
|
|
222
|
+
return last ? ` (git: ${last.slice(0, 300)})` : "";
|
|
223
|
+
}
|
|
224
|
+
async function remoteBranches(repo) {
|
|
225
|
+
const remote = await git(["remote", "get-url", "origin"], repo);
|
|
226
|
+
if (remote.code !== 0) throw new Error("This repository has no 'origin' remote to open a pull request on");
|
|
227
|
+
const out = await gitOrThrow(["ls-remote", "--symref", "origin", "HEAD", "refs/heads/*"], repo);
|
|
228
|
+
const branches = [];
|
|
229
|
+
let defaultBranch;
|
|
230
|
+
for (const line of out.split("\n")) {
|
|
231
|
+
const [left = "", ref = ""] = line.split(" ");
|
|
232
|
+
if (left.startsWith("ref: refs/heads/") && ref === "HEAD") defaultBranch = left.slice("ref: refs/heads/".length);
|
|
233
|
+
else if (ref.startsWith("refs/heads/")) branches.push(ref.slice("refs/heads/".length));
|
|
234
|
+
}
|
|
235
|
+
return { branches, defaultBranch };
|
|
236
|
+
}
|
|
237
|
+
async function pullRequestTarget(repo, startedFrom) {
|
|
238
|
+
const { branches, defaultBranch } = await remoteBranches(repo);
|
|
239
|
+
if (startedFrom && branches.includes(startedFrom)) return startedFrom;
|
|
240
|
+
if (defaultBranch) return defaultBranch;
|
|
241
|
+
throw new Error("Pick a branch to open the pull request into");
|
|
242
|
+
}
|
|
243
|
+
async function openPullRequest(repo, branch, into, title, body, base, options = {}) {
|
|
244
|
+
const { branches, defaultBranch } = await remoteBranches(repo);
|
|
245
|
+
if (!branches.includes(into)) {
|
|
246
|
+
const instead = defaultBranch && defaultBranch !== into ? `, such as ${defaultBranch}` : "";
|
|
247
|
+
throw new Error(`"${into}" isn't a branch on GitHub (it may have been merged and deleted), so a pull request can't go into it. Pick another branch${instead}.`);
|
|
248
|
+
}
|
|
249
|
+
const caught = base && !options.expectedHead ? await catchUpWithSquashedBase(repo, branch, into, base) : {};
|
|
250
|
+
if (options.expectedHead && (await gitOrThrow(["rev-parse", `refs/heads/${branch}`], repo)).trim() !== options.expectedHead) throw new Error("The approved result moved before publication; review the current commit again");
|
|
251
|
+
const push = await git(options.expectedHead ? ["push", "origin", `${options.expectedHead}:refs/heads/${branch}`] : ["push", "-u", "origin", branch], repo, { hooks: true });
|
|
252
|
+
if (push.code !== 0) {
|
|
253
|
+
const kind = classifyGitFailure(push);
|
|
254
|
+
throw new Error(`Pushing ${branch} failed: ${kind === "other" ? push.stderr.trim().split("\n").slice(-2).join(" ") : describeGitFailure(kind)}`);
|
|
255
|
+
}
|
|
256
|
+
const url = await createPullRequest(repo, { base: into, head: branch, title, body });
|
|
257
|
+
return { url, caughtUp: caught.commit, note: caught.note };
|
|
258
|
+
}
|
|
259
|
+
async function catchUpWithSquashedBase(repo, branch, into, base) {
|
|
260
|
+
const target = `refs/remotes/origin/${into}`;
|
|
261
|
+
await gitOrThrow(["fetch", "--quiet", "origin", `+refs/heads/${into}:${target}`], repo);
|
|
262
|
+
if ((await git(["merge-base", "--is-ancestor", base, target], repo)).code === 0) return {};
|
|
263
|
+
const cantTell = (why) => {
|
|
264
|
+
const note = `Couldn't check whether the run's starting point was already squash-merged into ${into} (${why}); the pull request may show those changes again`;
|
|
265
|
+
logWarn(note, { repo, branch });
|
|
266
|
+
return { note };
|
|
267
|
+
};
|
|
268
|
+
if (!await gitAtLeast(2, 38)) return cantTell("that needs git 2.38 or later");
|
|
269
|
+
const merged = await git(["merge-tree", "--write-tree", target, base], repo);
|
|
270
|
+
if (merged.code === 1) return {};
|
|
271
|
+
if (merged.code !== 0) return cantTell(gitError(["merge-tree"], merged));
|
|
272
|
+
const targetTree = (await gitOrThrow(["rev-parse", `${target}^{tree}`], repo)).trim();
|
|
273
|
+
if (merged.stdout.split("\n")[0].trim() !== targetTree) return {};
|
|
274
|
+
const outcome = await mergeInto(repo, target, branch, `Merge origin/${into} into ${branch}`);
|
|
275
|
+
if (!outcome.ok) {
|
|
276
|
+
throw new Error(`origin/${into} conflicts with ${branch} in ${outcome.conflicts.join(", ")}. Merge it into the branch and resolve them, then open the pull request again.`);
|
|
277
|
+
}
|
|
278
|
+
return { commit: outcome.commit };
|
|
279
|
+
}
|
|
280
|
+
async function removeWorktrees(repo, worktrees, deleteBranches) {
|
|
281
|
+
return withRepoLock(repo, async () => {
|
|
282
|
+
const removed = [];
|
|
283
|
+
const deletedBranches = [];
|
|
284
|
+
const failed = [];
|
|
285
|
+
for (const wt of worktrees) {
|
|
286
|
+
if (existsSync(wt.path)) {
|
|
287
|
+
const unsafe = await assertWorktree(repo, { path: wt.path }).then(
|
|
288
|
+
() => void 0,
|
|
289
|
+
(err) => err.message
|
|
290
|
+
);
|
|
291
|
+
if (unsafe) {
|
|
292
|
+
failed.push(unsafe);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
let res = await git(["worktree", "remove", "--force", wt.path], repo);
|
|
296
|
+
if (res.code !== 0) res = await git(["worktree", "remove", "--force", "--force", wt.path], repo);
|
|
297
|
+
if (res.code === 0) removed.push(wt.path);
|
|
298
|
+
else failed.push(`${wt.path}: ${shortError(res)}`);
|
|
299
|
+
}
|
|
300
|
+
if (deleteBranches && (await git(["show-ref", "--verify", "--quiet", `refs/heads/${wt.branch}`], repo)).code === 0) {
|
|
301
|
+
const res = await git(["branch", "-D", wt.branch], repo);
|
|
302
|
+
if (res.code === 0) deletedBranches.push(wt.branch);
|
|
303
|
+
else failed.push(`branch ${wt.branch}: ${shortError(res)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
await git(["worktree", "prune"], repo);
|
|
307
|
+
if (failed.length) logWarn(`Couldn't remove everything in ${repo}`, { failed });
|
|
308
|
+
return { removed, deletedBranches, failed };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function shortError(res) {
|
|
312
|
+
const kind = classifyGitFailure(res);
|
|
313
|
+
const last = (res.stderr.trim() || res.stdout.trim()).split("\n").pop()?.trim() ?? "";
|
|
314
|
+
return kind === "other" ? last.replace(/^(fatal|error):\s*/, "") || "git failed" : describeGitFailure(kind);
|
|
315
|
+
}
|
|
316
|
+
async function gitOrThrow(args, cwd) {
|
|
317
|
+
const res = await git(args, cwd);
|
|
318
|
+
if (res.code !== 0) throw new Error(`git ${args[0]} failed: ${res.stderr.trim() || res.stdout.trim()}`);
|
|
319
|
+
return res.stdout;
|
|
320
|
+
}
|
|
321
|
+
async function folderSizes(paths, timeoutMs = 3e4) {
|
|
322
|
+
const existing = paths.filter((p) => existsSync(p));
|
|
323
|
+
const sizes = {};
|
|
324
|
+
for (let i = 0; i < existing.length; i += 50) {
|
|
325
|
+
const batch = existing.slice(i, i + 50);
|
|
326
|
+
const out = await new Promise((resolve2) => {
|
|
327
|
+
execFile("du", ["-sk", ...batch], { timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024 }, (_err, stdout) => resolve2(stdout?.toString() ?? ""));
|
|
328
|
+
});
|
|
329
|
+
for (const line of out.split("\n")) {
|
|
330
|
+
const tab = line.indexOf(" ");
|
|
331
|
+
if (tab > 0) sizes[line.slice(tab + 1)] = Number(line.slice(0, tab)) * 1024;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return sizes;
|
|
335
|
+
}
|
|
336
|
+
export {
|
|
337
|
+
branchDiff,
|
|
338
|
+
folderSizes,
|
|
339
|
+
mergeInto,
|
|
340
|
+
openPullRequest,
|
|
341
|
+
pullRequestTarget,
|
|
342
|
+
remoteBranches,
|
|
343
|
+
removeWorktrees
|
|
344
|
+
};
|
package/src/slots.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
class Slots {
|
|
2
|
+
constructor(size) {
|
|
3
|
+
this.size = size;
|
|
4
|
+
}
|
|
5
|
+
used = 0;
|
|
6
|
+
waiting = [];
|
|
7
|
+
activeGroups = /* @__PURE__ */ new Map();
|
|
8
|
+
lastGroup = "";
|
|
9
|
+
get busy() {
|
|
10
|
+
return this.used;
|
|
11
|
+
}
|
|
12
|
+
get queued() {
|
|
13
|
+
return this.waiting.length;
|
|
14
|
+
}
|
|
15
|
+
/** Resolves with a release function once a slot is free; rejects if `signal` aborts first. */
|
|
16
|
+
acquire(signal, opts = {}) {
|
|
17
|
+
const group = opts.group ?? "";
|
|
18
|
+
const priority = () => {
|
|
19
|
+
const value = typeof opts.priority === "function" ? opts.priority() : opts.priority ?? 0;
|
|
20
|
+
return Number.isFinite(value) ? value : 0;
|
|
21
|
+
};
|
|
22
|
+
if (signal?.aborted) return Promise.reject(new Error("aborted"));
|
|
23
|
+
if (this.used < this.size) {
|
|
24
|
+
this.used++;
|
|
25
|
+
this.activeGroups.set(group, (this.activeGroups.get(group) ?? 0) + 1);
|
|
26
|
+
this.lastGroup = group;
|
|
27
|
+
return Promise.resolve(this.releaser(group));
|
|
28
|
+
}
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const entry = {
|
|
31
|
+
signal,
|
|
32
|
+
priority,
|
|
33
|
+
group,
|
|
34
|
+
grant: () => {
|
|
35
|
+
signal?.removeEventListener("abort", entry.onAbort);
|
|
36
|
+
this.used++;
|
|
37
|
+
this.activeGroups.set(group, (this.activeGroups.get(group) ?? 0) + 1);
|
|
38
|
+
this.lastGroup = group;
|
|
39
|
+
resolve(this.releaser(group));
|
|
40
|
+
},
|
|
41
|
+
onAbort: () => {
|
|
42
|
+
const i = this.waiting.indexOf(entry);
|
|
43
|
+
if (i >= 0) this.waiting.splice(i, 1);
|
|
44
|
+
reject(new Error("aborted"));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
signal?.addEventListener("abort", entry.onAbort, { once: true });
|
|
48
|
+
this.waiting.push(entry);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
releaser(group) {
|
|
52
|
+
let released = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (released) return;
|
|
55
|
+
released = true;
|
|
56
|
+
this.used--;
|
|
57
|
+
const remaining = (this.activeGroups.get(group) ?? 1) - 1;
|
|
58
|
+
if (remaining) this.activeGroups.set(group, remaining);
|
|
59
|
+
else this.activeGroups.delete(group);
|
|
60
|
+
let best = -1;
|
|
61
|
+
for (let i = 0; i < this.waiting.length; i++) {
|
|
62
|
+
const candidate = this.waiting[i];
|
|
63
|
+
const current = this.waiting[best];
|
|
64
|
+
const load = this.activeGroups.get(candidate.group) ?? 0;
|
|
65
|
+
const currentLoad = current ? this.activeGroups.get(current.group) ?? 0 : 0;
|
|
66
|
+
if (!current || candidate.priority() > current.priority() || candidate.priority() === current.priority() && (load < currentLoad || load === currentLoad && current.group === this.lastGroup && candidate.group !== this.lastGroup)) best = i;
|
|
67
|
+
}
|
|
68
|
+
if (best >= 0) this.waiting.splice(best, 1)[0].grant();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
Slots
|
|
74
|
+
};
|