@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.
Files changed (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/browser-7XI52A6V.js +8 -0
  17. package/public/assets/changes-EYFPASEI.js +1 -0
  18. package/public/assets/chunk-7H3ZLTKA.js +2 -0
  19. package/public/assets/chunk-L7TUCIYF.js +1 -0
  20. package/public/assets/chunk-LLMQG23J.js +163 -0
  21. package/public/assets/main-KIRM2OEK.js +1 -0
  22. package/public/assets/settings-QJZVW22Y.js +9 -0
  23. package/public/assets/styles-615EBB20.css +32 -0
  24. package/public/assets/terminal-A2FI2NAX.js +62 -0
  25. package/public/assets.json +1 -0
  26. package/public/bundle-meta.json +1 -0
  27. package/public/docs/.nojekyll +0 -0
  28. package/public/docs/accessibility.html +19 -0
  29. package/public/docs/acp-agents.html +117 -0
  30. package/public/docs/agent-containers.html +79 -0
  31. package/public/docs/api.html +1130 -0
  32. package/public/docs/app.js +342 -0
  33. package/public/docs/application-improvements.html +275 -0
  34. package/public/docs/architecture-explorer.html +19 -0
  35. package/public/docs/architecture.html +149 -0
  36. package/public/docs/auto-team.html +142 -0
  37. package/public/docs/automations.html +109 -0
  38. package/public/docs/checkpoint-review.html +37 -0
  39. package/public/docs/code-hosts.html +35 -0
  40. package/public/docs/code-intelligence.html +35 -0
  41. package/public/docs/configuration.html +346 -0
  42. package/public/docs/context-inspection.html +23 -0
  43. package/public/docs/coordinated-changes.html +28 -0
  44. package/public/docs/deferred-runs.html +25 -0
  45. package/public/docs/dependency-discovery.html +19 -0
  46. package/public/docs/documentation.html +84 -0
  47. package/public/docs/dynamic-teams-evaluation.html +174 -0
  48. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  49. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  50. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  51. package/public/docs/extensions.html +42 -0
  52. package/public/docs/external-sources.html +96 -0
  53. package/public/docs/filesystem-policy.html +26 -0
  54. package/public/docs/filtered-tasks.html +44 -0
  55. package/public/docs/font-license.txt +92 -0
  56. package/public/docs/getting-started.html +118 -0
  57. package/public/docs/github-review-threads.html +26 -0
  58. package/public/docs/github.html +157 -0
  59. package/public/docs/help.html +69 -0
  60. package/public/docs/index.html +77 -0
  61. package/public/docs/installation.html +74 -0
  62. package/public/docs/launch-profiles.html +48 -0
  63. package/public/docs/maintenance.html +60 -0
  64. package/public/docs/memory-claims.html +31 -0
  65. package/public/docs/memory-storage.html +58 -0
  66. package/public/docs/notifications.html +39 -0
  67. package/public/docs/patterns.html +195 -0
  68. package/public/docs/performance-results.html +210 -0
  69. package/public/docs/performance.html +166 -0
  70. package/public/docs/project-conventions.html +22 -0
  71. package/public/docs/project-first-navigation-plan.html +587 -0
  72. package/public/docs/project-teams-implementation-plan.html +772 -0
  73. package/public/docs/project-teams.html +99 -0
  74. package/public/docs/provider-compatibility.html +20 -0
  75. package/public/docs/releasing.html +61 -0
  76. package/public/docs/remote-workers.html +64 -0
  77. package/public/docs/review-inbox.html +26 -0
  78. package/public/docs/run-storage.html +23 -0
  79. package/public/docs/runs.html +141 -0
  80. package/public/docs/search.json +1 -0
  81. package/public/docs/security.html +133 -0
  82. package/public/docs/shared-projects.html +31 -0
  83. package/public/docs/ssh-projects.html +60 -0
  84. package/public/docs/style.css +768 -0
  85. package/public/docs/task-reruns.html +30 -0
  86. package/public/docs/team-collaboration.html +379 -0
  87. package/public/docs/verification.html +22 -0
  88. package/public/docs/vscode.html +53 -0
  89. package/public/docs/worker-permission-verification.html +122 -0
  90. package/public/index.html +17 -0
  91. package/scripts/check-node.mjs +7 -0
  92. package/scripts/codeflow.mjs +55 -0
  93. package/scripts/pty.py +273 -0
  94. package/src/acp-connections.js +110 -0
  95. package/src/adapters/acp-presets.js +37 -0
  96. package/src/adapters/acp-types.js +0 -0
  97. package/src/adapters/acp.js +328 -0
  98. package/src/adapters/agy-auth.js +67 -0
  99. package/src/adapters/agy-isolation.js +223 -0
  100. package/src/adapters/agy.js +297 -0
  101. package/src/adapters/claude.js +220 -0
  102. package/src/adapters/codex-app-server.js +518 -0
  103. package/src/adapters/codex.js +299 -0
  104. package/src/adapters/errors.js +57 -0
  105. package/src/adapters/index.js +91 -0
  106. package/src/adapters/managed-permissions.js +37 -0
  107. package/src/adapters/model-discovery.js +100 -0
  108. package/src/adapters/process.js +418 -0
  109. package/src/adapters/team-tools.js +102 -0
  110. package/src/adapters/types.js +0 -0
  111. package/src/agent-container.js +155 -0
  112. package/src/approvals.js +121 -0
  113. package/src/architecture.js +456 -0
  114. package/src/assets.js +44 -0
  115. package/src/automation.js +901 -0
  116. package/src/certificate-renewal.js +75 -0
  117. package/src/chat-handoff.js +55 -0
  118. package/src/checkpoint-review.js +72 -0
  119. package/src/checks.js +303 -0
  120. package/src/code-host-git.js +102 -0
  121. package/src/code-hosts-api.js +99 -0
  122. package/src/code-hosts.js +482 -0
  123. package/src/code-intelligence-worker.mjs +452 -0
  124. package/src/code-intelligence.js +141 -0
  125. package/src/collab/api.js +365 -0
  126. package/src/collab/checkpoints.js +198 -0
  127. package/src/collab/connections.js +139 -0
  128. package/src/collab/host-execution.js +347 -0
  129. package/src/collab/hub-client.js +210 -0
  130. package/src/collab/link-api.js +229 -0
  131. package/src/collab/link.js +595 -0
  132. package/src/collab/model.js +496 -0
  133. package/src/collab/runner.js +1014 -0
  134. package/src/collab/store.js +1452 -0
  135. package/src/collab/terminals.js +361 -0
  136. package/src/collaboration-reviews.js +116 -0
  137. package/src/config.js +48 -0
  138. package/src/context-budget.js +14 -0
  139. package/src/context-inspection.js +204 -0
  140. package/src/context.js +154 -0
  141. package/src/conventions.js +283 -0
  142. package/src/coordinated-api.js +247 -0
  143. package/src/deferred-launch.js +82 -0
  144. package/src/deferred-runs.js +515 -0
  145. package/src/dependency-health.js +35 -0
  146. package/src/diff.js +43 -0
  147. package/src/docs-help.js +316 -0
  148. package/src/doctor.js +232 -0
  149. package/src/engine.js +4366 -0
  150. package/src/env.js +44 -0
  151. package/src/experience.js +50 -0
  152. package/src/extensions.js +144 -0
  153. package/src/filesystem-access.js +107 -0
  154. package/src/filesystem-policy.js +238 -0
  155. package/src/filtered-task-limits.js +93 -0
  156. package/src/filtered-tasks-api.js +53 -0
  157. package/src/filtered-tasks.js +576 -0
  158. package/src/fs.js +57 -0
  159. package/src/git.js +626 -0
  160. package/src/github-review-threads.js +181 -0
  161. package/src/github.js +538 -0
  162. package/src/http.js +69 -0
  163. package/src/improve-settings.js +38 -0
  164. package/src/improve.js +187 -0
  165. package/src/integrations.js +373 -0
  166. package/src/integrity.js +259 -0
  167. package/src/journey-api.js +77 -0
  168. package/src/journey-replay.js +65 -0
  169. package/src/journey-store.js +97 -0
  170. package/src/journeys.js +60 -0
  171. package/src/json.js +81 -0
  172. package/src/launch-profiles.js +292 -0
  173. package/src/learnings.js +80 -0
  174. package/src/log.js +39 -0
  175. package/src/maintenance.js +335 -0
  176. package/src/memory-analysis.js +182 -0
  177. package/src/memory-claims.js +153 -0
  178. package/src/memory-sqlite.js +236 -0
  179. package/src/memory.js +552 -0
  180. package/src/merge-validation.js +58 -0
  181. package/src/model-catalog.js +238 -0
  182. package/src/model-research.js +91 -0
  183. package/src/notes.js +176 -0
  184. package/src/notifications.js +413 -0
  185. package/src/patterns/auto-team-model.js +212 -0
  186. package/src/patterns/auto-team.js +659 -0
  187. package/src/patterns/council.js +199 -0
  188. package/src/patterns/debate.js +188 -0
  189. package/src/patterns/findings.js +141 -0
  190. package/src/patterns/index.js +19 -0
  191. package/src/patterns/loop.js +158 -0
  192. package/src/patterns/pipeline.js +125 -0
  193. package/src/patterns/prompts.js +181 -0
  194. package/src/patterns/race.js +147 -0
  195. package/src/patterns/schemas.js +40 -0
  196. package/src/patterns/solo.js +49 -0
  197. package/src/patterns/supervisor.js +235 -0
  198. package/src/patterns/task-draft.js +29 -0
  199. package/src/patterns/tdd.js +231 -0
  200. package/src/patterns/team-strategies.js +67 -0
  201. package/src/performance.js +43 -0
  202. package/src/persistence-status.js +35 -0
  203. package/src/phone-sessions.js +96 -0
  204. package/src/preview-overlay.js +383 -0
  205. package/src/preview.js +219 -0
  206. package/src/privacy.js +23 -0
  207. package/src/project-report.js +22 -0
  208. package/src/project-team-model.js +178 -0
  209. package/src/project-team-policy.js +42 -0
  210. package/src/project-team-store.js +213 -0
  211. package/src/project-teams-api.js +36 -0
  212. package/src/project-teams.js +551 -0
  213. package/src/projects.js +286 -0
  214. package/src/provider-eligibility.js +9 -0
  215. package/src/readiness.js +98 -0
  216. package/src/remote-tls.js +66 -0
  217. package/src/remote-workers/bundles.js +198 -0
  218. package/src/remote-workers/client.js +332 -0
  219. package/src/remote-workers/durability.js +37 -0
  220. package/src/remote-workers/protocol.js +68 -0
  221. package/src/remote-workers/stdio.js +138 -0
  222. package/src/remote-workers/worker.js +304 -0
  223. package/src/remote.js +204 -0
  224. package/src/repo-config.js +201 -0
  225. package/src/repo-coordination.js +732 -0
  226. package/src/repo-tree.js +106 -0
  227. package/src/retention-policy.js +466 -0
  228. package/src/review-inbox.js +38 -0
  229. package/src/routing.js +49 -0
  230. package/src/run-analysis.js +111 -0
  231. package/src/run-cold-store.js +231 -0
  232. package/src/run-environment.js +15 -0
  233. package/src/run-history.js +91 -0
  234. package/src/run-log.js +160 -0
  235. package/src/run-permissions.js +110 -0
  236. package/src/run-projection.js +75 -0
  237. package/src/run-recovery.js +53 -0
  238. package/src/run-status.js +54 -0
  239. package/src/serve.js +138 -0
  240. package/src/server.js +2738 -0
  241. package/src/shared-access.js +103 -0
  242. package/src/shared-accounts.js +301 -0
  243. package/src/shared-api.js +239 -0
  244. package/src/shell.js +346 -0
  245. package/src/ship.js +344 -0
  246. package/src/slots.js +74 -0
  247. package/src/snapshot.js +388 -0
  248. package/src/source-context.js +17 -0
  249. package/src/sse-writer.js +70 -0
  250. package/src/ssh-projects/bootstrap.mjs +95 -0
  251. package/src/ssh-projects/config.js +45 -0
  252. package/src/ssh-projects/gateway.js +143 -0
  253. package/src/ssh-projects/manager.js +320 -0
  254. package/src/ssh-projects/team-autostart.mjs +93 -0
  255. package/src/ssh-projects/team-remove.mjs +69 -0
  256. package/src/ssh-projects/team-status.mjs +39 -0
  257. package/src/ssh-projects/transport.js +116 -0
  258. package/src/stats.js +139 -0
  259. package/src/store.js +544 -0
  260. package/src/stream.js +91 -0
  261. package/src/sweeps.js +241 -0
  262. package/src/tailscale.js +101 -0
  263. package/src/task-draft.js +23 -0
  264. package/src/team-evaluation.js +242 -0
  265. package/src/team-mailbox.js +160 -0
  266. package/src/team-memory.js +84 -0
  267. package/src/team-rerun-graph.js +15 -0
  268. package/src/team-reruns.js +130 -0
  269. package/src/team-scheduler.js +121 -0
  270. package/src/team-template-store.js +98 -0
  271. package/src/team-templates.js +117 -0
  272. package/src/team-tool-client.mjs +79 -0
  273. package/src/team-tools.js +176 -0
  274. package/src/teams.js +0 -0
  275. package/src/templates.js +62 -0
  276. package/src/transcript.js +134 -0
  277. package/src/verification-integrity.js +27 -0
  278. package/src/verification.js +183 -0
  279. package/src/watch.js +195 -0
  280. package/src/work-queue.js +47 -0
  281. package/src/workflow-control.js +83 -0
  282. package/src/workspace-api.js +207 -0
  283. package/src/workspace-changes.js +61 -0
  284. package/src/workspace.js +146 -0
package/src/git.js ADDED
@@ -0,0 +1,626 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, existsSync, mkdirSync, openSync, realpathSync } from "node:fs";
3
+ import { randomBytes } from "node:crypto";
4
+ import { appendFile, copyFile, lstat, mkdir, readdir, readFile, realpath, rm } from "node:fs/promises";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { envNumber, timerMs, WORKTREE_DIR } from "./config.js";
7
+ import { childEnv } from "./env.js";
8
+ import { logWarn } from "./log.js";
9
+ const GIT_TIMEOUT_MS = timerMs(envNumber("CODEFLOW_GIT_TIMEOUT_SEC", 300, { min: 1 }) * 1e3);
10
+ const GIT_MAX_OUTPUT = 64 * 1024 * 1024;
11
+ function gitEnv(extra) {
12
+ return childEnv({ GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_OPTIONAL_LOCKS: "0", LANGUAGE: "C", LC_MESSAGES: "C", ...extra });
13
+ }
14
+ const folderOf = (cwd) => typeof cwd === "string" ? cwd : cwd.path;
15
+ function gitCommand(args, cwd, opts = {}) {
16
+ const own = ["-c", "core.fsmonitor=false", ...opts.hooks ? [] : ["-c", "core.hooksPath=/dev/null"]];
17
+ const ceiling = opts.discover ? {} : { GIT_CEILING_DIRECTORIES: dirname(realPathSync(folderOf(cwd))) };
18
+ const pinned = typeof cwd === "string" ? {} : { GIT_DIR: cwd.gitDir, GIT_WORK_TREE: cwd.path };
19
+ return { args: [...own, ...args], env: gitEnv({ ...ceiling, ...pinned, ...opts.env }) };
20
+ }
21
+ function realPathSync(path) {
22
+ try {
23
+ return realpathSync(path);
24
+ } catch {
25
+ return resolve(path);
26
+ }
27
+ }
28
+ const realPath = (path) => realpath(path).catch(() => resolve(path));
29
+ function git(args, cwd, opts = {}) {
30
+ const timeoutMs = opts.timeoutMs ?? GIT_TIMEOUT_MS;
31
+ return new Promise((resolve2) => {
32
+ const command = gitCommand(args, cwd, opts);
33
+ const child = spawn("git", command.args, { cwd: folderOf(cwd), env: command.env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
34
+ const out = [];
35
+ const err = [];
36
+ let outBytes = 0;
37
+ let errBytes = 0;
38
+ let overflow = false;
39
+ let timedOut = false;
40
+ let aborted = false;
41
+ let settled = false;
42
+ const kill = () => {
43
+ try {
44
+ process.kill(-child.pid, "SIGKILL");
45
+ } catch {
46
+ child.kill("SIGKILL");
47
+ }
48
+ };
49
+ const timer = setTimeout(() => {
50
+ timedOut = true;
51
+ kill();
52
+ }, timeoutMs);
53
+ timer.unref();
54
+ const onAbort = () => {
55
+ aborted = true;
56
+ kill();
57
+ };
58
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
59
+ if (opts.signal?.aborted) onAbort();
60
+ child.stdout.on("data", (chunk) => {
61
+ if (overflow) return;
62
+ outBytes += chunk.length;
63
+ if (outBytes > GIT_MAX_OUTPUT) {
64
+ overflow = true;
65
+ kill();
66
+ } else out.push(chunk);
67
+ });
68
+ child.stderr.on("data", (chunk) => {
69
+ if (errBytes > 1024 * 1024) return;
70
+ errBytes += chunk.length;
71
+ err.push(chunk);
72
+ });
73
+ const finish = (exitCode) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ clearTimeout(timer);
77
+ opts.signal?.removeEventListener("abort", onAbort);
78
+ const stdout = Buffer.concat(out).toString();
79
+ const stderr = Buffer.concat(err).toString();
80
+ if (overflow) resolve2({ code: 1, stdout: "", stderr: `git ${args[0]} printed more than ${GIT_MAX_OUTPUT / 1024 / 1024} MB` });
81
+ else if (aborted) resolve2({ code: 1, stdout: "", stderr: `git ${args[0]} was cancelled`, aborted: true });
82
+ else if (timedOut) resolve2({ code: 1, stdout, stderr: `git ${args[0]} timed out after ${Math.round(timeoutMs / 1e3)}s
83
+ ${stderr}`.trim(), timedOut: true });
84
+ else resolve2({ code: exitCode ?? 1, stdout, stderr });
85
+ };
86
+ child.on("error", (e) => {
87
+ err.push(Buffer.from(e.message));
88
+ finish(1);
89
+ });
90
+ child.on("close", (code) => finish(code));
91
+ child.on("exit", (code) => {
92
+ setTimeout(() => {
93
+ child.stdout.destroy();
94
+ child.stderr.destroy();
95
+ finish(code);
96
+ }, 2e3).unref();
97
+ });
98
+ child.stdin.on("error", () => {
99
+ });
100
+ child.stdin.end(opts.input);
101
+ });
102
+ }
103
+ function gitError(args, res) {
104
+ const text = (res.stderr.trim() || res.stdout.trim()).slice(0, 2e3);
105
+ return `git ${args.join(" ").slice(0, 200)} failed: ${text}`;
106
+ }
107
+ async function gitOk(args, cwd, opts) {
108
+ const res = await git(args, cwd, opts);
109
+ if (res.code !== 0) throw new Error(gitError(args, res));
110
+ return res.stdout.trim();
111
+ }
112
+ function classifyGitFailure(res) {
113
+ if (res.timedOut) return "timeout";
114
+ const text = `${res.stderr}
115
+ ${res.stdout}`;
116
+ if (/index\.lock|Unable to create '.*\.lock'|another git process seems to be running/i.test(text)) return "locked";
117
+ if (/untracked working tree files would be (?:overwritten|removed)/i.test(text)) return "untracked";
118
+ if (/local changes to the following files would be overwritten|Please commit your changes or stash them/i.test(text)) return "dirty";
119
+ if (/^CONFLICT|Automatic merge failed|fix conflicts and then commit/im.test(text)) return "conflict";
120
+ if (/Please tell me who you are|unable to auto-detect email address|empty ident name/i.test(text)) return "identity";
121
+ if (/Authentication failed|could not read (?:Username|Password)|terminal prompts disabled|Permission denied \(publickey|returned error: 403|Repository not found/i.test(text)) return "auth";
122
+ if (/hook\b.*(?:failed|declined|exit)|(?:pre|post)-\w+ hook/i.test(text)) return "hook";
123
+ return "other";
124
+ }
125
+ function describeGitFailure(kind) {
126
+ switch (kind) {
127
+ case "locked":
128
+ return "another git process is using this repository (an index.lock exists); try again when it finishes";
129
+ case "untracked":
130
+ return "untracked files in your checkout would be overwritten";
131
+ case "dirty":
132
+ return "your checkout has uncommitted changes that would be overwritten";
133
+ case "conflict":
134
+ return "the branches conflict";
135
+ case "hook":
136
+ return "a git hook failed";
137
+ case "identity":
138
+ return "git doesn't know your name and email (set user.name and user.email)";
139
+ case "auth":
140
+ return "git couldn't sign in to the remote (check your credentials)";
141
+ case "timeout":
142
+ return "git took too long";
143
+ default:
144
+ return "git failed";
145
+ }
146
+ }
147
+ let versionPromise;
148
+ function gitVersion() {
149
+ const pending = versionPromise ??= git(["--version"], process.cwd(), { timeoutMs: 1e4 }).then((res) => {
150
+ const m = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(res.stdout);
151
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)] : void 0;
152
+ });
153
+ void pending.then((v) => {
154
+ if (!v && versionPromise === pending) versionPromise = void 0;
155
+ });
156
+ return pending;
157
+ }
158
+ async function gitAtLeast(major, minor) {
159
+ const v = await gitVersion();
160
+ return !!v && (v[0] > major || v[0] === major && v[1] >= minor);
161
+ }
162
+ function serialize(locks, key, fn) {
163
+ const prev = locks.get(key) ?? Promise.resolve();
164
+ const next = prev.catch(() => void 0).then(fn);
165
+ const tail = next.catch(() => void 0);
166
+ locks.set(key, tail);
167
+ void tail.then(() => {
168
+ if (locks.get(key) === tail) locks.delete(key);
169
+ });
170
+ return next;
171
+ }
172
+ const repoLocks = /* @__PURE__ */ new Map();
173
+ function withRepoLock(repo, fn) {
174
+ return serialize(repoLocks, resolve(repo), fn);
175
+ }
176
+ const worktreeLocks = /* @__PURE__ */ new Map();
177
+ function withWorktreeLock(cwd, fn) {
178
+ return serialize(worktreeLocks, resolve(cwd), fn);
179
+ }
180
+ async function repoRoot(path) {
181
+ return gitOk(["rev-parse", "--show-toplevel"], path, { discover: true });
182
+ }
183
+ async function resolveCommit(repo, ref = "HEAD") {
184
+ return gitOk(["rev-parse", "--verify", `${ref}^{commit}`], repo);
185
+ }
186
+ class WorktreeMismatchError extends Error {
187
+ }
188
+ async function worktreeGit(repo, path) {
189
+ const found = await findGitDir(repo, path);
190
+ if (!found.gitDir) {
191
+ throw new WorktreeMismatchError(`${path} isn't a checkout of ${repo} (no git folder of ${repo}'s names it), so code-flow didn't run git there.`);
192
+ }
193
+ return { path: found.path, gitDir: found.gitDir };
194
+ }
195
+ async function commonDir(repo) {
196
+ return realPath(resolve(repo, await gitOk(["rev-parse", "--git-common-dir"], repo)));
197
+ }
198
+ const realPathOfEntry = async (path) => join(await realPath(dirname(path)), basename(path));
199
+ async function namedGitDir(dotGit) {
200
+ const info = await lstat(dotGit).catch(() => void 0);
201
+ if (!info?.isFile() || info.size > 4096) return void 0;
202
+ const named = /^gitdir: *(.+?)\s*$/m.exec(await readFile(dotGit, "utf8").catch(() => ""))?.[1];
203
+ return named ? realPath(resolve(dirname(dotGit), named)) : void 0;
204
+ }
205
+ async function namesCheckout(gitDir, dotGit) {
206
+ const back = (await readFile(join(gitDir, "gitdir"), "utf8").catch(() => "")).trim();
207
+ return !!back && await realPathOfEntry(resolve(gitDir, back)) === dotGit;
208
+ }
209
+ async function findGitDir(repo, path) {
210
+ const common = await commonDir(repo);
211
+ const real = await realPath(path);
212
+ const dotGit = join(real, ".git");
213
+ const worktrees = join(common, "worktrees");
214
+ const hint = await namedGitDir(dotGit);
215
+ if (hint && dirname(hint) === worktrees && await namesCheckout(hint, dotGit)) return { path: real, common, gitDir: hint };
216
+ for (const entry of await readdir(worktrees).catch(() => [])) {
217
+ if (await namesCheckout(join(worktrees, entry), dotGit)) return { path: real, common, gitDir: join(worktrees, entry) };
218
+ }
219
+ const main = await mainWorktree(repo);
220
+ return { path: real, common, gitDir: main && await realPath(main) === real ? common : void 0 };
221
+ }
222
+ async function assertWorktree(repo, wt) {
223
+ const found = await findGitDir(repo, wt.path);
224
+ const problem = await dotGitProblem(repo, found);
225
+ const checkout = found.gitDir && !problem ? { path: found.path, gitDir: found.gitDir } : void 0;
226
+ const branchProblem = checkout && wt.branch ? await headProblem(checkout, wt.branch) : void 0;
227
+ if (!checkout || branchProblem) {
228
+ throw new WorktreeMismatchError(
229
+ `${wt.path} isn't the checkout code-flow expects (${problem ?? branchProblem ?? `no git folder of ${repo}'s names it`}), so code-flow didn't run git there: it could have changed another checkout or branch, such as yours. Nothing was changed.`
230
+ );
231
+ }
232
+ return checkout;
233
+ }
234
+ async function dotGitProblem(repo, found) {
235
+ const dotGit = join(found.path, ".git");
236
+ const info = await lstat(dotGit).catch(() => void 0);
237
+ if (!info) return "its .git is missing";
238
+ let named;
239
+ if (info.isDirectory()) named = await realPath(dotGit);
240
+ else if (info.isFile()) {
241
+ const gitDir = await namedGitDir(dotGit);
242
+ if (!gitDir) return "its .git doesn't name a git folder";
243
+ named = gitDir;
244
+ } else return `its .git is a ${info.isSymbolicLink() ? "symbolic link" : "special file"}`;
245
+ if (named === found.gitDir) return void 0;
246
+ if (named === found.common) return `its .git points to the git folder of ${await mainWorktree(repo) ?? repo} itself`;
247
+ if (dirname(named) === join(found.common, "worktrees")) {
248
+ return existsSync(join(named, "gitdir")) ? `its .git points to the git folder of another worktree (${named})` : `its .git points to ${named}, which isn't there any more`;
249
+ }
250
+ return `its .git points to ${named}, which isn't one of ${repo}'s worktrees`;
251
+ }
252
+ async function headProblem(checkout, branch) {
253
+ const head = (await git(["symbolic-ref", "-q", "HEAD"], checkout)).stdout.trim();
254
+ if (head === `refs/heads/${branch}`) return void 0;
255
+ return head ? `it's on ${head.replace(/^refs\/heads\//, "")}, not ${branch}` : `its HEAD is detached, not on ${branch}`;
256
+ }
257
+ async function mainWorktree(repo) {
258
+ const lines = (await gitOk(["worktree", "list", "--porcelain"], repo)).split("\n");
259
+ const end = lines.indexOf("");
260
+ const first = lines.slice(0, end === -1 ? void 0 : end);
261
+ return first.includes("bare") ? void 0 : first.find((l) => l.startsWith("worktree "))?.slice("worktree ".length);
262
+ }
263
+ const IDENTITY = ["-c", "user.name=code-flow", "-c", "user.email=code-flow@localhost"];
264
+ const RAW_DIFF = ["--no-ext-diff", "--no-textconv"];
265
+ function worktreeRoot(repo) {
266
+ return resolve(repo, WORKTREE_DIR);
267
+ }
268
+ const CONTEXT_DIR = ".codeflow-context";
269
+ async function ensureCodeflowExcludes(repo) {
270
+ const patterns = [`/${CONTEXT_DIR}/`];
271
+ const rel = relative(repo, worktreeRoot(repo));
272
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) patterns.unshift(`/${rel.split(sep).join("/")}/`);
273
+ const excludeFile = resolve(repo, await gitOk(["rev-parse", "--git-path", "info/exclude"], repo));
274
+ const current = await readFile(excludeFile, "utf8").catch(() => "");
275
+ const existing = new Set(current.split("\n").map((line) => line.trim()));
276
+ const missing = patterns.filter((p) => !existing.has(p));
277
+ if (!missing.length) return;
278
+ await mkdir(dirname(excludeFile), { recursive: true });
279
+ const newline = current && !current.endsWith("\n") ? "\n" : "";
280
+ await appendFile(excludeFile, `${newline}# code-flow
281
+ ${missing.join("\n")}
282
+ `);
283
+ }
284
+ function gitWithInput(args, cwd, input) {
285
+ return git(args, cwd, { input });
286
+ }
287
+ async function objectTypeAt(repo, ref, path) {
288
+ const res = await git(["cat-file", "-t", `${ref}:${path}`], repo);
289
+ const type = res.stdout.trim();
290
+ return res.code === 0 && (type === "blob" || type === "tree") ? type : null;
291
+ }
292
+ async function workingCopyDiffers(repo, ref, path) {
293
+ return (await git(["diff", "--quiet", ...RAW_DIFF, ref, "--", path], repo)).code !== 0;
294
+ }
295
+ const SUBMODULE_TIMEOUT_MS = 10 * 6e4;
296
+ async function registerSubmodules(path) {
297
+ if (!existsSync(join(path, ".gitmodules"))) return void 0;
298
+ const args = ["submodule", "init"];
299
+ const res = await git(args, path);
300
+ return res.code === 0 ? void 0 : submoduleWarning(path, args, res);
301
+ }
302
+ async function updateSubmodules(path, signal) {
303
+ if (!existsSync(join(path, ".gitmodules"))) return void 0;
304
+ const args = ["submodule", "update", "--init", "--recursive"];
305
+ const res = await git(args, path, { timeoutMs: SUBMODULE_TIMEOUT_MS, signal });
306
+ return res.code === 0 ? void 0 : submoduleWarning(path, args, res);
307
+ }
308
+ const submoduleWarning = (path, args, res) => `Submodules weren't checked out in ${path}, so setup and checks that need them may fail. ${gitError(args, res)}`;
309
+ async function openWorktree(repo, name, path, branch, fromRef, opts = {}) {
310
+ const base = await resolveCommit(repo, fromRef);
311
+ const wt = { name, path, branch, base };
312
+ if (existsSync(join(path, ".git"))) return { wt, created: false };
313
+ const opened = await withRepoLock(repo, async () => {
314
+ await mkdir(dirname(path), { recursive: true });
315
+ let created = false;
316
+ if ((await git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repo)).code === 0) {
317
+ await git(["worktree", "prune"], repo);
318
+ await gitOk(["worktree", "add", path, branch], repo);
319
+ } else {
320
+ await gitOk(["worktree", "add", "-b", branch, path, fromRef], repo);
321
+ created = true;
322
+ }
323
+ return { created, warning: await registerSubmodules(path) };
324
+ });
325
+ const warning = opened.warning ?? await updateSubmodules(path, opts.signal);
326
+ return warning ? { wt, created: opened.created, warning } : { wt, created: opened.created };
327
+ }
328
+ async function openDetachedWorktree(repo, path, ref, opts = {}) {
329
+ await removeDetachedWorktree(repo, path);
330
+ const registered = await withRepoLock(repo, async () => {
331
+ await mkdir(dirname(path), { recursive: true });
332
+ await gitOk(["worktree", "add", "--detach", path, ref], repo);
333
+ return registerSubmodules(path);
334
+ });
335
+ const warning = registered ?? await updateSubmodules(path, opts.signal);
336
+ return warning ? { warning } : {};
337
+ }
338
+ async function removeDetachedWorktree(repo, path) {
339
+ await withRepoLock(repo, async () => {
340
+ if (existsSync(path)) await git(["worktree", "remove", "--force", path], repo);
341
+ await rm(path, { recursive: true, force: true });
342
+ await git(["worktree", "prune"], repo);
343
+ });
344
+ }
345
+ async function listBranches(repo) {
346
+ const out = await gitOk(
347
+ [
348
+ "for-each-ref",
349
+ "--sort=-committerdate",
350
+ "--format=%(refname)%09%(refname:short)%09%(committerdate:relative)",
351
+ "refs/heads",
352
+ "refs/remotes"
353
+ ],
354
+ repo
355
+ );
356
+ const branches = [];
357
+ for (const line of out.split("\n").filter(Boolean)) {
358
+ const [ref = "", name = "", updated = ""] = line.split(" ");
359
+ if (ref.startsWith("refs/remotes/") && ref.endsWith("/HEAD")) continue;
360
+ const kind = ref.startsWith("refs/remotes/") ? "remote" : ref.startsWith("refs/heads/codeflow/") ? "run" : "local";
361
+ branches.push({ name, kind, updated });
362
+ }
363
+ const head = await git(["symbolic-ref", "--short", "-q", "HEAD"], repo);
364
+ return { current: head.code === 0 ? head.stdout.trim() : null, branches };
365
+ }
366
+ async function validateBranchPrefix(repo, prefix) {
367
+ if ((await git(["check-ref-format", "--branch", `${prefix}/x`], repo)).code !== 0) {
368
+ throw new Error(`"${prefix}" is not a valid branch name prefix`);
369
+ }
370
+ if ((await git(["show-ref", "--verify", "--quiet", `refs/heads/${prefix}`], repo)).code === 0) {
371
+ throw new Error(`A branch named "${prefix}" already exists, so "${prefix}/\u2026" branches can't be created`);
372
+ }
373
+ if (await gitOk(["for-each-ref", "--count=1", "--format=%(refname:short)", `refs/heads/${prefix}/`], repo)) {
374
+ throw new Error(`Branches under "${prefix}/" already exist; pick another prefix`);
375
+ }
376
+ }
377
+ async function commitAll(repo, wt, message, onWarning) {
378
+ const at = await assertWorktree(repo, wt);
379
+ await gitOk(["add", "-A"], at);
380
+ await unstageSecrets(at, onWarning);
381
+ const staged = await git(["diff", "--cached", "--quiet", ...RAW_DIFF], at);
382
+ if (staged.code === 0 && !await isMerging(at)) return null;
383
+ await gitOk([...IDENTITY, "commit", "--no-verify", "-m", message], at);
384
+ return resolveCommit(at);
385
+ }
386
+ const COPIED_FILES = "codeflow-copied";
387
+ const SECRET_NAME = /^(?:\.env(?:\..+)?|.+\.pem|id_rsa.*|id_ed25519.*)$/;
388
+ const SECRET_TEMPLATE = /^\.env\.(?:example|sample|template)$/;
389
+ async function unstageSecrets(at, onWarning) {
390
+ const staged = await git(["diff", "--cached", "--name-status", "--no-renames", "-z", ...RAW_DIFF], at);
391
+ if (staged.code !== 0 || !staged.stdout) return;
392
+ const record = (await git(["rev-parse", "--git-path", COPIED_FILES], at)).stdout.trim();
393
+ const copied = new Set(record ? (await readFile(resolve(folderOf(at), record), "utf8").catch(() => "")).split("\0").filter(Boolean) : []);
394
+ const tokens = staged.stdout.split("\0");
395
+ const drop = [];
396
+ for (let i = 0; i + 1 < tokens.length; i += 2) {
397
+ const [status, path] = [tokens[i], tokens[i + 1]];
398
+ const name = path.slice(path.lastIndexOf("/") + 1);
399
+ const secret = status === "A" && SECRET_NAME.test(name) && !SECRET_TEMPLATE.test(name);
400
+ if (copied.has(path) && status !== "D" || secret) drop.push(path);
401
+ }
402
+ if (!drop.length) return;
403
+ await gitOk(["reset", "-q", "--", ...drop], at, { env: { GIT_LITERAL_PATHSPECS: "1" } });
404
+ const text = `Left ${drop.join(", ")} out of the commit: ${drop.length === 1 ? "it was" : "they were"} copied in from your checkout or ${drop.length === 1 ? "is" : "are"} named like a secret. Commit it yourself if it belongs in the repository.`;
405
+ logWarn(text, { worktree: folderOf(at) });
406
+ onWarning?.(text);
407
+ }
408
+ async function diffSince(at, base) {
409
+ return gitOk(["diff", "--no-color", ...RAW_DIFF, `${base}..HEAD`], at);
410
+ }
411
+ async function diffStat(at, base) {
412
+ return gitOk(["diff", "--stat", "--no-color", ...RAW_DIFF, `${base}..HEAD`], at);
413
+ }
414
+ async function changedFiles(cwd, from, to = "HEAD") {
415
+ const out = await gitOk(["diff", "--name-only", ...RAW_DIFF, `${from}..${to}`], cwd);
416
+ return out ? out.split("\n") : [];
417
+ }
418
+ async function mergeBranch(repo, wt, branch) {
419
+ const at = await assertWorktree(repo, wt);
420
+ const res = await git([...IDENTITY, "merge", "--no-ff", "--no-edit", branch], at);
421
+ if (res.code === 0) return { ok: true, conflicts: [], output: res.stdout };
422
+ const conflicts = await git(["diff", "--name-only", "--diff-filter=U", ...RAW_DIFF], at);
423
+ return {
424
+ ok: false,
425
+ conflicts: conflicts.stdout.trim().split("\n").filter(Boolean),
426
+ output: `${res.stdout}
427
+ ${res.stderr}`.trim()
428
+ };
429
+ }
430
+ async function isClean(cwd) {
431
+ return await gitOk(["status", "--porcelain"], cwd) === "";
432
+ }
433
+ async function uncommittedCount(cwd) {
434
+ const out = await gitOk(["status", "--porcelain"], cwd);
435
+ return out ? out.split("\n").length : 0;
436
+ }
437
+ async function discardChanges(repo, wt, to = "HEAD") {
438
+ const at = await assertWorktree(repo, wt);
439
+ await gitOk(["reset", "--hard", "-q", to], at);
440
+ await gitOk(["clean", "-fdq"], at);
441
+ }
442
+ const SNAPSHOT_FILE_LIMIT = { bytes: 8 * 1024 * 1024, files: 2e3 };
443
+ const HASH_AS_IS = ["hash-object", "--no-filters", "--stdin-paths"];
444
+ async function saveUntracked(at, paths) {
445
+ const files = [];
446
+ for (const path of paths) {
447
+ if (files.length >= SNAPSHOT_FILE_LIMIT.files) break;
448
+ if (path.includes("\n")) continue;
449
+ const info = await lstat(join(at.path, path)).catch(() => void 0);
450
+ if (info?.isFile() && info.size <= SNAPSHOT_FILE_LIMIT.bytes) files.push(path);
451
+ }
452
+ if (!files.length) return {};
453
+ const ids = (await gitOk([...HASH_AS_IS, "-w"], at, { input: `${files.join("\n")}
454
+ ` })).split("\n");
455
+ return Object.fromEntries(files.map((f, i) => [f, ids[i]]).filter(([, id]) => /^[0-9a-f]{40,64}$/.test(id)));
456
+ }
457
+ function writeBlob(at, id, path) {
458
+ return new Promise((resolve2, reject) => {
459
+ const target = join(at.path, path);
460
+ mkdirSync(dirname(target), { recursive: true });
461
+ const fd = openSync(target, "w");
462
+ const command = gitCommand(["cat-file", "blob", id], at);
463
+ const child = spawn("git", command.args, { cwd: at.path, env: command.env, stdio: ["ignore", fd, "pipe"] });
464
+ let stderr = "";
465
+ child.stderr?.on("data", (c) => stderr += c.toString());
466
+ child.on("error", (err) => {
467
+ closeSync(fd);
468
+ reject(err);
469
+ });
470
+ child.on("close", (code) => {
471
+ closeSync(fd);
472
+ if (code === 0) resolve2();
473
+ else reject(new Error(`git cat-file blob ${id} failed: ${stderr.trim()}`));
474
+ });
475
+ });
476
+ }
477
+ async function stashCreate(at) {
478
+ const index = resolve(at.path, await gitOk(["rev-parse", "--git-path", "index"], at));
479
+ const copy = `${index}.codeflow-${process.pid}-${randomBytes(4).toString("hex")}`;
480
+ const copied = await copyFile(index, copy).then(() => true, () => false);
481
+ const env = copied ? { env: { GIT_INDEX_FILE: copy } } : void 0;
482
+ try {
483
+ if (copied) await git(["update-index", "-q", "--refresh"], at, env);
484
+ return (await gitOk(["stash", "create"], at, env)).trim();
485
+ } finally {
486
+ if (copied) await Promise.all([rm(copy, { force: true }), rm(`${copy}.lock`, { force: true })]);
487
+ }
488
+ }
489
+ async function gitOkWhenFree(args, at) {
490
+ for (let attempt = 0; ; attempt++) {
491
+ const res = await git(args, at);
492
+ if (res.code === 0) return res.stdout.trim();
493
+ if (attempt >= 5 || classifyGitFailure(res) !== "locked") throw new Error(gitError(args, res));
494
+ await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
495
+ }
496
+ }
497
+ async function uncommittedTree(at) {
498
+ const sha = await stashCreate(at);
499
+ return sha ? (await gitOk(["rev-parse", `${sha}^{tree}`], at)).trim() : "";
500
+ }
501
+ const untrackedFiles = async (at) => (await gitOk(["ls-files", "--others", "--exclude-standard", "-z"], at)).split("\0").filter(Boolean);
502
+ async function snapshot(repo, cwd) {
503
+ return withWorktreeLock(cwd, async () => {
504
+ const at = await assertWorktree(repo, { path: cwd });
505
+ const untracked = await untrackedFiles(at);
506
+ return {
507
+ head: await resolveCommit(at),
508
+ changes: await stashCreate(at),
509
+ untracked,
510
+ untrackedBlobs: await saveUntracked(at, untracked)
511
+ };
512
+ });
513
+ }
514
+ async function restoreSnapshot(repo, wt, snap) {
515
+ return compareSnapshot(repo, wt, snap, true);
516
+ }
517
+ async function changedSince(repo, wt, snap) {
518
+ return compareSnapshot(repo, wt, snap, false);
519
+ }
520
+ function compareSnapshot(repo, wt, snap, undo) {
521
+ const cwd = wt.path;
522
+ return withWorktreeLock(cwd, async () => {
523
+ const at = await assertWorktree(repo, undo ? wt : { path: cwd });
524
+ const found = [];
525
+ const head = await resolveCommit(at);
526
+ const changesNow = await uncommittedTree(at);
527
+ const changesBefore = snap.changes ? (await gitOk(["rev-parse", `${snap.changes}^{tree}`], at)).trim() : "";
528
+ if (head !== snap.head) found.push("commits");
529
+ if (head !== snap.head || changesNow !== changesBefore) {
530
+ if (head === snap.head) found.push("edits");
531
+ if (undo) {
532
+ await gitOkWhenFree(["reset", "--hard", "-q", snap.head], at);
533
+ if (snap.changes) await gitOkWhenFree(["stash", "apply", "-q", snap.changes], at);
534
+ }
535
+ }
536
+ const before = new Set(snap.untracked);
537
+ const added = (await untrackedFiles(at)).filter((f) => !before.has(f));
538
+ if (added.length) {
539
+ found.push(`${added.length} new file${added.length === 1 ? "" : "s"}`);
540
+ if (undo) for (let i = 0; i < added.length; i += 100) await gitOkWhenFree(["clean", "-fq", "--", ...added.slice(i, i + 100)], at);
541
+ }
542
+ const saved = Object.entries(snap.untrackedBlobs ?? {});
543
+ if (saved.length) {
544
+ const present = (await Promise.all(saved.map(async ([path]) => (await lstat(join(at.path, path)).catch(() => void 0))?.isFile() ? path : void 0))).filter(
545
+ (p) => !!p
546
+ );
547
+ const now = present.length ? (await gitOk(HASH_AS_IS, at, { input: `${present.join("\n")}
548
+ ` })).split("\n") : [];
549
+ const current = new Map(present.map((p, i) => [p, now[i]]));
550
+ const changed = saved.filter(([path, id]) => current.get(path) !== id);
551
+ for (const [path, id] of undo ? changed : []) {
552
+ await rm(join(at.path, path), { recursive: true, force: true });
553
+ await writeBlob(at, id, path);
554
+ }
555
+ if (changed.length) found.push(`${changed.length} untracked file${changed.length === 1 ? "" : "s"} ${undo ? "put back" : "changed"}`);
556
+ }
557
+ return found;
558
+ });
559
+ }
560
+ async function isMerging(cwd) {
561
+ return (await git(["rev-parse", "-q", "--verify", "MERGE_HEAD"], cwd)).code === 0;
562
+ }
563
+ async function branchExists(repo, branch) {
564
+ return (await git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repo)).code === 0;
565
+ }
566
+ async function isAncestor(cwd, a, b = "HEAD") {
567
+ return (await git(["merge-base", "--is-ancestor", a, b], cwd)).code === 0;
568
+ }
569
+ async function restorePaths(repo, wt, ref, paths) {
570
+ if (!paths.length) return;
571
+ const at = await assertWorktree(repo, wt);
572
+ for (const path of paths) {
573
+ if (await objectTypeAt(at, ref, path)) await gitOk(["checkout", ref, "--", path], at);
574
+ else await git(["rm", "-q", "-f", "--ignore-unmatch", "--", path], at);
575
+ }
576
+ }
577
+ async function validRef(repo, ref) {
578
+ return !!ref && !ref.startsWith("-") && (await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], repo)).code === 0;
579
+ }
580
+ async function hasConflictMarkers(at) {
581
+ const res = await git(["diff", "--check", ...RAW_DIFF], at);
582
+ return /conflict marker/i.test(res.stdout + res.stderr);
583
+ }
584
+ export {
585
+ CONTEXT_DIR,
586
+ COPIED_FILES,
587
+ WorktreeMismatchError,
588
+ assertWorktree,
589
+ branchExists,
590
+ changedFiles,
591
+ changedSince,
592
+ classifyGitFailure,
593
+ commitAll,
594
+ describeGitFailure,
595
+ diffSince,
596
+ diffStat,
597
+ discardChanges,
598
+ ensureCodeflowExcludes,
599
+ git,
600
+ gitAtLeast,
601
+ gitError,
602
+ gitVersion,
603
+ gitWithInput,
604
+ hasConflictMarkers,
605
+ isAncestor,
606
+ isClean,
607
+ isMerging,
608
+ listBranches,
609
+ mergeBranch,
610
+ objectTypeAt,
611
+ openDetachedWorktree,
612
+ openWorktree,
613
+ removeDetachedWorktree,
614
+ repoRoot,
615
+ resolveCommit,
616
+ restorePaths,
617
+ restoreSnapshot,
618
+ snapshot,
619
+ uncommittedCount,
620
+ validRef,
621
+ validateBranchPrefix,
622
+ withRepoLock,
623
+ workingCopyDiffers,
624
+ worktreeGit,
625
+ worktreeRoot
626
+ };