@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -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/changes-62ZIEQSN.js +1 -0
  17. package/public/assets/chunk-2PZVOKID.js +1 -0
  18. package/public/assets/chunk-4D5IENEB.js +1 -0
  19. package/public/assets/chunk-5SARP5MU.js +1 -0
  20. package/public/assets/chunk-63YLZ3PR.js +1 -0
  21. package/public/assets/chunk-7FUHKF2M.js +1 -0
  22. package/public/assets/chunk-APQEVJ2K.js +1 -0
  23. package/public/assets/chunk-CGOZQMEZ.js +2 -0
  24. package/public/assets/chunk-J4VYS3KV.js +2 -0
  25. package/public/assets/chunk-L7TUCIYF.js +1 -0
  26. package/public/assets/chunk-SIHDWM2Y.js +154 -0
  27. package/public/assets/chunk-TEJVYIKC.js +1 -0
  28. package/public/assets/code-browser-VB2HGZSG.js +4 -0
  29. package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
  30. package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
  31. package/public/assets/join-project-6RTFHNCA.js +1 -0
  32. package/public/assets/main-65EW73SB.js +1 -0
  33. package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
  34. package/public/assets/settings-UB5Q4AQM.js +9 -0
  35. package/public/assets/styles-18792BB4.css +32 -0
  36. package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
  37. package/public/assets/terminal-IWAIM4DE.js +62 -0
  38. package/public/assets.json +1 -0
  39. package/public/bundle-meta.json +1 -0
  40. package/public/docs/.nojekyll +0 -0
  41. package/public/docs/accessibility.html +19 -0
  42. package/public/docs/acp-agents.html +117 -0
  43. package/public/docs/agent-containers.html +79 -0
  44. package/public/docs/api.html +1130 -0
  45. package/public/docs/app.js +342 -0
  46. package/public/docs/application-improvements.html +275 -0
  47. package/public/docs/architecture-explorer.html +19 -0
  48. package/public/docs/architecture.html +149 -0
  49. package/public/docs/auto-team.html +148 -0
  50. package/public/docs/automations.html +109 -0
  51. package/public/docs/checkpoint-review.html +37 -0
  52. package/public/docs/code-hosts.html +35 -0
  53. package/public/docs/code-intelligence.html +35 -0
  54. package/public/docs/configuration.html +346 -0
  55. package/public/docs/context-inspection.html +23 -0
  56. package/public/docs/coordinated-changes.html +28 -0
  57. package/public/docs/deferred-runs.html +25 -0
  58. package/public/docs/dependency-discovery.html +19 -0
  59. package/public/docs/documentation.html +84 -0
  60. package/public/docs/dynamic-teams-evaluation.html +174 -0
  61. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  62. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  63. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  64. package/public/docs/extensions.html +42 -0
  65. package/public/docs/external-sources.html +96 -0
  66. package/public/docs/filesystem-policy.html +26 -0
  67. package/public/docs/filtered-tasks.html +44 -0
  68. package/public/docs/font-license.txt +92 -0
  69. package/public/docs/getting-started.html +143 -0
  70. package/public/docs/github-review-threads.html +26 -0
  71. package/public/docs/github.html +157 -0
  72. package/public/docs/help.html +69 -0
  73. package/public/docs/index.html +88 -0
  74. package/public/docs/installation.html +80 -0
  75. package/public/docs/launch-profiles.html +48 -0
  76. package/public/docs/maintenance.html +60 -0
  77. package/public/docs/memory-claims.html +31 -0
  78. package/public/docs/memory-storage.html +58 -0
  79. package/public/docs/notifications.html +39 -0
  80. package/public/docs/patterns.html +195 -0
  81. package/public/docs/performance-results.html +210 -0
  82. package/public/docs/performance.html +166 -0
  83. package/public/docs/project-conventions.html +22 -0
  84. package/public/docs/project-first-navigation-plan.html +587 -0
  85. package/public/docs/project-teams-implementation-plan.html +772 -0
  86. package/public/docs/project-teams.html +129 -0
  87. package/public/docs/provider-compatibility.html +20 -0
  88. package/public/docs/releasing.html +65 -0
  89. package/public/docs/remote-workers.html +64 -0
  90. package/public/docs/review-inbox.html +26 -0
  91. package/public/docs/run-storage.html +23 -0
  92. package/public/docs/runs.html +141 -0
  93. package/public/docs/search.json +1 -0
  94. package/public/docs/security.html +133 -0
  95. package/public/docs/shared-projects.html +31 -0
  96. package/public/docs/ssh-projects.html +60 -0
  97. package/public/docs/style.css +768 -0
  98. package/public/docs/task-reruns.html +30 -0
  99. package/public/docs/team-collaboration.html +412 -0
  100. package/public/docs/verification.html +22 -0
  101. package/public/docs/vscode.html +53 -0
  102. package/public/docs/worker-permission-verification.html +131 -0
  103. package/public/index.html +17 -0
  104. package/scripts/check-node.mjs +7 -0
  105. package/scripts/codeflow.mjs +55 -0
  106. package/scripts/computer-use.swift +64 -0
  107. package/scripts/pty.py +273 -0
  108. package/src/acp-connections.js +110 -0
  109. package/src/adapters/acp-presets.js +37 -0
  110. package/src/adapters/acp-types.js +0 -0
  111. package/src/adapters/acp.js +328 -0
  112. package/src/adapters/agent-tools.js +28 -0
  113. package/src/adapters/agy-auth.js +67 -0
  114. package/src/adapters/agy-isolation.js +242 -0
  115. package/src/adapters/agy-tools.js +45 -0
  116. package/src/adapters/agy.js +326 -0
  117. package/src/adapters/claude-stream.js +58 -0
  118. package/src/adapters/claude.js +282 -0
  119. package/src/adapters/codex-app-server.js +555 -0
  120. package/src/adapters/codex.js +311 -0
  121. package/src/adapters/errors.js +57 -0
  122. package/src/adapters/index.js +91 -0
  123. package/src/adapters/managed-permissions.js +37 -0
  124. package/src/adapters/model-discovery.js +100 -0
  125. package/src/adapters/process.js +433 -0
  126. package/src/adapters/team-tools.js +103 -0
  127. package/src/adapters/types.js +0 -0
  128. package/src/agent-browser.js +688 -0
  129. package/src/agent-computer.js +88 -0
  130. package/src/agent-container.js +155 -0
  131. package/src/agent-team-definitions.js +131 -0
  132. package/src/agent-tool-client.mjs +79 -0
  133. package/src/agent-tools.js +296 -0
  134. package/src/approvals.js +134 -0
  135. package/src/architecture.js +456 -0
  136. package/src/assets.js +44 -0
  137. package/src/automation.js +901 -0
  138. package/src/browser-files.js +293 -0
  139. package/src/browser-ownership.js +150 -0
  140. package/src/certificate-renewal.js +75 -0
  141. package/src/chat-family.js +25 -0
  142. package/src/chat-handoff.js +87 -0
  143. package/src/checkpoint-review.js +72 -0
  144. package/src/checks.js +303 -0
  145. package/src/code-host-git.js +102 -0
  146. package/src/code-hosts-api.js +99 -0
  147. package/src/code-hosts.js +482 -0
  148. package/src/code-intelligence-worker.mjs +452 -0
  149. package/src/code-intelligence.js +141 -0
  150. package/src/collab/api.js +365 -0
  151. package/src/collab/checkpoints.js +198 -0
  152. package/src/collab/connections.js +139 -0
  153. package/src/collab/host-execution.js +347 -0
  154. package/src/collab/hub-client.js +210 -0
  155. package/src/collab/link-api.js +229 -0
  156. package/src/collab/link.js +595 -0
  157. package/src/collab/model.js +496 -0
  158. package/src/collab/runner.js +1014 -0
  159. package/src/collab/store.js +1452 -0
  160. package/src/collab/terminals.js +361 -0
  161. package/src/collaboration-reviews.js +116 -0
  162. package/src/config.js +48 -0
  163. package/src/context-budget.js +14 -0
  164. package/src/context-inspection.js +204 -0
  165. package/src/context.js +154 -0
  166. package/src/conventions.js +283 -0
  167. package/src/coordinated-api.js +247 -0
  168. package/src/deferred-launch.js +82 -0
  169. package/src/deferred-runs.js +515 -0
  170. package/src/dependency-health.js +35 -0
  171. package/src/diff.js +43 -0
  172. package/src/docs-help.js +316 -0
  173. package/src/doctor.js +232 -0
  174. package/src/documentation-changes.js +20 -0
  175. package/src/engine.js +5258 -0
  176. package/src/env.js +44 -0
  177. package/src/execution-budget.js +8 -0
  178. package/src/experience.js +50 -0
  179. package/src/extensions.js +144 -0
  180. package/src/filesystem-access.js +107 -0
  181. package/src/filesystem-policy.js +238 -0
  182. package/src/filtered-task-limits.js +93 -0
  183. package/src/filtered-tasks-api.js +53 -0
  184. package/src/filtered-tasks.js +576 -0
  185. package/src/frame-policy.js +66 -0
  186. package/src/fs.js +57 -0
  187. package/src/git.js +626 -0
  188. package/src/github-review-threads.js +181 -0
  189. package/src/github.js +538 -0
  190. package/src/http.js +69 -0
  191. package/src/improve-settings.js +38 -0
  192. package/src/improve.js +187 -0
  193. package/src/integrations.js +373 -0
  194. package/src/integrity.js +259 -0
  195. package/src/journey-api.js +77 -0
  196. package/src/journey-replay.js +65 -0
  197. package/src/journey-store.js +97 -0
  198. package/src/journeys.js +60 -0
  199. package/src/json.js +81 -0
  200. package/src/launch-profiles.js +292 -0
  201. package/src/learnings.js +80 -0
  202. package/src/local-app-url.js +19 -0
  203. package/src/log.js +39 -0
  204. package/src/maintenance.js +335 -0
  205. package/src/memory-analysis.js +182 -0
  206. package/src/memory-claims.js +153 -0
  207. package/src/memory-sqlite.js +236 -0
  208. package/src/memory.js +552 -0
  209. package/src/merge-validation.js +58 -0
  210. package/src/model-catalog.js +239 -0
  211. package/src/model-research.js +91 -0
  212. package/src/notes.js +176 -0
  213. package/src/notifications.js +413 -0
  214. package/src/patterns/auto-team-model.js +230 -0
  215. package/src/patterns/auto-team.js +816 -0
  216. package/src/patterns/council.js +199 -0
  217. package/src/patterns/debate.js +188 -0
  218. package/src/patterns/findings.js +141 -0
  219. package/src/patterns/index.js +21 -0
  220. package/src/patterns/loop.js +158 -0
  221. package/src/patterns/pipeline.js +125 -0
  222. package/src/patterns/prompts.js +181 -0
  223. package/src/patterns/race.js +147 -0
  224. package/src/patterns/schemas.js +40 -0
  225. package/src/patterns/solo.js +46 -0
  226. package/src/patterns/supervisor.js +235 -0
  227. package/src/patterns/task-draft.js +29 -0
  228. package/src/patterns/task-review.js +45 -0
  229. package/src/patterns/tdd.js +231 -0
  230. package/src/patterns/team-strategies.js +67 -0
  231. package/src/performance.js +43 -0
  232. package/src/persistence-status.js +35 -0
  233. package/src/phone-sessions.js +96 -0
  234. package/src/preview-overlay.js +392 -0
  235. package/src/preview.js +230 -0
  236. package/src/privacy.js +23 -0
  237. package/src/project-report.js +22 -0
  238. package/src/project-team-budget.js +22 -0
  239. package/src/project-team-model.js +225 -0
  240. package/src/project-team-policy.js +42 -0
  241. package/src/project-team-store.js +234 -0
  242. package/src/project-team-usage.js +24 -0
  243. package/src/project-teams-api.js +43 -0
  244. package/src/project-teams.js +644 -0
  245. package/src/projects.js +290 -0
  246. package/src/provider-eligibility.js +9 -0
  247. package/src/readiness.js +98 -0
  248. package/src/remote-tls.js +66 -0
  249. package/src/remote-workers/bundles.js +198 -0
  250. package/src/remote-workers/client.js +332 -0
  251. package/src/remote-workers/durability.js +37 -0
  252. package/src/remote-workers/protocol.js +68 -0
  253. package/src/remote-workers/stdio.js +138 -0
  254. package/src/remote-workers/worker.js +304 -0
  255. package/src/remote.js +204 -0
  256. package/src/repo-config.js +201 -0
  257. package/src/repo-coordination.js +725 -0
  258. package/src/repo-tree.js +106 -0
  259. package/src/repository-identity.js +13 -0
  260. package/src/retention-policy.js +466 -0
  261. package/src/review-inbox.js +38 -0
  262. package/src/routing.js +49 -0
  263. package/src/run-analysis.js +111 -0
  264. package/src/run-cold-store.js +231 -0
  265. package/src/run-environment.js +15 -0
  266. package/src/run-history.js +91 -0
  267. package/src/run-log.js +160 -0
  268. package/src/run-permissions.js +111 -0
  269. package/src/run-projection.js +77 -0
  270. package/src/run-recovery.js +53 -0
  271. package/src/run-status.js +56 -0
  272. package/src/serve.js +138 -0
  273. package/src/server.js +2871 -0
  274. package/src/shared-access.js +103 -0
  275. package/src/shared-accounts.js +381 -0
  276. package/src/shared-api.js +239 -0
  277. package/src/shell.js +346 -0
  278. package/src/ship.js +344 -0
  279. package/src/slots.js +74 -0
  280. package/src/snapshot.js +388 -0
  281. package/src/source-context.js +17 -0
  282. package/src/sse-writer.js +70 -0
  283. package/src/ssh-projects/bootstrap.mjs +95 -0
  284. package/src/ssh-projects/config.js +45 -0
  285. package/src/ssh-projects/gateway.js +143 -0
  286. package/src/ssh-projects/manager.js +320 -0
  287. package/src/ssh-projects/team-autostart.mjs +93 -0
  288. package/src/ssh-projects/team-remove.mjs +69 -0
  289. package/src/ssh-projects/team-status.mjs +39 -0
  290. package/src/ssh-projects/transport.js +133 -0
  291. package/src/stats.js +139 -0
  292. package/src/store.js +544 -0
  293. package/src/stream.js +91 -0
  294. package/src/sweeps.js +241 -0
  295. package/src/tailscale.js +101 -0
  296. package/src/task-draft.js +23 -0
  297. package/src/task-team.js +164 -0
  298. package/src/team-evaluation.js +243 -0
  299. package/src/team-mailbox.js +160 -0
  300. package/src/team-memory.js +84 -0
  301. package/src/team-rerun-graph.js +15 -0
  302. package/src/team-reruns.js +130 -0
  303. package/src/team-scheduler.js +121 -0
  304. package/src/team-steering.js +21 -0
  305. package/src/team-template-store.js +98 -0
  306. package/src/team-templates.js +117 -0
  307. package/src/team-tool-client.mjs +79 -0
  308. package/src/team-tools.js +235 -0
  309. package/src/teams.js +0 -0
  310. package/src/templates.js +62 -0
  311. package/src/transcript.js +134 -0
  312. package/src/verification-integrity.js +27 -0
  313. package/src/verification.js +183 -0
  314. package/src/watch.js +195 -0
  315. package/src/work-queue.js +47 -0
  316. package/src/workflow-control.js +83 -0
  317. package/src/workspace-api.js +207 -0
  318. package/src/workspace-changes.js +61 -0
  319. package/src/workspace.js +146 -0
@@ -0,0 +1,239 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { HttpError, readJson, send } from "./http.js";
3
+ import { roleAtLeast } from "./shared-accounts.js";
4
+ import { sharedCanReadRun, sharedCoverageProjection, sharedDiffProjection, sharedEventProjection, sharedProjectProjection, sharedProjectRole, sharedRoutePolicy, sharedRunProjection } from "./shared-access.js";
5
+ import { afterRunCursor, compareRuns, decodeRunCursor, encodeRunCursor } from "./run-history.js";
6
+ import { sensitiveContextPath } from "./context-inspection.js";
7
+ import { redactExport } from "./privacy.js";
8
+ const sharedStreams = /* @__PURE__ */ new Map();
9
+ let totalSharedStreams = 0;
10
+ const STREAM_BYTES = 2 * 1024 * 1024;
11
+ function sharedStream(res, userId) {
12
+ const count = sharedStreams.get(userId) ?? 0;
13
+ if (count >= 4 || totalSharedStreams >= 128) throw new HttpError(429, "Too many shared event streams; close another tab and retry");
14
+ sharedStreams.set(userId, count + 1);
15
+ totalSharedStreams++;
16
+ let closed = false, blocked = false;
17
+ const disposers = [];
18
+ const close = () => {
19
+ if (closed) return;
20
+ closed = true;
21
+ const remaining = (sharedStreams.get(userId) ?? 1) - 1;
22
+ if (remaining) sharedStreams.set(userId, remaining);
23
+ else sharedStreams.delete(userId);
24
+ totalSharedStreams--;
25
+ for (const dispose of disposers.splice(0)) dispose();
26
+ res.destroy();
27
+ };
28
+ const write = (frame) => {
29
+ if (closed) return;
30
+ if (blocked || Buffer.byteLength(frame) > STREAM_BYTES || res.writableLength + Buffer.byteLength(frame) > STREAM_BYTES) return close();
31
+ blocked = !res.write(frame);
32
+ };
33
+ res.on("drain", () => {
34
+ blocked = false;
35
+ });
36
+ res.on("close", close);
37
+ res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store", connection: "keep-alive", "x-accel-buffering": "no" });
38
+ return { close, add: (dispose) => disposers.push(dispose), emit: (value) => write(`data: ${JSON.stringify(value)}
39
+
40
+ `), ping: () => write(": connected\n\n") };
41
+ }
42
+ async function handleSharedApi(req, res, url, parts, host) {
43
+ const policy = sharedRoutePolicy(req.method ?? "GET", parts);
44
+ if (policy === "owner" || policy === "identity") throw new HttpError(403, "This operation requires the Code Flow owner");
45
+ const current = () => {
46
+ const identity = host.accounts.authenticate(host.token, { touch: false });
47
+ if (!identity || identity.userId !== host.identity.userId || identity.revision !== host.identity.revision) throw new HttpError(401, "Shared session expired or was revoked");
48
+ return identity;
49
+ };
50
+ const projectId = (run2) => run2.project ?? (() => {
51
+ const matches = host.projects.list().filter((project) => project.repos.includes(run2.repo));
52
+ return matches.length === 1 ? matches[0].id : void 0;
53
+ })();
54
+ const canRead = (run2) => (!host.canReadFilePolicy || host.canReadFilePolicy(run2)) && sharedCanReadRun(current(), run2, host.projects.list());
55
+ const canReadId = (id) => {
56
+ const run2 = host.manager.get(id);
57
+ return !!run2 && canRead(run2);
58
+ };
59
+ const projection = (run2) => sharedRunProjection({ ...run2, project: projectId(run2) }, canReadId);
60
+ const authorized = () => host.manager.list().filter(canRead).sort(compareRuns);
61
+ const requestedRun = () => {
62
+ const run2 = host.manager.get(parts[2] ?? "");
63
+ if (!run2 || !canRead(run2)) throw new HttpError(404, "Run not found");
64
+ return run2;
65
+ };
66
+ const assertBranch = (run2, branch) => {
67
+ if (typeof branch !== "string" || !run2.worktrees.some((tree) => tree.branch === branch)) throw new HttpError(404, "Result branch not found");
68
+ return branch;
69
+ };
70
+ const coverage = sharedCoverageProjection;
71
+ current();
72
+ if (policy === "catalog") return send(res, 200, redactExport(parts[1] === "patterns" ? host.patterns : host.templates));
73
+ if (policy === "project-list") return send(res, 200, host.projects.list().filter((project) => sharedProjectRole(current(), project.id)).map(sharedProjectProjection));
74
+ if (policy === "run-list" && parts[1] === "runs") {
75
+ const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : 50;
76
+ const query = url.searchParams.get("q") ?? "", selectedProject = url.searchParams.get("project");
77
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100 || query.length > 300) throw new HttpError(400, "Choose a history limit of 1\u2013100 and a bounded search");
78
+ if (selectedProject && !sharedProjectRole(current(), selectedProject)) throw new HttpError(404, "Project not found");
79
+ const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
80
+ const filtered = authorized().filter((run2) => {
81
+ if (selectedProject && projectId(run2) !== selectedProject) return false;
82
+ if (!words.length) return true;
83
+ const full = run2.cold ? host.manager.get(run2.id) : run2;
84
+ if (!full || !canRead(full) || selectedProject && projectId(full) !== selectedProject) return false;
85
+ const text = [full.id, full.task, full.pattern].join("\n").toLowerCase();
86
+ return words.every((word) => text.includes(word));
87
+ });
88
+ const cursor = url.searchParams.get("cursor");
89
+ let after;
90
+ try {
91
+ after = cursor ? decodeRunCursor(cursor) : void 0;
92
+ } catch {
93
+ throw new HttpError(400, "Invalid shared history cursor");
94
+ }
95
+ const remaining = after ? filtered.filter((run2) => afterRunCursor(run2, after)) : filtered;
96
+ const page = remaining.slice(0, limit);
97
+ return send(res, 200, { runs: page.map(projection), total: filtered.length, ...remaining.length > limit ? { nextCursor: encodeRunCursor(page.at(-1)) } : {} });
98
+ }
99
+ if (policy === "run-list") {
100
+ const { emit, close, add, ping } = sharedStream(res, host.identity.userId);
101
+ let visible = /* @__PURE__ */ new Set();
102
+ const notification = (run2) => ({ id: run2.id, project: projectId(run2), status: run2.status, createdAt: run2.createdAt, endedAt: run2.endedAt });
103
+ const reset = () => {
104
+ const list = authorized();
105
+ visible = new Set(list.map((run2) => run2.id));
106
+ emit({ type: "hello", epoch: randomUUID(), runs: list.slice(0, 100).map(notification), total: list.length });
107
+ };
108
+ add(host.manager.subscribeAll((run2) => {
109
+ if (run2.deleted) {
110
+ if (visible.delete(run2.id)) reset();
111
+ return;
112
+ }
113
+ try {
114
+ if (canRead(run2)) {
115
+ visible.add(run2.id);
116
+ emit({ type: "run", run: notification(run2) });
117
+ } else if (visible.delete(run2.id)) reset();
118
+ } catch {
119
+ close();
120
+ }
121
+ }));
122
+ if (host.onFilePolicyChange) add(host.onFilePolicyChange(() => close()));
123
+ add(host.accounts.onChange(() => {
124
+ try {
125
+ current();
126
+ } catch {
127
+ close();
128
+ }
129
+ }));
130
+ add(host.projects.onChange(() => {
131
+ try {
132
+ current();
133
+ reset();
134
+ } catch {
135
+ close();
136
+ }
137
+ }));
138
+ const timer = setInterval(() => {
139
+ try {
140
+ current();
141
+ ping();
142
+ } catch {
143
+ close();
144
+ }
145
+ }, 15e3);
146
+ timer.unref();
147
+ add(() => clearInterval(timer));
148
+ reset();
149
+ return;
150
+ }
151
+ const run = requestedRun();
152
+ if (policy === "run-review") {
153
+ const reviewer = (identity2) => roleAtLeast(sharedProjectRole(identity2, projectId(run)), "reviewer");
154
+ if (!reviewer(current())) throw new HttpError(403, "A project reviewer role is required");
155
+ const body = await readJson(req, 16 * 1024, { timeoutMs: 1e4 });
156
+ const branch = assertBranch(run, body.branch);
157
+ const identity = current();
158
+ if (!canRead(run) || !reviewer(identity)) throw new HttpError(404, "Run not found");
159
+ if (parts[3] === "review-coverage") {
160
+ if (typeof body.path !== "string" || sensitiveContextPath(body.path)) throw new HttpError(403, "This path requires owner review");
161
+ const result2 = await host.manager.recordFileReview(run.id, { branch, expectedHead: body.expectedHead, path: body.path, reviewed: body.reviewed }, `shared:${identity.userId}:${identity.username}`, () => {
162
+ if (!canRead(run) || !reviewer(current())) throw new HttpError(403, "Project reviewer access changed");
163
+ });
164
+ current();
165
+ if (!canRead(run)) throw new HttpError(404, "Run not found");
166
+ return send(res, 200, coverage(result2));
167
+ }
168
+ const result = await host.comments.add(run.id, { branch, expectedHead: body.expectedHead, text: body.text }, { id: `shared:${identity.userId}`, name: identity.displayName }, async () => {
169
+ current();
170
+ const latest = requestedRun();
171
+ assertBranch(latest, branch);
172
+ return (await host.manager.reviewCoverage(run.id, branch)).head;
173
+ }, () => {
174
+ if (!canRead(run) || !reviewer(current())) throw new HttpError(403, "Project reviewer access changed");
175
+ });
176
+ current();
177
+ if (!canRead(run)) throw new HttpError(404, "Run not found");
178
+ return send(res, 201, result);
179
+ }
180
+ if (parts.length === 3) return send(res, 200, projection(run));
181
+ if (parts[3] === "export") return send(res, 200, { version: 1, exportedAt: Date.now(), run: projection(run) });
182
+ if (parts[3] === "collaboration") {
183
+ const result = await host.comments.list(run.id);
184
+ current();
185
+ if (!canRead(run)) throw new HttpError(404, "Run not found");
186
+ return send(res, 200, result);
187
+ }
188
+ if (parts[3] === "diff" || parts[3] === "review-coverage") {
189
+ const branch = assertBranch(run, url.searchParams.get("branch"));
190
+ const result = parts[3] === "diff" ? sharedDiffProjection(await host.manager.diff(run.id, branch, false)) : coverage(await host.manager.reviewCoverage(run.id, branch));
191
+ current();
192
+ if (!canRead(run)) throw new HttpError(404, "Run not found");
193
+ return send(res, 200, result);
194
+ }
195
+ if (parts[3] === "events") {
196
+ const { emit, close, add, ping } = sharedStream(res, host.identity.userId);
197
+ add(host.manager.subscribe(run.id, (event) => {
198
+ try {
199
+ requestedRun();
200
+ const value = sharedEventProjection(event, canReadId);
201
+ if (value) emit(value);
202
+ } catch {
203
+ close();
204
+ }
205
+ }));
206
+ if (host.onFilePolicyChange) add(host.onFilePolicyChange(() => close()));
207
+ add(host.accounts.onChange(() => {
208
+ try {
209
+ requestedRun();
210
+ } catch {
211
+ close();
212
+ }
213
+ }));
214
+ add(host.projects.onChange(() => {
215
+ try {
216
+ requestedRun();
217
+ } catch {
218
+ close();
219
+ }
220
+ }));
221
+ const timer = setInterval(() => {
222
+ try {
223
+ requestedRun();
224
+ ping();
225
+ } catch {
226
+ close();
227
+ }
228
+ }, 15e3);
229
+ timer.unref();
230
+ add(() => clearInterval(timer));
231
+ const snapshot = projection(run);
232
+ emit({ type: "hello", snapshot: { run: { ...snapshot, steps: void 0 }, steps: snapshot.steps ?? [] } });
233
+ return;
234
+ }
235
+ throw new HttpError(403, "This operation requires the Code Flow owner");
236
+ }
237
+ export {
238
+ handleSharedApi
239
+ };
package/src/shell.js ADDED
@@ -0,0 +1,346 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { basename } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { killGroup, trackGroup, untrackGroup } from "./adapters/process.js";
7
+ import { childEnv } from "./env.js";
8
+ const MARK = "";
9
+ const MARKER = new RegExp(`${MARK}(\\d+)${MARK}([^${MARK}]*)${MARK}\\n?`);
10
+ const HISTORY_BYTES = 256 * 1024;
11
+ const MAX_SHELLS = 12;
12
+ const PTY_SCRIPT = fileURLToPath(new URL("../scripts/pty.py", import.meta.url));
13
+ const PTY_CLOSE_MS = 3e3;
14
+ const clamp = (n, lo, hi, fallback) => Number.isFinite(Number(n)) ? Math.min(hi, Math.max(lo, Math.round(Number(n)))) : fallback;
15
+ class ShellLimitError extends Error {
16
+ }
17
+ function shellProgram(env = process.env) {
18
+ const own = env.SHELL ?? "";
19
+ if (["bash", "zsh", "sh", "dash", "ksh"].includes(basename(own)) && existsSync(own)) return own;
20
+ return existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh";
21
+ }
22
+ function loginShell(env = process.env) {
23
+ const own = env.SHELL ?? "";
24
+ return own.startsWith("/") && existsSync(own) ? own : shellProgram(env);
25
+ }
26
+ const probes = /* @__PURE__ */ new Map();
27
+ function ptyPython(python = defaultPython()) {
28
+ if (process.platform === "win32") return Promise.resolve(void 0);
29
+ let probe = probes.get(python);
30
+ if (!probe) {
31
+ probe = new Promise((resolve) => {
32
+ const check = "import sys, pty, termios, fcntl, select\nif sys.version_info < (3, 6): sys.exit(1)\nprint(sys.executable)";
33
+ execFile(python, ["-I", "-c", check], { timeout: 1e4, env: childEnv() }, (err, stdout) => resolve(err ? void 0 : String(stdout).trim() || python));
34
+ });
35
+ probes.set(python, probe);
36
+ }
37
+ return probe;
38
+ }
39
+ function ptyAvailable(python = defaultPython(), options = {}) {
40
+ if (options.refresh) probes.delete(python);
41
+ return ptyPython(python).then(Boolean);
42
+ }
43
+ function defaultPython() {
44
+ return process.env.CODEFLOW_PYTHON || "python3";
45
+ }
46
+ class Shells {
47
+ shells = /* @__PURE__ */ new Map();
48
+ python;
49
+ constructor(opts = {}) {
50
+ this.python = opts.python ?? defaultPython();
51
+ }
52
+ /** Whether new terminals can be interactive (python3 is there to run the pseudo-terminal). */
53
+ interactive() {
54
+ return ptyAvailable(this.python);
55
+ }
56
+ list() {
57
+ return [...this.shells.values()].map(describe);
58
+ }
59
+ get(id) {
60
+ const s = this.shells.get(id);
61
+ return s && describe(s);
62
+ }
63
+ /** What a client needs to catch up: the kept output, in order. */
64
+ history(id) {
65
+ const s = this.shells.get(id);
66
+ if (!s) return [];
67
+ const events = [...s.history];
68
+ const first = events[0];
69
+ if (s.interactive && s.trimmed && first?.type === "output") {
70
+ const nl = first.text.indexOf("\n");
71
+ events[0] = { type: "output", text: nl < 0 ? "" : first.text.slice(nl + 1) };
72
+ }
73
+ return events;
74
+ }
75
+ /**
76
+ * A new shell in `cwd`. `pty` asks for a real terminal of `cols` × `rows`; without python3 it's
77
+ * a line-based one (`interactive: false`).
78
+ */
79
+ async open(cwd, opts = {}) {
80
+ const python = opts.pty ? await ptyPython(this.python) : void 0;
81
+ const pty = !!python;
82
+ while (this.shells.size >= MAX_SHELLS) {
83
+ const spare = await this.spare();
84
+ if (!spare) throw new ShellLimitError(`${MAX_SHELLS} terminals are open and each is running something: stop or close one first`);
85
+ this.close(spare.id);
86
+ }
87
+ const id = `sh${Date.now().toString(36)}${randomBytes(2).toString("hex")}`;
88
+ const shell = python ? this.startPty(id, python, cwd, clamp(opts.cols, 2, 1e3, 80), clamp(opts.rows, 1, 500, 24)) : this.startLines(id, cwd);
89
+ shell.child.stdin?.on("error", () => {
90
+ });
91
+ const ended = (code) => {
92
+ if (shell.status === "exited") return;
93
+ untrackGroup(shell.child.pid);
94
+ untrackGroup(shell.pid);
95
+ if (shell.pending) this.emit(shell, { type: "output", text: shell.pending });
96
+ const rest = shell.decoder?.decode();
97
+ if (rest) this.emit(shell, { type: "output", text: rest });
98
+ shell.pending = "";
99
+ shell.status = "exited";
100
+ shell.exitCode = code;
101
+ shell.busy = false;
102
+ shell.running = void 0;
103
+ this.emit(shell, { type: "exit", code });
104
+ };
105
+ shell.child.on(pty ? "close" : "exit", ended);
106
+ shell.child.on("error", () => shell.child.pid === void 0 && ended(null));
107
+ this.shells.set(id, shell);
108
+ return describe(shell);
109
+ }
110
+ /**
111
+ * The shell to close to make room for another: one that's done, else the oldest one waiting for
112
+ * a command. Never one that's running something, in the foreground or as a job in the
113
+ * background (`npm run dev &`, a stopped vim): closing the shell would end it.
114
+ */
115
+ async spare() {
116
+ const byAge = [...this.shells.values()].sort((a, b) => a.startedAt - b.startedAt);
117
+ const exited = byAge.find((s) => s.status === "exited");
118
+ if (exited) return exited;
119
+ if (!byAge.some((s) => !s.busy)) return void 0;
120
+ const procs = await processes();
121
+ return byAge.find((s) => s.status === "exited" || !s.busy && !hasJobs(s, procs));
122
+ }
123
+ /** Send a command to a line-based shell. Its exit code and the folder it left the shell in come back as `done`. */
124
+ write(id, text) {
125
+ const s = this.shells.get(id);
126
+ if (!s || s.status !== "running" || !s.child.stdin?.writable) return false;
127
+ if (s.interactive) return this.type(id, `${text.replace(/\r\n?|\n/g, "\r").replace(/\r$/, "")}\r`);
128
+ const command = text.replace(/\r\n?/g, "\n").replace(/\n$/, "");
129
+ this.emit(s, { type: "input", text: command });
130
+ s.busy = true;
131
+ s.child.stdin.write(`${command}
132
+ printf '\\036%s\\036%s\\036\\n' "$?" "$PWD"
133
+ `);
134
+ return true;
135
+ }
136
+ /**
137
+ * Keystrokes for an interactive shell, as typed (a paste, an escape sequence, Ctrl-C as \x03).
138
+ * `latin1`: bytes, one per character, sent as they are (xterm's mouse reports, which aren't UTF-8).
139
+ */
140
+ type(id, data, encoding = "utf8") {
141
+ const s = this.shells.get(id);
142
+ if (!s || !s.interactive || s.status !== "running" || !s.child.stdin?.writable) return false;
143
+ s.child.stdin.write(Buffer.from(data, encoding));
144
+ return true;
145
+ }
146
+ /** Buffered keystrokes, for bounded remote terminal input. */
147
+ pendingInputBytes(id) {
148
+ return this.shells.get(id)?.child.stdin?.writableLength ?? 0;
149
+ }
150
+ /** An interactive shell's window changed size: the programs in it get SIGWINCH. */
151
+ resize(id, cols, rows) {
152
+ const s = this.shells.get(id);
153
+ const control = s?.child.stdio[3];
154
+ if (!s || !s.interactive || s.status !== "running" || !control?.writable) return false;
155
+ s.cols = clamp(cols, 2, 1e3, s.cols ?? 80);
156
+ s.rows = clamp(rows, 1, 500, s.rows ?? 24);
157
+ control.write(`${JSON.stringify({ resize: [s.cols, s.rows] })}
158
+ `);
159
+ return true;
160
+ }
161
+ subscribe(id, client) {
162
+ const s = this.shells.get(id);
163
+ if (!s) return void 0;
164
+ s.clients.add(client);
165
+ return () => s.clients.delete(client);
166
+ }
167
+ close(id) {
168
+ const s = this.shells.get(id);
169
+ if (!s) return false;
170
+ if (s.status === "running") {
171
+ if (s.interactive) {
172
+ if (s.pid) signalGroup(s.pid, "SIGHUP");
173
+ killGroup(s.child.pid, PTY_CLOSE_MS);
174
+ } else killGroup(s.child.pid, 2e3);
175
+ }
176
+ untrackGroup(s.child.pid);
177
+ this.shells.delete(id);
178
+ return true;
179
+ }
180
+ /**
181
+ * Close every shell: code-flow is stopping. An interactive shell's helper is left to hang up its
182
+ * terminal on its own, after code-flow has exited (about a second), so it's no longer tracked:
183
+ * killed at exit, it couldn't, and jobs in process groups of their own (`nohup cmd &`, `disown`)
184
+ * would keep running with nothing left that knows about them.
185
+ */
186
+ closeAll() {
187
+ for (const s of [...this.shells.values()]) {
188
+ if (!s.interactive || s.status !== "running") {
189
+ this.close(s.id);
190
+ continue;
191
+ }
192
+ if (s.pid) signalGroup(s.pid, "SIGHUP");
193
+ if (s.child.pid) signalGroup(s.child.pid, "SIGTERM");
194
+ untrackGroup(s.child.pid);
195
+ untrackGroup(s.pid);
196
+ this.shells.delete(s.id);
197
+ }
198
+ }
199
+ startLines(id, cwd) {
200
+ const program = shellProgram();
201
+ const child = spawn(program, [], {
202
+ cwd,
203
+ detached: true,
204
+ // its own group: closing the shell closes whatever it started
205
+ stdio: ["pipe", "pipe", "pipe"],
206
+ env: childEnv({ TERM: "dumb", NO_COLOR: "1", FORCE_COLOR: "0", PAGER: "cat", GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" })
207
+ });
208
+ trackGroup(child.pid);
209
+ const shell = newShell(id, cwd, program, child, false);
210
+ child.stdout.on("data", (chunk) => this.onStdout(shell, chunk.toString()));
211
+ child.stderr.on("data", (chunk) => this.emit(shell, { type: "output", text: chunk.toString() }));
212
+ child.on("error", (err) => this.emit(shell, { type: "output", text: `Couldn't start ${program}: ${err.message}
213
+ ` }));
214
+ return shell;
215
+ }
216
+ startPty(id, python, cwd, cols, rows) {
217
+ const program = loginShell();
218
+ const env = childEnv({ TERM: "xterm-256color", COLORTERM: "truecolor" });
219
+ if (!env.LC_ALL && !env.LC_CTYPE && !env.LANG) env.LANG = process.platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8";
220
+ const child = spawn(python, ["-I", PTY_SCRIPT, String(cols), String(rows), program, `-${basename(program)}`], {
221
+ cwd,
222
+ detached: true,
223
+ // the helper's own group; the shell gets a session of its own in the terminal
224
+ stdio: ["pipe", "pipe", "pipe", "pipe"],
225
+ env
226
+ });
227
+ trackGroup(child.pid);
228
+ const shell = newShell(id, cwd, program, child, true);
229
+ shell.cols = cols;
230
+ shell.rows = rows;
231
+ shell.decoder = new TextDecoder("utf-8");
232
+ child.stdout.on("data", (chunk) => {
233
+ const text = shell.decoder.decode(chunk, { stream: true });
234
+ if (text) this.emit(shell, { type: "output", text });
235
+ });
236
+ child.stderr.on("data", (chunk) => this.emit(shell, { type: "output", text: chunk.toString().replace(/\r?\n/g, "\r\n") }));
237
+ child.on("error", (err) => this.emit(shell, { type: "output", text: `Couldn't start the terminal (${python}): ${err.message}\r
238
+ ` }));
239
+ let control = "";
240
+ child.stdio[3]?.on("data", (chunk) => {
241
+ control += chunk.toString();
242
+ const lines = control.split("\n");
243
+ control = lines.pop();
244
+ for (const line of lines) {
245
+ let msg;
246
+ try {
247
+ msg = JSON.parse(line);
248
+ } catch {
249
+ continue;
250
+ }
251
+ if (typeof msg.pid === "number") {
252
+ shell.pid = msg.pid;
253
+ if (shell.status === "running") trackGroup(msg.pid);
254
+ }
255
+ if (typeof msg.tty === "string") shell.tty = msg.tty.replace(/^\/dev\//, "");
256
+ if ("fg" in msg) this.onForeground(shell, msg.fg ?? null);
257
+ }
258
+ });
259
+ child.stdio[3]?.on("error", () => {
260
+ });
261
+ return shell;
262
+ }
263
+ /** The terminal's foreground changed: busy unless it's the shell itself; name what's running. */
264
+ onForeground(s, fg) {
265
+ s.fg = fg;
266
+ const busy = fg !== null && s.pid !== void 0 && fg !== s.pid;
267
+ if (busy === s.busy && !busy) return;
268
+ s.busy = busy;
269
+ s.running = void 0;
270
+ this.emit(s, { type: "busy", busy });
271
+ if (!busy) return;
272
+ execFile("ps", ["-o", "comm=", "-p", String(fg)], { timeout: 5e3, env: { ...process.env, LC_ALL: "C" } }, (err, stdout) => {
273
+ const name = basename(String(stdout).trim()).replace(/^-/, "");
274
+ if (err || !name || s.fg !== fg || s.status !== "running") return;
275
+ s.running = name;
276
+ this.emit(s, { type: "busy", busy: true, running: name });
277
+ });
278
+ }
279
+ onStdout(s, chunk) {
280
+ s.pending += chunk;
281
+ for (; ; ) {
282
+ const m = MARKER.exec(s.pending);
283
+ if (!m) break;
284
+ const before = s.pending.slice(0, m.index);
285
+ if (before) this.emit(s, { type: "output", text: before });
286
+ s.pending = s.pending.slice(m.index + m[0].length);
287
+ s.busy = false;
288
+ s.cwd = m[2] || s.cwd;
289
+ this.emit(s, { type: "done", code: Number(m[1]), cwd: s.cwd });
290
+ }
291
+ const partial = s.pending.indexOf(MARK);
292
+ const ready = partial < 0 ? s.pending : s.pending.slice(0, partial);
293
+ if (ready) {
294
+ this.emit(s, { type: "output", text: ready });
295
+ s.pending = s.pending.slice(ready.length);
296
+ }
297
+ }
298
+ emit(s, event) {
299
+ if (event.type === "busy") {
300
+ for (const client of s.clients) client(event);
301
+ return;
302
+ }
303
+ const last = s.history.at(-1);
304
+ if (event.type === "output" && last?.type === "output" && last.text.length < 8192) last.text += event.text;
305
+ else s.history.push(event.type === "output" ? { ...event } : event);
306
+ s.historyBytes += event.type === "output" || event.type === "input" ? event.text.length : 16;
307
+ while (s.historyBytes > HISTORY_BYTES && s.history.length > 1) {
308
+ const dropped = s.history.shift();
309
+ s.historyBytes -= dropped.type === "output" || dropped.type === "input" ? dropped.text.length : 16;
310
+ s.trimmed = true;
311
+ }
312
+ for (const client of s.clients) client(event);
313
+ }
314
+ }
315
+ function newShell(id, cwd, program, child, interactive) {
316
+ return { id, cwd, shell: program, status: "running", startedAt: Date.now(), busy: false, interactive, child, history: [], historyBytes: 0, trimmed: false, pending: "", clients: /* @__PURE__ */ new Set() };
317
+ }
318
+ function processes() {
319
+ return new Promise((resolve) => {
320
+ execFile("ps", ["-A", "-o", "pid=,pgid=,tty="], { timeout: 5e3, maxBuffer: 16 * 1024 * 1024, env: { ...process.env, LC_ALL: "C" } }, (err, stdout) => {
321
+ if (err) return resolve([]);
322
+ const rows = String(stdout).split("\n").map((line) => line.trim().split(/\s+/));
323
+ resolve(rows.filter((r) => r.length >= 2).map(([pid, pgid, tty]) => ({ pid: Number(pid), pgid: Number(pgid), tty: tty ?? "" })));
324
+ });
325
+ });
326
+ }
327
+ function hasJobs(s, procs) {
328
+ if (s.interactive) return !!s.tty && procs.some((p) => p.tty === s.tty && p.pgid !== s.pid);
329
+ return procs.some((p) => p.pgid === s.child.pid && p.pid !== s.child.pid);
330
+ }
331
+ function signalGroup(pid, signal) {
332
+ try {
333
+ process.kill(-pid, signal);
334
+ } catch {
335
+ }
336
+ }
337
+ function describe(s) {
338
+ return { id: s.id, cwd: s.cwd, shell: s.shell, status: s.status, exitCode: s.exitCode, startedAt: s.startedAt, busy: s.busy, interactive: s.interactive, cols: s.cols, rows: s.rows, running: s.running };
339
+ }
340
+ export {
341
+ MAX_SHELLS,
342
+ ShellLimitError,
343
+ Shells,
344
+ ptyAvailable,
345
+ shellProgram
346
+ };