@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,292 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, realpath } from "node:fs/promises";
3
+ import { get as httpGet } from "node:http";
4
+ import { isAbsolute, join, relative, sep } from "node:path";
5
+ import { freePort } from "./serve.js";
6
+ class LaunchProfileError extends Error {
7
+ constructor(message, status = 400, confirm) {
8
+ super(message);
9
+ this.status = status;
10
+ this.confirm = confirm;
11
+ }
12
+ }
13
+ const identifier = /^[a-z][a-z0-9-]{0,39}$/;
14
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
15
+ const text = (value, max) => typeof value === "string" && !!value.trim() && value.length <= max && !value.includes("\0");
16
+ const envPrefix = (id) => id.replace(/-/g, "_").toUpperCase();
17
+ function directoryAllowed(path) {
18
+ return path === "." || typeof path === "string" && path.length <= 500 && !isAbsolute(path) && !/^[A-Za-z]:/.test(path) && !/[\\\x00-\x1f]/.test(path) && !path.split("/").some((part) => !part || part === "." || part === ".." || part === ".git");
19
+ }
20
+ function validateLaunchProfiles(value) {
21
+ if (!object(value) || value.version !== 1 || !Array.isArray(value.profiles) || value.profiles.length > 12 || Object.keys(value).some((key) => !["version", "profiles"].includes(key))) throw new LaunchProfileError("Expected version 1 launch configuration with at most 12 profiles.");
22
+ const profiles = value.profiles.map((raw) => {
23
+ if (!object(raw) || typeof raw.id !== "string" || !identifier.test(raw.id) || !text(raw.name, 120) || !Array.isArray(raw.services) || raw.services.length < 1 || raw.services.length > 6 || Object.keys(raw).some((key) => !["id", "name", "services"].includes(key))) throw new LaunchProfileError("Each named launch profile needs a valid ID and 1\u20136 services.");
24
+ const services = raw.services.map((entry) => {
25
+ if (!object(entry) || typeof entry.id !== "string" || !identifier.test(entry.id) || !text(entry.command, 4e3) || entry.name !== void 0 && !text(entry.name, 120) || !directoryAllowed(entry.cwd ?? ".") || Object.keys(entry).some((key) => !["id", "name", "command", "cwd", "dependsOn", "health", "env"].includes(key))) throw new LaunchProfileError("Services need a valid ID, bounded command and a safe relative working directory.");
26
+ const dependsOn = entry.dependsOn ?? [];
27
+ if (!Array.isArray(dependsOn) || dependsOn.length > 5 || dependsOn.some((id) => typeof id !== "string" || !identifier.test(id)) || new Set(dependsOn).size !== dependsOn.length) throw new LaunchProfileError(`${entry.id} has invalid dependencies.`);
28
+ let health = { path: "/", timeoutMs: 15e3 };
29
+ if (entry.health === false) health = false;
30
+ else if (entry.health !== void 0) {
31
+ if (!object(entry.health) || !text(entry.health.path, 500) || !entry.health.path.startsWith("/") || entry.health.path.startsWith("//") || /[\\#\x00-\x20\x7f]/.test(entry.health.path) || Object.keys(entry.health).some((key) => !["path", "timeoutMs"].includes(key))) throw new LaunchProfileError(`${entry.id} needs a local HTTP health path.`);
32
+ const timeoutMs = entry.health.timeoutMs ?? 15e3;
33
+ if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 12e4) throw new LaunchProfileError("Health timeout must be between 100 and 120,000 ms.");
34
+ health = { path: entry.health.path, timeoutMs };
35
+ }
36
+ if (entry.env !== void 0 && !object(entry.env)) throw new LaunchProfileError("Service environment must be an object.");
37
+ const env = {};
38
+ for (const [key, val] of Object.entries(entry.env ?? {})) {
39
+ if (!/^[A-Z_][A-Z0-9_]{0,99}$/.test(key) || key.startsWith("CODEFLOW_") || ["PORT", "HOST", "SERVICE_NAME"].includes(key) || typeof val !== "string" || val.length > 4e3 || val.includes("\0")) throw new LaunchProfileError(`Invalid or reserved service environment variable: ${key}`);
40
+ env[key] = val;
41
+ }
42
+ if (Object.keys(env).length > 40) throw new LaunchProfileError("Service environment is limited to 40 entries.");
43
+ return { id: entry.id, ...entry.name ? { name: entry.name } : {}, command: entry.command, cwd: entry.cwd ?? ".", dependsOn: [...dependsOn], health, env };
44
+ });
45
+ if (new Set(services.map((service) => service.id)).size !== services.length) throw new LaunchProfileError("Service IDs must be unique within a profile.");
46
+ const ordered = /* @__PURE__ */ new Set();
47
+ while (ordered.size < services.length) {
48
+ const ready = services.find((service) => !ordered.has(service.id) && service.dependsOn.every((id) => ordered.has(id)));
49
+ if (!ready) throw new LaunchProfileError("Service dependencies must name existing services and form an acyclic graph.");
50
+ ordered.add(ready.id);
51
+ }
52
+ const reserved = new Set(services.flatMap((service) => [`${envPrefix(service.id)}_URL`, `${envPrefix(service.id)}_PORT`]));
53
+ if (services.some((service) => Object.keys(service.env).some((key) => reserved.has(key)))) throw new LaunchProfileError("Generated peer URL and port variables cannot be overridden.");
54
+ const generated = /* @__PURE__ */ new Set([...reserved, "PORT", "HOST", "SERVICE_NAME"]);
55
+ for (const service of services) for (const value2 of Object.values(service.env)) for (const match of value2.matchAll(/\$\{([A-Z_][A-Z0-9_]*)\}/g)) if (!generated.has(match[1])) throw new LaunchProfileError(`Unknown generated environment variable: ${match[1]}`);
56
+ return { id: raw.id, name: raw.name, services };
57
+ });
58
+ if (new Set(profiles.map((profile) => profile.id)).size !== profiles.length) throw new LaunchProfileError("Launch profile IDs must be unique.");
59
+ return profiles;
60
+ }
61
+ async function launchDirectory(root, path) {
62
+ if (!directoryAllowed(path)) throw new LaunchProfileError("Choose a safe relative service working directory.");
63
+ if (!(await lstat(root)).isDirectory()) throw new LaunchProfileError("Launch root must be a regular worktree directory.");
64
+ const canonical = await realpath(root);
65
+ let current = canonical;
66
+ for (const part of path === "." ? [] : path.split("/")) {
67
+ current = join(current, part);
68
+ if (!(await lstat(current)).isDirectory()) throw new LaunchProfileError(`Service directory is missing or uses a symbolic link: ${path}`);
69
+ }
70
+ const resolved = await realpath(current);
71
+ const rel = relative(canonical, resolved);
72
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new LaunchProfileError("Service working directory escapes the worktree.");
73
+ return resolved;
74
+ }
75
+ async function readLaunchProfiles(root) {
76
+ try {
77
+ const directory = await launchDirectory(root, ".codeflow");
78
+ const handle = await open(join(directory, "launch.json"), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
79
+ try {
80
+ const info = await handle.stat();
81
+ if (!info.isFile() || info.size > 128 * 1024) throw new LaunchProfileError("Launch configuration must be a regular file under 128 KiB.");
82
+ return { profiles: validateLaunchProfiles(JSON.parse(await handle.readFile("utf8"))), found: true };
83
+ } finally {
84
+ await handle.close();
85
+ }
86
+ } catch (error) {
87
+ if (error.code === "ENOENT") return { profiles: [], found: false };
88
+ return { profiles: [], found: true, error: error.message };
89
+ }
90
+ }
91
+ function healthProbe(port, path, signal, budgetMs) {
92
+ if (signal.aborted) return Promise.resolve(false);
93
+ return new Promise((resolve) => {
94
+ let settled = false;
95
+ const done = (ok) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ clearTimeout(timer);
99
+ signal.removeEventListener("abort", aborted);
100
+ resolve(ok);
101
+ };
102
+ const request = httpGet({ hostname: "127.0.0.1", port, path, timeout: 1e3, agent: false }, (response) => {
103
+ const ok = (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300;
104
+ response.destroy();
105
+ done(ok);
106
+ });
107
+ const aborted = () => {
108
+ request.destroy();
109
+ done(false);
110
+ };
111
+ const timer = setTimeout(aborted, Math.max(1, Math.min(1e3, budgetMs)));
112
+ signal.addEventListener("abort", aborted, { once: true });
113
+ request.on("timeout", () => {
114
+ request.destroy();
115
+ done(false);
116
+ });
117
+ request.on("error", () => done(false));
118
+ });
119
+ }
120
+ const delay = (ms, signal) => new Promise((resolve) => {
121
+ if (signal.aborted) return resolve();
122
+ const finish = () => {
123
+ clearTimeout(timer);
124
+ signal.removeEventListener("abort", finish);
125
+ resolve();
126
+ };
127
+ const timer = setTimeout(finish, ms);
128
+ signal.addEventListener("abort", finish, { once: true });
129
+ });
130
+ class LaunchProfiles {
131
+ constructor(servers) {
132
+ this.servers = servers;
133
+ }
134
+ sessions = /* @__PURE__ */ new Map();
135
+ get(key) {
136
+ const entry = this.sessions.get(key);
137
+ if (!entry) return void 0;
138
+ this.refresh(entry);
139
+ return structuredClone(entry.session);
140
+ }
141
+ refresh(entry) {
142
+ for (const service of entry.session.services) {
143
+ const live = this.servers.get(`${entry.session.key}/${service.id}`);
144
+ if (live) {
145
+ service.logs = live.logs.slice(-100).map((line) => line.slice(-2e3));
146
+ service.exitCode = live.exitCode;
147
+ if (live.status === "exited") service.status = "exited";
148
+ } else if (entry.session.status === "ready") service.status = "exited";
149
+ }
150
+ if (entry.session.status === "ready" && entry.session.services.some((service) => service.status === "exited")) {
151
+ const failed = entry.session.services.find((service) => service.status === "exited");
152
+ entry.session.status = "failed";
153
+ entry.session.error = `Service ${failed.name} exited; the stack was stopped.`;
154
+ this.cleanup(entry);
155
+ }
156
+ }
157
+ cleanup(entry) {
158
+ if (entry.monitor) clearInterval(entry.monitor);
159
+ for (const service of entry.session.services) {
160
+ const live = this.servers.get(`${entry.session.key}/${service.id}`);
161
+ if (live) {
162
+ service.logs = live.logs.slice(-100).map((line) => line.slice(-2e3));
163
+ service.exitCode = live.exitCode;
164
+ if (live.status === "exited") service.status = "exited";
165
+ }
166
+ if (service.status !== "exited") service.status = "stopped";
167
+ }
168
+ for (const key of [...entry.serviceKeys].reverse()) this.servers.stop(key);
169
+ }
170
+ stop(key) {
171
+ const entry = this.sessions.get(key);
172
+ if (!entry) return false;
173
+ this.refresh(entry);
174
+ entry.controller.abort();
175
+ entry.session.status = "stopped";
176
+ for (const service of entry.session.services) if (service.status !== "exited") service.status = "stopped";
177
+ this.cleanup(entry);
178
+ return true;
179
+ }
180
+ stopAll() {
181
+ for (const key of this.sessions.keys()) this.stop(key);
182
+ }
183
+ stopMatching(prefix) {
184
+ for (const key of this.sessions.keys()) if (key.startsWith(prefix)) this.stop(key);
185
+ }
186
+ async start(key, root, rawProfile, options = {}) {
187
+ if (options.lockdown && !options.allowUntrusted) throw new LaunchProfileError(`${options.lockdown}. Starting this profile executes repository commands on this machine.`, 409, "allowUntrusted");
188
+ const profile = validateLaunchProfiles({ version: 1, profiles: [rawProfile] })[0];
189
+ const previous = this.sessions.get(key);
190
+ if (previous && (previous.inFlight || ["starting", "ready"].includes(previous.session.status))) throw new LaunchProfileError("This launch profile is already active or still stopping. Wait for it to stop before starting another.", 409);
191
+ if (this.sessions.size >= 50) for (const [oldKey, entry2] of this.sessions) {
192
+ if (["stopped", "failed"].includes(entry2.session.status)) {
193
+ this.sessions.delete(oldKey);
194
+ if (this.sessions.size < 50) break;
195
+ }
196
+ }
197
+ if (!previous && this.sessions.size >= 50) throw new LaunchProfileError("Too many active launch profiles. Stop a stack before starting another.", 409);
198
+ const entry = { session: { key, root, profile: profile.id, status: "starting", startedAt: Date.now(), services: [] }, controller: new AbortController(), serviceKeys: [], inFlight: true };
199
+ this.sessions.set(key, entry);
200
+ const aborted = () => entry.controller.abort();
201
+ options.signal?.addEventListener("abort", aborted, { once: true });
202
+ if (options.signal?.aborted) entry.controller.abort();
203
+ const active = () => {
204
+ if (entry.controller.signal.aborted) throw new LaunchProfileError("Launch startup was cancelled.", 409);
205
+ };
206
+ const childrenActive = () => {
207
+ active();
208
+ this.refresh(entry);
209
+ for (const serviceKey of entry.serviceKeys) if (this.servers.get(serviceKey)?.status !== "running") throw new LaunchProfileError(`Service ${serviceKey.split("/").at(-1)} exited or was stopped during startup.`, 422);
210
+ };
211
+ try {
212
+ const ports = /* @__PURE__ */ new Set();
213
+ const generated = {};
214
+ for (const service of profile.services) {
215
+ active();
216
+ const cwd = await launchDirectory(root, service.cwd);
217
+ let port = await freePort();
218
+ for (let retry = 0; ports.has(port) && retry < 20; retry++) port = await freePort();
219
+ if (ports.has(port)) throw new LaunchProfileError("Could not allocate distinct service ports.", 409);
220
+ ports.add(port);
221
+ const url = `http://127.0.0.1:${port}`;
222
+ generated[`${envPrefix(service.id)}_URL`] = url;
223
+ generated[`${envPrefix(service.id)}_PORT`] = String(port);
224
+ entry.session.services.push({ id: service.id, name: service.name ?? service.id, command: service.command, cwd, port, url, status: "pending", healthChecked: false, logs: [] });
225
+ }
226
+ const ready = /* @__PURE__ */ new Set();
227
+ const deadline = Date.now() + 18e4;
228
+ while (ready.size < profile.services.length) {
229
+ childrenActive();
230
+ if (Date.now() >= deadline) throw new LaunchProfileError("The launch profile exceeded its startup deadline.", 422);
231
+ const service = profile.services.find((service2) => !ready.has(service2.id) && service2.dependsOn.every((id) => ready.has(id)));
232
+ const snapshot = entry.session.services.find((item) => item.id === service.id);
233
+ const serviceKey = `${key}/${service.id}`;
234
+ if (await launchDirectory(root, service.cwd) !== snapshot.cwd) throw new LaunchProfileError("A service directory changed during startup.", 409);
235
+ childrenActive();
236
+ const allocated = { ...generated, PORT: String(snapshot.port), HOST: "127.0.0.1", SERVICE_NAME: service.id };
237
+ const env = { ...allocated };
238
+ for (const [name, value] of Object.entries(service.env)) env[name] = value.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/g, (_, id) => {
239
+ if (allocated[id] === void 0) throw new LaunchProfileError(`Unknown generated environment variable: ${id}`);
240
+ return allocated[id];
241
+ });
242
+ snapshot.status = "starting";
243
+ entry.serviceKeys.push(serviceKey);
244
+ await this.servers.start(serviceKey, snapshot.cwd, service.command, snapshot.port, env, entry.controller.signal);
245
+ if (service.health !== false) {
246
+ const expires = Math.min(deadline, Date.now() + service.health.timeoutMs);
247
+ let healthy = false;
248
+ while (Date.now() < expires) {
249
+ childrenActive();
250
+ if (await healthProbe(snapshot.port, service.health.path, entry.controller.signal, expires - Date.now())) {
251
+ healthy = true;
252
+ break;
253
+ }
254
+ await delay(Math.min(100, Math.max(0, expires - Date.now())), entry.controller.signal);
255
+ }
256
+ if (!healthy) throw new LaunchProfileError(`Service ${snapshot.name} did not pass its HTTP health check before the startup deadline.`, 422);
257
+ snapshot.status = "healthy";
258
+ snapshot.healthChecked = true;
259
+ } else {
260
+ await delay(100, entry.controller.signal);
261
+ childrenActive();
262
+ snapshot.status = "running";
263
+ }
264
+ ready.add(service.id);
265
+ }
266
+ childrenActive();
267
+ entry.session.status = "ready";
268
+ entry.session.readyAt = Date.now();
269
+ entry.monitor = setInterval(() => this.refresh(entry), 1e3);
270
+ entry.monitor.unref?.();
271
+ return this.get(key);
272
+ } catch (error) {
273
+ this.refresh(entry);
274
+ if (entry.session.status !== "stopped") {
275
+ entry.session.status = "failed";
276
+ entry.session.error = error.message;
277
+ }
278
+ this.cleanup(entry);
279
+ throw error;
280
+ } finally {
281
+ entry.inFlight = false;
282
+ options.signal?.removeEventListener("abort", aborted);
283
+ }
284
+ }
285
+ }
286
+ export {
287
+ LaunchProfileError,
288
+ LaunchProfiles,
289
+ launchDirectory,
290
+ readLaunchProfiles,
291
+ validateLaunchProfiles
292
+ };
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { CODEFLOW_HOME } from "./config.js";
6
+ import { git } from "./git.js";
7
+ const CONVENTION_FILES = [".codeflow/conventions.md", "AGENTS.md", "CLAUDE.md", "GEMINI.md", ".github/copilot-instructions.md", ".cursorrules"];
8
+ const MAX_CONVENTIONS = 6e3;
9
+ async function readConventions(repo, ref) {
10
+ const parts = [];
11
+ let size = 0;
12
+ for (const path of CONVENTION_FILES) {
13
+ const res = await git(["show", `${ref}:${path}`], repo);
14
+ const text = res.code === 0 ? res.stdout.trim() : "";
15
+ if (!text) continue;
16
+ const room = MAX_CONVENTIONS - size;
17
+ if (room < 200) break;
18
+ const body = text.length > room ? `${text.slice(0, room)}
19
+ \u2026 (${path} is longer; read the file for the rest)` : text;
20
+ parts.push(`### ${path}
21
+ ${body}`);
22
+ size += body.length;
23
+ }
24
+ return parts.join("\n\n");
25
+ }
26
+ const DIR = join(CODEFLOW_HOME, "learnings");
27
+ const MAX_LESSONS = 200;
28
+ const RECURRING = 2;
29
+ const fileFor = (repo) => join(DIR, `${createHash("sha1").update(repo).digest("hex").slice(0, 16)}.json`);
30
+ async function load(repo) {
31
+ const file = fileFor(repo);
32
+ if (!existsSync(file)) return [];
33
+ try {
34
+ return JSON.parse(await readFile(file, "utf8"));
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+ const words = (s) => new Set(s.toLowerCase().match(/[a-z0-9_]{3,}/g) ?? []);
40
+ function similar(a, b) {
41
+ const x = words(a);
42
+ const y = words(b);
43
+ if (!x.size || !y.size) return false;
44
+ let shared = 0;
45
+ for (const w of x) if (y.has(w)) shared++;
46
+ return shared / (x.size + y.size - shared) >= 0.6;
47
+ }
48
+ let saving = Promise.resolve();
49
+ function recordFindings(repo, runId, findings) {
50
+ const worth = findings.filter((f) => f.severity === "high" || f.severity === "medium").filter((f) => f.problem?.trim());
51
+ if (!worth.length) return saving;
52
+ saving = saving.then(async () => {
53
+ const lessons = await load(repo);
54
+ for (const f of worth) {
55
+ const problem = f.problem.trim().slice(0, 300);
56
+ const match = lessons.find((l) => similar(l.problem, problem));
57
+ if (match) {
58
+ if (!match.runs.includes(runId)) match.runs.push(runId);
59
+ match.at = Date.now();
60
+ } else {
61
+ lessons.push({ problem, file: f.file ?? "", runs: [runId], at: Date.now() });
62
+ }
63
+ }
64
+ lessons.sort((a, b) => b.at - a.at);
65
+ await mkdir(DIR, { recursive: true });
66
+ const file = fileFor(repo);
67
+ await writeFile(`${file}.tmp`, JSON.stringify(lessons.slice(0, MAX_LESSONS), null, 1));
68
+ await rename(`${file}.tmp`, file);
69
+ }).catch(() => void 0);
70
+ return saving;
71
+ }
72
+ async function recurringLessons(repo, limit = 8) {
73
+ await saving;
74
+ return (await load(repo)).filter((l) => l.runs.length >= RECURRING).sort((a, b) => b.runs.length - a.runs.length || b.at - a.at).slice(0, limit);
75
+ }
76
+ export {
77
+ readConventions,
78
+ recordFindings,
79
+ recurringLessons
80
+ };
@@ -0,0 +1,19 @@
1
+ const LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
2
+ function localAppUrl(value, ownAddress) {
3
+ try {
4
+ const url = new URL(value);
5
+ if (url.protocol !== "http:" || !LOOPBACK.has(url.hostname) || !url.port || url.username || url.password) return;
6
+ if (ownAddress && url.port === new URL(ownAddress).port) return;
7
+ return url.href;
8
+ } catch {
9
+ return;
10
+ }
11
+ }
12
+ function localAppUrls(text, ownAddress) {
13
+ const found = [...text.matchAll(/http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+(?:[/?#][^\s<>"'`()\[\]]*)?/gi)];
14
+ return [...new Set(found.map(([value]) => localAppUrl(value.replace(/[.,;!]+$/, ""), ownAddress)).filter((url) => !!url))];
15
+ }
16
+ export {
17
+ localAppUrl,
18
+ localAppUrls
19
+ };
package/src/log.js ADDED
@@ -0,0 +1,39 @@
1
+ import { appendFileSync, mkdirSync, renameSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CODEFLOW_HOME } from "./config.js";
4
+ const LOG_FILE = join(CODEFLOW_HOME, "server.log");
5
+ const MAX_BYTES = 5 * 1024 * 1024;
6
+ let writes = 0;
7
+ function write(level, message, detail) {
8
+ try {
9
+ mkdirSync(CODEFLOW_HOME, { recursive: true });
10
+ if (writes++ % 50 === 0) {
11
+ const size = statSync(LOG_FILE, { throwIfNoEntry: false })?.size ?? 0;
12
+ if (size > MAX_BYTES) renameSync(LOG_FILE, `${LOG_FILE}.1`);
13
+ }
14
+ appendFileSync(LOG_FILE, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), level, message, ...detail })}
15
+ `);
16
+ } catch {
17
+ }
18
+ }
19
+ function describe(err) {
20
+ if (err instanceof Error) return { error: err.message, stack: err.stack?.split("\n").slice(0, 12).join("\n") };
21
+ return { error: String(err) };
22
+ }
23
+ function logInfo(message, detail) {
24
+ write("info", message, detail);
25
+ }
26
+ function logWarn(message, detail) {
27
+ write("warn", message, detail);
28
+ }
29
+ function logError(message, err, detail) {
30
+ const info = err === void 0 ? {} : describe(err);
31
+ write("error", message, { ...info, ...detail });
32
+ console.error(`code-flow: ${message}${info.error ? `: ${info.error}` : ""}`);
33
+ }
34
+ export {
35
+ LOG_FILE,
36
+ logError,
37
+ logInfo,
38
+ logWarn
39
+ };