@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
@@ -0,0 +1,515 @@
1
+ import { permissionRequirements } from "./run-permissions.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { mkdir, open, rename, rm } from "node:fs/promises";
5
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { CODEFLOW_HOME } from "./config.js";
7
+ import { validateAgentSettings } from "./adapters/index.js";
8
+ import { validateAgentContainer } from "./agent-container.js";
9
+ import { validateVerificationProfile } from "./verification.js";
10
+ import { validateContextBudget } from "./context-budget.js";
11
+ import { exhausted } from "./routing.js";
12
+ import { redactText } from "./privacy.js";
13
+ class DeferredRunConflictError extends Error {
14
+ constructor(current2) {
15
+ super("This deferred job changed. Refresh before acting on it.");
16
+ this.current = current2;
17
+ }
18
+ status = 409;
19
+ }
20
+ class DeferredRunRejectedError extends Error {
21
+ }
22
+ const AGENTS = ["claude", "codex", "agy"];
23
+ const STATUSES = ["queued", "waiting-time", "waiting-provider", "claimed", "started", "failed", "unknown", "cancelled"];
24
+ const PENDING = /* @__PURE__ */ new Set(["queued", "waiting-time", "waiting-provider"]);
25
+ const DEFERRED_RUN_LIMITS = { active: 100, retained: 200, history: 30, attempts: 3, requestBytes: 96 * 1024, fileBytes: 32 * 1024 * 1024, futureMs: 365 * 24 * 60 * 6e4 };
26
+ const queues = /* @__PURE__ */ new Map();
27
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
28
+ const clone = (value) => structuredClone(value);
29
+ function text(value, max, label) {
30
+ if (typeof value !== "string" || !value.trim() || value.length > max || value.includes("\0")) throw new Error(`Invalid deferred ${label}`);
31
+ return value.trim();
32
+ }
33
+ function keys(value, allowed, label) {
34
+ if (!object(value) || Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(`Unsupported deferred ${label} fields`);
35
+ return value;
36
+ }
37
+ function record(raw, max, parse, label) {
38
+ if (!object(raw) || Object.keys(raw).length > max || Object.keys(raw).some((key) => !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(key))) throw new Error(`Invalid deferred ${label}`);
39
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, parse(value)]));
40
+ }
41
+ const positiveDate = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 864e13;
42
+ const diagnostic = (value) => redactText(String(value ?? "Unknown deferred run error")).replace(/\0/g, "").slice(0, 1e3);
43
+ const canonical = (value) => JSON.stringify(value, (_key, item) => object(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
44
+ function sanitizeDeferredRequest(value) {
45
+ const raw = keys(value, ["templateRef", "permissionRequirements", "repo", "project", "projectSelection", "pattern", "task", "roles", "options", "baseRef", "branchPrefix", "parentRunId", "context", "agentSettings", "roleSettings", "loop", "trust", "allowUntrusted", "approvals", "source", "externalSourceRef"], "run request");
46
+ const repo = text(raw.repo, 4e3, "repository");
47
+ if (!isAbsolute(repo)) throw new Error("Deferred runs need an absolute repository path");
48
+ const roles = record(raw.roles ?? {}, 20, (value2) => {
49
+ if (!Array.isArray(value2) || value2.length > 6 || value2.some((agent) => agent !== "auto" && !AGENTS.includes(agent))) throw new Error("Invalid deferred provider roles");
50
+ return [...new Set(value2)];
51
+ }, "roles");
52
+ const options = record(raw.options ?? {}, 64, (value2) => {
53
+ if (typeof value2 !== "boolean" && (typeof value2 !== "number" || !Number.isFinite(value2))) throw new Error("Invalid deferred pattern option");
54
+ return value2;
55
+ }, "options");
56
+ const loopRaw = raw.loop === void 0 ? {} : keys(raw.loop, ["check", "setup", "copy", "maxMinutes", "maxSteps", "maxInvocations", "maxTokens", "maxCostUsd", "checkTimeoutMin", "verification", "container", "agentContainer", "contextBudget", "remoteWorker"], "loop");
57
+ const loop = { copy: [] };
58
+ for (const key of ["check", "setup"]) if (loopRaw[key] !== void 0 && loopRaw[key] !== "") loop[key] = text(loopRaw[key], 4e3, `${key} command`);
59
+ const bounds = { maxMinutes: [1, 720], maxSteps: [1, 200], maxInvocations: [1, 200], maxTokens: [1, 1e9], maxCostUsd: [1e-3, 1e6], checkTimeoutMin: [1, 120] };
60
+ for (const key of Object.keys(bounds)) if (loopRaw[key] !== void 0) {
61
+ const number = loopRaw[key], [min, max] = bounds[key];
62
+ if (typeof number !== "number" || !Number.isFinite(number) || number < min || number > max) throw new Error(`Invalid deferred ${key}`);
63
+ loop[key] = number;
64
+ }
65
+ if (loopRaw.remoteWorker !== void 0) loop.remoteWorker = validateWorkerSelection(loopRaw.remoteWorker);
66
+ if (loopRaw.verification !== void 0) loop.verification = validateVerificationProfile(loopRaw.verification);
67
+ if (loopRaw.contextBudget !== void 0) loop.contextBudget = validateContextBudget(loopRaw.contextBudget);
68
+ if (loopRaw.agentContainer !== void 0) loop.agentContainer = validateAgentContainer(loopRaw.agentContainer);
69
+ if (loopRaw.container !== void 0) {
70
+ const container = keys(loopRaw.container, ["image", "network"], "check container");
71
+ const image = text(container.image, 200, "container image");
72
+ if (!/^[\w./:@-]+$/.test(image) || image.startsWith("-")) throw new Error("Invalid deferred container image");
73
+ if (container.network !== void 0 && typeof container.network !== "boolean") throw new Error("Invalid deferred container network");
74
+ loop.container = { image, network: container.network !== false };
75
+ }
76
+ const run = { repo, pattern: text(raw.pattern, 100, "pattern"), task: text(raw.task, 24e3, "task"), roles, options, loop, trust: "safe", allowUntrusted: false, approvals: { plan: false, tools: false } };
77
+ if (raw.permissionRequirements !== void 0) run.permissionRequirements = permissionRequirements(raw.permissionRequirements);
78
+ if (raw.templateRef !== void 0) {
79
+ const ref = keys(raw.templateRef, ["id", "version", "inputs"], "template reference");
80
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(ref.id) || !Number.isSafeInteger(ref.version) || ref.version < 1) throw new Error("Invalid deferred template version");
81
+ run.templateRef = { id: ref.id, version: ref.version, inputs: record(ref.inputs, 24, (v) => text(v, 1e4, "template input"), "template inputs") };
82
+ }
83
+ for (const key of ["project", "baseRef", "branchPrefix", "parentRunId"]) if (raw[key] !== void 0 && raw[key] !== "") run[key] = text(raw[key], key === "project" || key === "parentRunId" ? 240 : 1e3, key);
84
+ if (raw.projectSelection !== void 0) {
85
+ if (raw.projectSelection !== "explicit" || !run.project) throw new Error("Choose an explicit project for the deferred run");
86
+ run.projectSelection = "explicit";
87
+ }
88
+ if (raw.agentSettings !== void 0) run.agentSettings = validateAgentSettings(raw.agentSettings);
89
+ if (raw.roleSettings !== void 0) run.roleSettings = record(raw.roleSettings, 20, (value2) => validateAgentSettings(value2), "role settings");
90
+ if (raw.context !== void 0) {
91
+ const context = keys(raw.context, ["paths", "attachments"], "context (attachments cannot be deferred)");
92
+ if (context.attachments !== void 0 && (!Array.isArray(context.attachments) || context.attachments.length)) throw new Error("Attachments cannot be deferred; remove them before queueing the run");
93
+ if (!Array.isArray(context.paths) || context.paths.length > 50) throw new Error("Invalid deferred context paths");
94
+ run.context = { paths: context.paths.map((value2) => {
95
+ const path = text(keys(value2, ["path"], "context path").path, 2e3, "context path").replace(/\\/g, "/");
96
+ if (isAbsolute(path) || path.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("Deferred context paths must stay inside the repository");
97
+ return { path };
98
+ }) };
99
+ }
100
+ if (raw.source !== void 0) {
101
+ const source = keys(raw.source, ["kind", "number", "ciLogs", "tool", "numbers"], "GitHub source reference");
102
+ if (!["issue", "pr", "alert"].includes(source.kind) || !Number.isSafeInteger(source.number) || source.number < 1) throw new Error("Invalid deferred GitHub source reference");
103
+ if (source.ciLogs !== void 0 && typeof source.ciLogs !== "boolean") throw new Error("Invalid deferred CI log preference");
104
+ run.source = { kind: source.kind, number: source.number, ...source.ciLogs !== void 0 ? { ciLogs: source.ciLogs } : {} };
105
+ if (source.kind === "alert") {
106
+ if (!["dependabot", "code-scanning"].includes(source.tool)) throw new Error("Invalid deferred alert tool");
107
+ run.source.tool = source.tool;
108
+ if (source.numbers !== void 0) {
109
+ if (!Array.isArray(source.numbers) || source.numbers.length > 50 || source.numbers.some((number) => !Number.isSafeInteger(number) || Number(number) < 1)) throw new Error("Invalid deferred alert references");
110
+ run.source.numbers = [...new Set(source.numbers)];
111
+ }
112
+ } else if (source.tool !== void 0 || source.numbers !== void 0) throw new Error("Alert fields require an alert source");
113
+ }
114
+ if (raw.externalSourceRef !== void 0) {
115
+ const ref = keys(raw.externalSourceRef, ["integration", "kind", "key"], "external source reference");
116
+ if (!["issue", "pr"].includes(ref.kind) || run.source) throw new Error("Choose one deferred source reference");
117
+ run.externalSourceRef = { integration: text(ref.integration, 100, "integration"), kind: ref.kind, key: text(ref.key, 240, "source key") };
118
+ }
119
+ if (Buffer.byteLength(JSON.stringify(run)) > DEFERRED_RUN_LIMITS.requestBytes) throw new Error("Deferred run configuration is too large");
120
+ return run;
121
+ }
122
+ function deferredProviderReadiness(providers, quota, installed, now = Date.now()) {
123
+ const blocked = providers.flatMap((provider) => {
124
+ const reason = installed && !installed.has(provider) ? "is not installed" : exhausted(provider, quota, now);
125
+ if (!reason) return [];
126
+ const q = quota[provider];
127
+ const resets = Object.values(q?.windows ?? {}).flatMap((window) => window.resetsAt && window.resetsAt > now ? [window.resetsAt] : []);
128
+ return [{ provider, reason, retryAt: resets.length ? Math.max(...resets) : now + 6e4 }];
129
+ });
130
+ return blocked.length ? { ready: false, reason: blocked.map((item) => `${item.provider} ${item.reason}`).join("; "), retryAt: Math.max(...blocked.map((item) => item.retryAt)) } : { ready: true };
131
+ }
132
+ class DeferredRuns {
133
+ constructor(options) {
134
+ this.options = options;
135
+ this.file = resolve(options.file ?? join(CODEFLOW_HOME, "deferred-runs.json"));
136
+ this.now = options.now ?? Date.now;
137
+ }
138
+ file;
139
+ timer;
140
+ ticking;
141
+ stopped = false;
142
+ loaded = false;
143
+ controllers = /* @__PURE__ */ new Map();
144
+ listeners = /* @__PURE__ */ new Set();
145
+ now;
146
+ async load() {
147
+ if (this.loaded) return;
148
+ await this.change((document) => {
149
+ for (const job of document.jobs) if (job.status === "claimed") this.transition(job, "unknown", "The application stopped after claiming this launch. Inspect existing runs before explicitly recovering it.");
150
+ });
151
+ this.loaded = true;
152
+ }
153
+ async list() {
154
+ await queues.get(this.file);
155
+ return clone((await readDocument(this.file)).jobs).sort((a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id));
156
+ }
157
+ subscribe(listener) {
158
+ this.listeners.add(listener);
159
+ return () => this.listeners.delete(listener);
160
+ }
161
+ async enqueue(raw) {
162
+ const run = sanitizeDeferredRequest(raw.run), now = this.now();
163
+ const notBefore = raw.notBefore ?? now;
164
+ if (!positiveDate(notBefore) || notBefore > now + DEFERRED_RUN_LIMITS.futureMs) throw new Error("Choose a deferred start within the next year");
165
+ if (raw.waitForProviders !== void 0 && (!Array.isArray(raw.waitForProviders) || raw.waitForProviders.length > AGENTS.length || raw.waitForProviders.some((provider) => !AGENTS.includes(provider)))) throw new Error("Choose valid providers to wait for");
166
+ const providers = [...new Set(raw.waitForProviders ?? [])].sort();
167
+ const maxAttempts = raw.maxAttempts ?? 3;
168
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > DEFERRED_RUN_LIMITS.attempts) throw new Error("Deferred jobs permit 1\u20133 explicit launch attempts");
169
+ const name = raw.name === void 0 || raw.name === "" ? run.task.split("\n")[0].slice(0, 120) : text(raw.name, 120, "name");
170
+ const operationId = raw.operationId === void 0 ? void 0 : text(raw.operationId, 240, "operation ID");
171
+ const operationHash = createHash("sha256").update(canonical({ run, name, notBefore: raw.notBefore, providers, maxAttempts })).digest("hex");
172
+ return this.change((document) => {
173
+ const previous = operationId ? document.jobs.find((job2) => job2.operationId === operationId) : void 0;
174
+ if (previous) {
175
+ if (previous.operationHash !== operationHash) throw new Error("Deferred operation ID already names different content");
176
+ return clone(previous);
177
+ }
178
+ if (document.jobs.filter((job2) => PENDING.has(job2.status) || job2.status === "claimed" || job2.status === "unknown").length >= DEFERRED_RUN_LIMITS.active) throw new Error("Deferred queue is full; cancel or resolve older jobs first");
179
+ const job = { id: `d-${randomUUID()}`, revision: 1, name, run, notBefore, waitForProviders: providers, createdAt: now, updatedAt: now, status: notBefore > now ? "waiting-time" : "queued", attempts: 0, maxAttempts, history: [], ...operationId ? { operationId, operationHash } : {} };
180
+ this.history(job, "Explicitly queued by you");
181
+ document.jobs.push(job);
182
+ trim(document);
183
+ return clone(job);
184
+ });
185
+ }
186
+ async cancel(id, expectedRevision) {
187
+ const job = await this.change((document) => {
188
+ const job2 = current(document, id, expectedRevision);
189
+ if (job2.status === "started") throw new Error("This job already started. Stop its run from the run view.");
190
+ if (job2.status === "cancelled") return clone(job2);
191
+ if (job2.status === "claimed") {
192
+ job2.cancelRequested = true;
193
+ this.transition(job2, "claimed", "Cancellation requested while launching; the outcome must still be confirmed.");
194
+ } else this.transition(job2, "cancelled", job2.status === "unknown" ? "Queue recovery cancelled. An unknown launched run may still need to be stopped separately." : "Cancelled before launch");
195
+ return clone(job2);
196
+ });
197
+ this.controllers.get(id)?.abort();
198
+ return job;
199
+ }
200
+ async retry(id, expectedRevision, options = {}) {
201
+ return this.change((document) => {
202
+ const job = current(document, id, expectedRevision);
203
+ if (job.status !== "failed" && job.status !== "unknown") throw new Error("Only failed or unknown launches can be explicitly retried");
204
+ if (job.status === "unknown" && options.acknowledgeUnknown !== true) throw new Error("Inspect existing runs and acknowledge that retrying an unknown launch can duplicate work");
205
+ if (job.attempts >= job.maxAttempts) throw new Error("This deferred job reached its launch attempt limit");
206
+ delete job.claimId;
207
+ delete job.claimedAt;
208
+ delete job.runId;
209
+ delete job.cancelRequested;
210
+ delete job.nextCheckAt;
211
+ this.transition(job, "queued", "Explicit retry requested; provider availability and configuration will be checked again");
212
+ return clone(job);
213
+ });
214
+ }
215
+ /** The HTTP boundary must verify the run's persisted deferred job/claim and repository. */
216
+ async attach(id, expectedRevision, runId) {
217
+ runId = text(runId, 240, "run ID");
218
+ const snapshot = (await this.list()).find((job) => job.id === id);
219
+ if (!snapshot) throw new Error("Deferred job not found");
220
+ if (snapshot.revision !== expectedRevision) throw new DeferredRunConflictError(snapshot);
221
+ if (snapshot.status !== "unknown" || !snapshot.claimId) throw new Error("Only an unknown claimed launch can be linked");
222
+ if (snapshot.cancelRequested) {
223
+ if (!this.options.cancelRun) throw new Error("The recovered run must be cancelled before resolving this requested cancellation");
224
+ await bounded(this.options.cancelRun(runId), 1e4, new AbortController(), "Recovered run cancellation could not be confirmed");
225
+ }
226
+ return this.change((document) => {
227
+ const job = current(document, id, expectedRevision);
228
+ if (job.status !== "unknown" || job.claimId !== snapshot.claimId) throw new Error("Only the reviewed unknown launch can be linked");
229
+ job.runId = runId;
230
+ this.transition(job, job.cancelRequested ? "cancelled" : "started", job.cancelRequested ? "Recovered the matching run and sent its requested cancellation" : "Recovered by linking the run belonging to this exact launch claim");
231
+ return clone(job);
232
+ });
233
+ }
234
+ start(intervalMs = 5e3) {
235
+ if (!this.loaded) throw new Error("Load deferred runs before starting the queue");
236
+ if (this.timer) return;
237
+ if (!Number.isInteger(intervalMs) || intervalMs < 100 || intervalMs > 6e4) throw new Error("Invalid deferred queue interval");
238
+ this.stopped = false;
239
+ this.timer = setInterval(() => {
240
+ void this.tick().catch(() => {
241
+ });
242
+ }, intervalMs);
243
+ this.timer.unref?.();
244
+ }
245
+ async stop() {
246
+ this.stopped = true;
247
+ if (this.timer) clearInterval(this.timer);
248
+ this.timer = void 0;
249
+ for (const controller of this.controllers.values()) controller.abort();
250
+ await this.ticking;
251
+ await queues.get(this.file);
252
+ }
253
+ /** At most one launch per tick. Claims are durable before the launch callback can run. */
254
+ tick(force = false) {
255
+ if (!this.loaded) return Promise.reject(new Error("Load deferred runs before checking the queue"));
256
+ if (this.stopped) return Promise.resolve();
257
+ if (this.ticking) return this.ticking;
258
+ const work = this.tickOnce(force).finally(() => {
259
+ if (this.ticking === work) this.ticking = void 0;
260
+ });
261
+ this.ticking = work;
262
+ return work;
263
+ }
264
+ async tickOnce(force) {
265
+ const jobs = (await this.list()).reverse();
266
+ for (const snapshot of jobs) {
267
+ if (!PENDING.has(snapshot.status)) continue;
268
+ const now = this.now();
269
+ if (snapshot.notBefore > now) continue;
270
+ if (!force && (snapshot.nextCheckAt ?? 0) > now) continue;
271
+ const controller = new AbortController();
272
+ this.controllers.set(snapshot.id, controller);
273
+ try {
274
+ let ready;
275
+ try {
276
+ ready = await bounded(this.options.ready(clone(snapshot.run), [...snapshot.waitForProviders], controller.signal), this.options.readinessTimeoutMs ?? 1e4, controller, "Provider availability check timed out");
277
+ } catch (error) {
278
+ ready = { ready: false, reason: diagnostic(error.message), retryAt: this.now() + 6e4 };
279
+ }
280
+ if (typeof ready?.ready !== "boolean") ready = { ready: false, reason: "Provider readiness returned no valid answer", retryAt: this.now() + 6e4 };
281
+ if (!ready.ready) {
282
+ await this.change((document) => {
283
+ const job = document.jobs.find((job2) => job2.id === snapshot.id);
284
+ if (!job || !PENDING.has(job.status) || job.revision !== snapshot.revision) return;
285
+ const reason = diagnostic(ready.reason ?? "Waiting for provider capacity");
286
+ job.nextCheckAt = positiveDate(ready.retryAt) && ready.retryAt > this.now() ? ready.retryAt : this.now() + 6e4;
287
+ this.transition(job, "waiting-provider", reason);
288
+ });
289
+ continue;
290
+ }
291
+ if (controller.signal.aborted) continue;
292
+ const claimed = await this.change((document) => {
293
+ const job = document.jobs.find((job2) => job2.id === snapshot.id);
294
+ if (!job || !PENDING.has(job.status) || job.revision !== snapshot.revision || job.notBefore > this.now() || job.attempts >= job.maxAttempts) return void 0;
295
+ job.attempts++;
296
+ job.claimId = randomUUID();
297
+ job.claimedAt = this.now();
298
+ delete job.nextCheckAt;
299
+ this.transition(job, "claimed", "Launch claim saved before execution");
300
+ return clone(job);
301
+ });
302
+ if (!claimed) continue;
303
+ const claim = { jobId: claimed.id, claimId: claimed.claimId, attempt: claimed.attempts };
304
+ if (controller.signal.aborted || this.stopped) {
305
+ await this.change((document) => {
306
+ const job = document.jobs.find((job2) => job2.id === claimed.id);
307
+ if (job?.claimId === claim.claimId && job.status === "claimed") this.transition(job, job.cancelRequested ? "cancelled" : "failed", "Launch stopped before execution began");
308
+ });
309
+ return;
310
+ }
311
+ const launch = Promise.resolve().then(() => this.options.launch(clone(claimed.run), claim, controller.signal));
312
+ try {
313
+ const receipt = await bounded(launch, this.options.launchTimeoutMs ?? 3e4, controller, "Launch receipt timed out; the run may already exist");
314
+ const runId = text(receipt?.runId, 240, "launch receipt");
315
+ const cancel = await this.change((document) => {
316
+ const job = document.jobs.find((job2) => job2.id === claimed.id);
317
+ if (!job || job.claimId !== claim.claimId || job.status !== "claimed") return false;
318
+ job.runId = runId;
319
+ this.transition(job, job.cancelRequested ? "claimed" : "started", job.cancelRequested ? "Launch completed during cancellation; stopping its run" : "Run started; open it to follow execution");
320
+ return job.cancelRequested === true;
321
+ });
322
+ if (cancel) {
323
+ if (!this.options.cancelRun) throw new Error("Launch completed during cancellation; its run must be inspected and stopped manually");
324
+ await bounded(this.options.cancelRun(runId), 1e4, new AbortController(), "Run cancellation could not be confirmed");
325
+ await this.change((document) => {
326
+ const job = document.jobs.find((job2) => job2.id === claimed.id);
327
+ if (job?.claimId === claim.claimId && job.status === "claimed") this.transition(job, "cancelled", "The launched run was sent a cancellation request");
328
+ });
329
+ }
330
+ } catch (error) {
331
+ await this.change((document) => {
332
+ const job = document.jobs.find((job2) => job2.id === claimed.id);
333
+ if (!job || job.claimId !== claim.claimId || job.status !== "claimed") return;
334
+ this.transition(job, error instanceof DeferredRunRejectedError ? "failed" : "unknown", diagnostic(error.message));
335
+ });
336
+ void launch.then(async (receipt) => {
337
+ const runId = text(receipt?.runId, 240, "late launch receipt");
338
+ const cancel = await this.change((document) => {
339
+ const job = document.jobs.find((job2) => job2.id === claimed.id);
340
+ if (!job) return false;
341
+ if (job.claimId === claim.claimId && job.status === "unknown") {
342
+ job.runId = runId;
343
+ this.transition(job, "unknown", "A late run receipt arrived. Link this run after verifying its launch claim.");
344
+ } else {
345
+ job.history = [...job.history, { id: randomUUID(), at: this.now(), status: "unknown", detail: "A late receipt arrived for an earlier launch attempt", claimId: claim.claimId, runId }].slice(-DEFERRED_RUN_LIMITS.history);
346
+ job.revision++;
347
+ job.updatedAt = this.now();
348
+ }
349
+ return job.cancelRequested === true && job.claimId === claim.claimId;
350
+ });
351
+ if (cancel && this.options.cancelRun) await this.options.cancelRun(runId);
352
+ }).catch(() => void 0);
353
+ }
354
+ return;
355
+ } finally {
356
+ if (this.controllers.get(snapshot.id) === controller) this.controllers.delete(snapshot.id);
357
+ }
358
+ }
359
+ }
360
+ history(job, detail) {
361
+ job.history = [...job.history, { id: randomUUID(), at: this.now(), status: job.status, detail: diagnostic(detail), ...job.claimId ? { claimId: job.claimId } : {}, ...job.runId ? { runId: job.runId } : {} }].slice(-DEFERRED_RUN_LIMITS.history);
362
+ }
363
+ transition(job, status, reason) {
364
+ job.status = status;
365
+ job.reason = diagnostic(reason);
366
+ job.revision++;
367
+ job.updatedAt = this.now();
368
+ this.history(job, reason);
369
+ }
370
+ change(mutate) {
371
+ const work = (queues.get(this.file) ?? Promise.resolve()).then(async () => {
372
+ const document = await readDocument(this.file);
373
+ const before = JSON.stringify(document);
374
+ const result = mutate(document);
375
+ trim(document);
376
+ if (JSON.stringify(document) !== before) await writeDocument(this.file, document);
377
+ return result;
378
+ });
379
+ const tail = work.then(() => void 0, () => void 0);
380
+ queues.set(this.file, tail);
381
+ void tail.then(() => {
382
+ if (queues.get(this.file) === tail) queues.delete(this.file);
383
+ });
384
+ return work.then((result) => {
385
+ for (const listener of this.listeners) {
386
+ try {
387
+ listener();
388
+ } catch {
389
+ }
390
+ }
391
+ return result;
392
+ });
393
+ }
394
+ }
395
+ function current(document, id, revision) {
396
+ const job = document.jobs.find((job2) => job2.id === id);
397
+ if (!job) throw new Error("Deferred job not found");
398
+ if (!Number.isSafeInteger(revision) || revision !== job.revision) throw new DeferredRunConflictError(clone(job));
399
+ return job;
400
+ }
401
+ function trim(document) {
402
+ if (document.jobs.length <= DEFERRED_RUN_LIMITS.retained) return;
403
+ const removable = document.jobs.filter((job) => ["started", "failed", "cancelled"].includes(job.status)).sort((a, b) => a.updatedAt - b.updatedAt);
404
+ const remove = new Set(removable.slice(0, document.jobs.length - DEFERRED_RUN_LIMITS.retained).map((job) => job.id));
405
+ document.jobs = document.jobs.filter((job) => !remove.has(job.id));
406
+ }
407
+ async function bounded(promise, timeoutMs, controller, message) {
408
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) throw new Error("Deferred callback timeout must be 1\u201360,000 ms");
409
+ let timer;
410
+ let abort;
411
+ try {
412
+ return await Promise.race([promise, new Promise((_resolve, reject) => {
413
+ abort = () => reject(new Error("Deferred operation was cancelled; its outcome may require review"));
414
+ controller.signal.addEventListener("abort", abort, { once: true });
415
+ if (controller.signal.aborted) abort();
416
+ timer = setTimeout(() => {
417
+ reject(new Error(message));
418
+ controller.abort();
419
+ }, timeoutMs);
420
+ })]);
421
+ } finally {
422
+ if (timer) clearTimeout(timer);
423
+ if (abort) controller.signal.removeEventListener("abort", abort);
424
+ }
425
+ }
426
+ async function readDocument(file) {
427
+ let body;
428
+ try {
429
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
430
+ try {
431
+ const info = await handle.stat();
432
+ if (!info.isFile() || info.size > DEFERRED_RUN_LIMITS.fileBytes) throw new Error("queue must be a bounded regular file");
433
+ body = await handle.readFile("utf8");
434
+ } finally {
435
+ await handle.close();
436
+ }
437
+ } catch (error) {
438
+ if (error.code === "ENOENT") return { version: 1, jobs: [] };
439
+ throw new Error(`Deferred queue cannot be read: ${error.message}`);
440
+ }
441
+ try {
442
+ return validateDeferredDocument(JSON.parse(body));
443
+ } catch (error) {
444
+ throw new Error(`Deferred queue cannot be read: ${error.message}`);
445
+ }
446
+ }
447
+ function validateDeferredDocument(value) {
448
+ const document = clone(value);
449
+ if (document?.version !== 1 || !Array.isArray(document.jobs) || document.jobs.length > DEFERRED_RUN_LIMITS.retained) throw new Error("invalid document");
450
+ const ids = /* @__PURE__ */ new Set();
451
+ for (const job of document.jobs) {
452
+ if (!object(job) || typeof job.id !== "string" || !/^d-[a-f0-9-]{36}$/.test(job.id) || ids.has(job.id) || !Number.isSafeInteger(job.revision) || job.revision < 1 || !STATUSES.includes(job.status) || !positiveDate(job.createdAt) || !positiveDate(job.updatedAt) || !positiveDate(job.notBefore) || !Number.isInteger(job.attempts) || !Number.isInteger(job.maxAttempts) || job.attempts < 0 || job.attempts > job.maxAttempts || job.maxAttempts < 1 || job.maxAttempts > 3 || !Array.isArray(job.waitForProviders) || job.waitForProviders.length > 3 || job.waitForProviders.some((provider) => !AGENTS.includes(provider)) || !Array.isArray(job.history) || job.history.length > DEFERRED_RUN_LIMITS.history) throw new Error("invalid job");
453
+ ids.add(job.id);
454
+ text(job.name, 120, "saved job name");
455
+ if (job.nextCheckAt !== void 0 && !positiveDate(job.nextCheckAt) || job.claimedAt !== void 0 && !positiveDate(job.claimedAt) || job.cancelRequested !== void 0 && typeof job.cancelRequested !== "boolean" || job.reason !== void 0 && (typeof job.reason !== "string" || job.reason.length > 1e3 || job.reason.includes("\0")) || job.operationId !== void 0 && (typeof job.operationId !== "string" || !job.operationId.trim() || job.operationId.length > 240) || job.operationHash !== void 0 && !/^[a-f0-9]{64}$/.test(job.operationHash)) throw new Error("invalid job metadata");
456
+ if (job.run?.trust !== "safe" || job.run.allowUntrusted !== false || job.run.approvals?.plan !== false || job.run.approvals?.tools !== false || !Array.isArray(job.run.loop?.copy) || job.run.loop.copy.length) throw new Error("unsafe saved run policy");
457
+ job.run = sanitizeDeferredRequest(job.run);
458
+ if (job.claimId !== void 0 && (typeof job.claimId !== "string" || !/^[a-f0-9-]{36}$/.test(job.claimId)) || ["claimed", "unknown", "started"].includes(job.status) && (!job.claimId || !job.attempts) || job.runId !== void 0 && (typeof job.runId !== "string" || job.runId.length > 240)) throw new Error("invalid claim");
459
+ if (job.claimId && job.claimedAt === void 0 || job.status === "started" && !job.runId) throw new Error("incomplete claim receipt");
460
+ for (const event of job.history) if (!object(event) || typeof event.id !== "string" || !positiveDate(event.at) || !STATUSES.includes(event.status) || typeof event.detail !== "string" || event.detail.length > 1e3) throw new Error("invalid history");
461
+ }
462
+ return document;
463
+ }
464
+ function disableRestoredDeferredRuns(value, now = Date.now()) {
465
+ if (!positiveDate(now)) throw new Error("Invalid deferred restore time");
466
+ const document = validateDeferredDocument(value);
467
+ let cancelled = 0;
468
+ for (const job of document.jobs) {
469
+ if (job.status === "started" || job.status === "cancelled") continue;
470
+ cancelled++;
471
+ job.status = "cancelled";
472
+ job.cancelRequested = true;
473
+ job.revision++;
474
+ job.updatedAt = now;
475
+ delete job.nextCheckAt;
476
+ job.reason = "Cancelled during backup restore. Review the configuration and explicitly enqueue a new job to execute it.";
477
+ job.history = [...job.history, { id: randomUUID(), at: now, status: "cancelled", detail: job.reason, ...job.claimId ? { claimId: job.claimId } : {}, ...job.runId ? { runId: job.runId } : {} }].slice(-DEFERRED_RUN_LIMITS.history);
478
+ }
479
+ return { document, cancelled };
480
+ }
481
+ async function writeDocument(file, document) {
482
+ const body = JSON.stringify(document);
483
+ if (Buffer.byteLength(body) > DEFERRED_RUN_LIMITS.fileBytes) throw new Error("Deferred queue storage limit reached");
484
+ await mkdir(dirname(file), { recursive: true });
485
+ const temporary = `${file}.${randomUUID()}.tmp`;
486
+ try {
487
+ const handle = await open(temporary, "wx", 384);
488
+ try {
489
+ await handle.writeFile(body);
490
+ await handle.sync();
491
+ } finally {
492
+ await handle.close();
493
+ }
494
+ await rename(temporary, file);
495
+ const folder = await open(dirname(file), "r");
496
+ try {
497
+ await folder.sync();
498
+ } finally {
499
+ await folder.close();
500
+ }
501
+ } finally {
502
+ await rm(temporary, { force: true });
503
+ }
504
+ }
505
+ import { validateWorkerSelection } from "./remote-workers/protocol.js";
506
+ export {
507
+ DEFERRED_RUN_LIMITS,
508
+ DeferredRunConflictError,
509
+ DeferredRunRejectedError,
510
+ DeferredRuns,
511
+ deferredProviderReadiness,
512
+ disableRestoredDeferredRuns,
513
+ sanitizeDeferredRequest,
514
+ validateDeferredDocument
515
+ };
@@ -0,0 +1,35 @@
1
+ import { execFile } from "node:child_process";
2
+ import { doctor, checkGit } from "./doctor.js";
3
+ import { childEnv } from "./env.js";
4
+ import { ptyAvailable, shellProgram } from "./shell.js";
5
+ import { findChrome } from "./snapshot.js";
6
+ const versionLine = (text) => text.replace(/\x1b\[[0-9;]*m/g, "").split(/\r?\n/).find((line) => /\d+\.\d+/.test(line))?.slice(0, 160);
7
+ function capture(command, args) {
8
+ return new Promise((resolve) => {
9
+ execFile(command, args, { timeout: 5e3, maxBuffer: 64 * 1024, env: childEnv() }, (error, stdout, stderr) => resolve({ ok: !error, missing: error?.code === "ENOENT", stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
10
+ });
11
+ }
12
+ async function optionalDependencyHealth(options = {}) {
13
+ const browser = options.chrome === void 0 ? findChrome() : options.chrome;
14
+ const [terminal, python, chrome, docker] = await Promise.all([
15
+ ptyAvailable(options.python, { refresh: true }),
16
+ capture(options.python ?? process.env.CODEFLOW_PYTHON ?? "python3", ["--version"]),
17
+ browser ? capture(browser, ["--version"]) : void 0,
18
+ capture(options.docker ?? "docker", ["--version"])
19
+ ]);
20
+ const runtime = docker.ok ? await capture(options.docker ?? "docker", ["info", "--format", "{{.ServerVersion}}"]) : void 0;
21
+ const daemonReady = !!runtime?.ok && /^\d+\.\d+/.test(runtime.stdout.trim());
22
+ return [
23
+ { id: "terminal", name: "Interactive terminal", installed: !python.missing, ready: terminal, path: shellProgram(), version: versionLine(python.stdout || python.stderr), detail: terminal ? "Python 3 can load the pseudo-terminal modules; interactive shells are available." : "Interactive pseudo-terminals are unavailable. The terminal can still use its line-based fallback.", ...!terminal ? { fix: "Install Python 3 with pty, termios and fcntl support, or choose its executable with CODEFLOW_PYTHON. Windows does not support this terminal helper." } : {} },
24
+ { id: "snapshots", name: "Browser snapshots", installed: !!browser, ready: !!chrome?.ok, ...browser ? { path: browser } : {}, ...chrome ? { version: versionLine(chrome.stdout || chrome.stderr) } : {}, detail: !browser ? "No supported local browser was found for snapshots." : chrome?.ok ? "The configured Chrome-compatible browser responds to its version probe." : "The browser file exists but its version probe failed or timed out.", ...!browser || !chrome?.ok ? { fix: "Install Chrome, Chromium, Edge or Brave, or set CODEFLOW_CHROME to its executable. The embedded preview remains available without browser snapshots." } : {} },
25
+ { id: "docker", name: "Optional Docker isolation", installed: !docker.missing, ready: !!docker.ok && daemonReady, ...docker.ok ? { version: versionLine(docker.stdout || docker.stderr) } : {}, detail: docker.missing ? "Docker was not found. Host CLI execution remains available." : !docker.ok ? "Docker's version probe failed or timed out." : daemonReady ? `The Docker daemon responds (${runtime.stdout.trim().slice(0, 80)}). Images and authenticated volumes must still be prepared deliberately.` : "Docker is installed, but its daemon is unavailable or access is denied.", ...!docker.ok || !daemonReady ? { fix: "Install or start Docker and check access to its daemon before choosing isolated agents. This check does not pull images or change Docker configuration." } : {} }
26
+ ];
27
+ }
28
+ async function discoverDependencies(quota = {}) {
29
+ const [providers, git, optional] = await Promise.all([doctor(quota), checkGit(), optionalDependencyHealth()]);
30
+ return { checkedAt: Date.now(), node: { version: process.version, ok: Number(process.versions.node.split(".")[0]) > 22 || Number(process.versions.node.split(".")[0]) === 22 && Number(process.versions.node.split(".")[1]) >= 13 }, git, providers, optional };
31
+ }
32
+ export {
33
+ discoverDependencies,
34
+ optionalDependencyHealth
35
+ };
package/src/diff.js ADDED
@@ -0,0 +1,43 @@
1
+ const LOW_PRIORITY = /(^|\/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock|Pipfile\.lock)$|\.(min\.(js|css)|map|snap)$|(^|\/)(dist|build|vendor|node_modules|__snapshots__)\//;
2
+ function fileName(chunk) {
3
+ return chunk.match(/^diff --git a\/(.+?) b\//)?.[1] ?? chunk.match(/^diff --git (\S+)/)?.[1] ?? "?";
4
+ }
5
+ function stat(chunk) {
6
+ let add = 0;
7
+ let del = 0;
8
+ for (const line of chunk.split("\n")) {
9
+ if (line.startsWith("+") && !line.startsWith("+++")) add++;
10
+ else if (line.startsWith("-") && !line.startsWith("---")) del++;
11
+ }
12
+ return `+${add} -${del}`;
13
+ }
14
+ function fitDiff(full, max, base) {
15
+ if (full.length <= max) return full;
16
+ const chunks = full.split(/^(?=diff --git )/m).filter(Boolean);
17
+ const budget = max - Math.min(Math.floor(max / 4), 300 + chunks.length * 60);
18
+ const shown = /* @__PURE__ */ new Map();
19
+ let used = 0;
20
+ const order = chunks.map((c, i) => ({ c, i, low: LOW_PRIORITY.test(fileName(c)) })).sort((a, b) => Number(a.low) - Number(b.low));
21
+ for (const { c, i } of order) {
22
+ if (used + c.length > budget) continue;
23
+ shown.set(i, c);
24
+ used += c.length;
25
+ }
26
+ const big = order.find(({ i, low }) => !shown.has(i) && !low);
27
+ if (big && budget - used > 2e3) {
28
+ const room = budget - used;
29
+ const cut = big.c.lastIndexOf("\n", room);
30
+ shown.set(big.i, `${big.c.slice(0, cut > 0 ? cut : room)}
31
+ \u2026 (the rest of ${fileName(big.c)} is cut: ${stat(big.c)} in total)
32
+ `);
33
+ }
34
+ const omitted = order.filter(({ i }) => !shown.has(i)).map(({ c }) => `- ${fileName(c)} (${stat(c)})`);
35
+ const body = chunks.map((_, i) => shown.get(i) ?? "").join("");
36
+ if (!omitted.length) return body;
37
+ return body + `
38
+ \u2026 the diff is ${full.length} characters, too long to show whole. ${omitted.length} file(s) aren't shown above; run \`git diff ${base}..HEAD -- <file>\` to read them:
39
+ ${omitted.join("\n")}`;
40
+ }
41
+ export {
42
+ fitDiff
43
+ };