@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,143 @@
1
+ import { request } from "node:http";
2
+ import { HttpError, readJson, send } from "../http.js";
3
+ import { Previews } from "../preview.js";
4
+ function parseRuntime(text) {
5
+ const raw = JSON.parse(text.split(/\r?\n/).reverse().find((line) => line.startsWith("CODEFLOW_SSH_READY "))?.slice(19) ?? "null");
6
+ if (raw?.protocol !== 1 || !Number.isInteger(raw.port) || raw.port < 1 || raw.port > 65535 || !/^[a-f0-9]{64}$/.test(raw.token) || !/^[a-f0-9]{32}$/.test(raw.instance) || !/^[a-f0-9]{64}$/.test(raw.version)) throw new Error("The SSH host returned an invalid backend handshake");
7
+ if (raw.remotePort !== void 0 && (!Number.isInteger(raw.remotePort) || raw.remotePort < 1 || raw.remotePort > 65535)) throw new Error("The SSH host returned an invalid team port");
8
+ return raw;
9
+ }
10
+ const HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);
11
+ class SshGateway {
12
+ constructor(transport, runtime, reconnect, probe = "/api/ssh-runtime") {
13
+ this.transport = transport;
14
+ this.runtime = runtime;
15
+ this.reconnect = reconnect;
16
+ this.probe = probe;
17
+ this.previews = new Previews(() => [...this.origins], () => [this.runtime.port], (target) => this.open(target.port, target.hostname));
18
+ }
19
+ streams = /* @__PURE__ */ new Set();
20
+ origins = /* @__PURE__ */ new Set();
21
+ closed = false;
22
+ previews;
23
+ restoring;
24
+ error;
25
+ open(port, hostname) {
26
+ const stream = this.transport.open(port, hostname);
27
+ this.streams.add(stream);
28
+ stream.once("close", () => this.streams.delete(stream));
29
+ return stream;
30
+ }
31
+ restore() {
32
+ return this.restoring ??= (async () => {
33
+ const next = await this.reconnect();
34
+ if (this.closed) {
35
+ await next.transport.close();
36
+ throw new HttpError(410, "This computer was disconnected");
37
+ }
38
+ this.previews.closeAll();
39
+ for (const stream of this.streams) stream.destroy();
40
+ this.transport = next.transport;
41
+ this.runtime = next.runtime;
42
+ this.error = void 0;
43
+ })().finally(() => {
44
+ this.restoring = void 0;
45
+ });
46
+ }
47
+ async ensureConnected() {
48
+ if (this.restoring) return this.restoring;
49
+ const probedTransport = this.transport;
50
+ try {
51
+ await this.json(this.probe);
52
+ } catch {
53
+ if (this.transport === probedTransport) await this.restore();
54
+ }
55
+ if (this.restoring) await this.restoring;
56
+ }
57
+ async forward(req, res, path, origin, body) {
58
+ if (this.restoring) await this.restoring;
59
+ if (this.closed) throw new HttpError(410, "This computer was disconnected");
60
+ if (!path.startsWith("/api/")) throw new HttpError(404, "Unknown remote operation");
61
+ this.origins.add(origin);
62
+ res.setHeader("referrer-policy", "no-referrer");
63
+ res.setHeader("cache-control", "no-store");
64
+ if (new URL(path, origin).pathname === "/api/preview" && req.method === "POST") {
65
+ const lease = await this.json("/api/ssh-runtime/preview", "POST", await readJson(req, 8192));
66
+ return send(res, 200, await this.previews.show(String(lease.url), new URL(origin).host));
67
+ }
68
+ const headers = {};
69
+ for (const [key, value] of Object.entries(req.headers)) if (value !== void 0 && !HOP.has(key) && !["cookie", "authorization", "host", "origin", "referer", "forwarded"].includes(key) && !key.startsWith("x-forwarded-")) headers[key] = value;
70
+ headers.host = `127.0.0.1:${this.runtime.port}`;
71
+ headers.authorization = `Bearer ${this.runtime.token}`;
72
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
73
+ if (payload !== void 0) {
74
+ headers["content-type"] = "application/json";
75
+ headers["content-length"] = String(Buffer.byteLength(payload));
76
+ }
77
+ if (req.headers.origin) headers.origin = `http://${headers.host}`;
78
+ const up = request({ host: "127.0.0.1", port: this.runtime.port, method: req.method, path, headers, createConnection: () => this.open(this.runtime.port) }, (incoming) => {
79
+ this.error = void 0;
80
+ const out = {};
81
+ for (const [key, value] of Object.entries(incoming.headers)) if (value !== void 0 && !HOP.has(key) && !["set-cookie", "clear-site-data", "location", "content-security-policy", "content-security-policy-report-only"].includes(key)) out[key] = value;
82
+ if (incoming.headers.location) {
83
+ incoming.destroy();
84
+ send(res, 502, { error: "Unexpected remote API redirect" });
85
+ return;
86
+ }
87
+ out["content-security-policy"] = "sandbox allow-downloads; default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
88
+ out["x-content-type-options"] = "nosniff";
89
+ out["referrer-policy"] = "no-referrer";
90
+ out["cache-control"] = "no-store";
91
+ res.writeHead(incoming.statusCode ?? 502, out);
92
+ incoming.pipe(res);
93
+ incoming.on("error", () => res.destroy());
94
+ });
95
+ up.on("error", () => {
96
+ if (res.destroyed) return;
97
+ this.error = "Connection interrupted. Reconnect this computer; your work remains there.";
98
+ if (res.headersSent) res.destroy();
99
+ else send(res, 502, { error: this.error });
100
+ });
101
+ res.on("close", () => {
102
+ if (!res.writableFinished) up.destroy();
103
+ });
104
+ if (payload !== void 0) up.end(payload);
105
+ else req.pipe(up);
106
+ }
107
+ json(path, method = "GET", body, timeoutMs = 3e4) {
108
+ return new Promise((resolve, reject) => {
109
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
110
+ const req = request({ host: "127.0.0.1", port: this.runtime.port, method, path, headers: { authorization: `Bearer ${this.runtime.token}`, ...payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {} }, createConnection: () => this.open(this.runtime.port) }, (res) => {
111
+ let text = "";
112
+ res.setEncoding("utf8");
113
+ res.on("data", (data) => {
114
+ text += data;
115
+ if (text.length > 2 * 1024 * 1024) req.destroy(new Error("Remote response exceeds its limit"));
116
+ });
117
+ res.on("error", reject);
118
+ res.on("end", () => {
119
+ try {
120
+ const value = JSON.parse(text);
121
+ if ((res.statusCode ?? 500) >= 400) reject(new HttpError(res.statusCode, value.error ?? "Remote request failed"));
122
+ else resolve(value);
123
+ } catch (error) {
124
+ reject(error);
125
+ }
126
+ });
127
+ });
128
+ const timer = setTimeout(() => req.destroy(new Error("Remote request timed out")), timeoutMs);
129
+ req.on("close", () => clearTimeout(timer));
130
+ req.on("error", reject);
131
+ req.end(payload);
132
+ });
133
+ }
134
+ async close() {
135
+ this.closed = true;
136
+ this.previews.closeAll();
137
+ for (const stream of this.streams) stream.destroy();
138
+ }
139
+ }
140
+ export {
141
+ SshGateway,
142
+ parseRuntime
143
+ };
@@ -0,0 +1,320 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CODEFLOW_HOME } from "../config.js";
6
+ import { HttpError, readJson } from "../http.js";
7
+ import { sshAliases } from "./config.js";
8
+ import { SshGateway, parseRuntime } from "./gateway.js";
9
+ import { SshTransport, validSshHost, validSshOptions } from "./transport.js";
10
+ class SshProjects {
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.file = join(options.home ?? CODEFLOW_HOME, "ssh-hosts.json");
14
+ }
15
+ hosts = [];
16
+ loaded;
17
+ writes = Promise.resolve();
18
+ sessions = /* @__PURE__ */ new Map();
19
+ pending = /* @__PURE__ */ new Map();
20
+ closed = false;
21
+ teamRemoving = /* @__PURE__ */ new Set();
22
+ teamSessions = /* @__PURE__ */ new Map();
23
+ teamPending = /* @__PURE__ */ new Map();
24
+ file;
25
+ load() {
26
+ return this.loaded ??= readFile(this.file, "utf8").then((text) => {
27
+ const raw = JSON.parse(text);
28
+ if (!raw || raw.version !== 1 || !Array.isArray(raw.hosts) || raw.hosts.length > 32 || raw.hosts.some((host) => !host || typeof host !== "object" || !/^[a-f0-9]{16}$/.test(host.id) || !validSshHost(host.sshHost) || !validSshOptions(host) || typeof host.name !== "string" || !host.name || host.name.length > 80) || new Set(raw.hosts.map((host) => host.id)).size !== raw.hosts.length) throw new Error("Repair the SSH connections file before continuing");
29
+ this.hosts = raw.hosts;
30
+ }).catch((error) => {
31
+ if (error.code !== "ENOENT") throw error;
32
+ });
33
+ }
34
+ async list() {
35
+ await this.load();
36
+ return { hosts: this.hosts.map((host) => ({ ...host, state: this.pending.has(host.id) ? "connecting" : this.sessions.has(host.id) ? this.sessions.get(host.id).gateway.error ? "disconnected" : "connected" : "disconnected", error: this.sessions.get(host.id)?.gateway.error })), aliases: await sshAliases(this.options.configPath) };
37
+ }
38
+ change(operation) {
39
+ const work = this.writes.catch(() => void 0).then(async () => {
40
+ await this.load();
41
+ const before = this.hosts.slice();
42
+ operation();
43
+ try {
44
+ await mkdir(this.options.home ?? CODEFLOW_HOME, { recursive: true, mode: 448 });
45
+ const temp = `${this.file}.tmp`;
46
+ await writeFile(temp, JSON.stringify({ version: 1, hosts: this.hosts }), { mode: 384 });
47
+ await rename(temp, this.file);
48
+ } catch (error) {
49
+ this.hosts = before;
50
+ throw error;
51
+ }
52
+ });
53
+ this.writes = work;
54
+ return work;
55
+ }
56
+ async add(body) {
57
+ if (!validSshHost(body.sshHost)) throw new HttpError(400, "Enter an SSH alias or user@hostname.");
58
+ if (!validSshOptions(body)) throw new HttpError(400, "Use a port from 1 to 65535 and an absolute or ~/ SSH key path without control characters or % tokens");
59
+ const name = typeof body.name === "string" ? body.name.trim() : body.sshHost;
60
+ if (!name || name.length > 80) throw new HttpError(400, "Name the server using at most 80 characters");
61
+ const host = { id: randomBytes(8).toString("hex"), name, sshHost: body.sshHost, ...body.port === void 0 ? {} : { port: body.port }, ...body.identityFile === void 0 ? {} : { identityFile: body.identityFile } };
62
+ await this.change(() => {
63
+ if (this.hosts.length >= 32) throw new HttpError(409, "Remove an unused SSH connection first (32 maximum)");
64
+ if (this.hosts.some((item) => item.sshHost === host.sshHost && item.port === host.port && item.identityFile === host.identityFile)) throw new HttpError(409, "This SSH host is already saved");
65
+ this.hosts.push(host);
66
+ });
67
+ return host;
68
+ }
69
+ async setup(host, remotePort) {
70
+ const [code, payload] = await Promise.all([readFile(new URL("./bootstrap.mjs", import.meta.url), "utf8"), readFile(this.options.packagePath ?? fileURLToPath(new URL("../../dist/remote-host.json", import.meta.url)), "utf8").catch(() => {
71
+ throw new Error("Build the SSH host package with npm run build:host, then connect again.");
72
+ })]);
73
+ if (payload.length > 32 * 1024 * 1024) throw new Error("Remote host package exceeds 32 MiB");
74
+ const transport = new SshTransport(host.sshHost, this.options.sshBinary, host);
75
+ await transport.init();
76
+ try {
77
+ const runtime = parseRuntime(await transport.bootstrap(code, remotePort === void 0 ? payload : JSON.stringify({ ...JSON.parse(payload), remotePort, serviceBootstrap: code }), remotePort !== void 0));
78
+ return { transport, runtime, currentVersion: runtime.version === JSON.parse(payload).version };
79
+ } catch (error) {
80
+ await transport.close();
81
+ throw error;
82
+ }
83
+ }
84
+ async connect(id) {
85
+ await this.load();
86
+ if (this.closed) throw new HttpError(503, "The local app is shutting down");
87
+ const host = this.hosts.find((item) => item.id === id);
88
+ if (!host) throw new HttpError(404, "SSH connection not found");
89
+ let session = this.sessions.get(id);
90
+ if (session) await session.gateway.ensureConnected();
91
+ if (!session) {
92
+ let pending = this.pending.get(id);
93
+ if (!pending) {
94
+ if (this.sessions.size + this.pending.size >= 8) throw new HttpError(409, "Disconnect an unused server first (8 active connections maximum)");
95
+ pending = (async () => {
96
+ const initial = await this.setup(host);
97
+ const state = { transport: initial.transport, gateway: void 0 };
98
+ let reconnecting;
99
+ const gateway = new SshGateway(initial.transport, initial.runtime, () => reconnecting ??= (async () => {
100
+ const next = await this.setup(host);
101
+ const previous = state.transport;
102
+ state.transport = next.transport;
103
+ await previous.close();
104
+ return next;
105
+ })().finally(() => {
106
+ reconnecting = void 0;
107
+ }));
108
+ state.gateway = gateway;
109
+ try {
110
+ if (this.closed) throw new Error("The local app is shutting down");
111
+ this.sessions.set(id, state);
112
+ this.directoryCache = void 0;
113
+ return state;
114
+ } catch (error) {
115
+ await gateway.close();
116
+ await state.transport.close();
117
+ throw error;
118
+ }
119
+ })();
120
+ this.pending.set(id, pending);
121
+ void pending.finally(() => this.pending.delete(id)).catch(() => void 0);
122
+ }
123
+ session = await pending;
124
+ }
125
+ return { url: `/computer/${id}/` };
126
+ }
127
+ /** Install an independent host. The workspace stays private and keeps running. */
128
+ async setupTeamServer(id, body) {
129
+ this.assertBrowse();
130
+ await this.load();
131
+ if (this.teamRemoving.has(id)) throw new HttpError(409, "Wait for team server deletion to finish");
132
+ if (this.closed) throw new HttpError(503, "The local app is shutting down");
133
+ const host = this.hosts.find((item) => item.id === id);
134
+ if (!host) throw new HttpError(404, "SSH connection not found");
135
+ const workspace = this.sessions.get(id);
136
+ if (!workspace || workspace.gateway.error) throw new HttpError(409, "Connect to this server in SSH connections first");
137
+ const remotePort = body.remotePort ?? 4318;
138
+ if (!Number.isInteger(remotePort) || Number(remotePort) < 1 || Number(remotePort) > 65535) throw new HttpError(400, "Choose a team port from 1 to 65535");
139
+ let pending = this.teamPending.get(id);
140
+ if (!pending) {
141
+ pending = (async () => {
142
+ const initial = await this.setup(host, Number(remotePort));
143
+ const state = { transport: initial.transport, gateway: void 0 };
144
+ state.gateway = new SshGateway(initial.transport, initial.runtime, async () => {
145
+ const next = await this.setup(host, initial.runtime.remotePort ?? Number(remotePort));
146
+ const previous = state.transport;
147
+ state.transport = next.transport;
148
+ await previous.close();
149
+ return next;
150
+ }, "/api/team-host-runtime");
151
+ try {
152
+ this.assertBrowse();
153
+ if (this.closed || !this.sessions.has(id)) throw new HttpError(409, "SSH disconnected during setup. Reconnect and resume; the team server stays installed.");
154
+ const previous = this.teamSessions.get(id);
155
+ this.teamSessions.set(id, state);
156
+ if (previous) {
157
+ await previous.gateway.close();
158
+ await previous.transport.close();
159
+ }
160
+ return { remotePort: initial.runtime.remotePort ?? Number(remotePort), currentVersion: initial.currentVersion };
161
+ } catch (error) {
162
+ await state.gateway.close();
163
+ await state.transport.close();
164
+ throw error;
165
+ }
166
+ })();
167
+ this.teamPending.set(id, pending);
168
+ void pending.finally(() => this.teamPending.delete(id)).catch(() => void 0);
169
+ }
170
+ return pending;
171
+ }
172
+ async teamAutostart(id, action = "status") {
173
+ if (this.teamRemoving.has(id)) throw new HttpError(409, "Wait for team server deletion to finish");
174
+ this.assertBrowse();
175
+ if (!["status", "enable", "disable"].includes(action)) throw new HttpError(400, "Choose status, enable or disable for automatic startup");
176
+ if (this.teamPending.has(id)) throw new HttpError(409, "Wait for team server setup to finish");
177
+ const session = this.teamSessions.get(id);
178
+ if (!session || !this.sessions.has(id)) throw new HttpError(409, "Open team server setup on this SSH connection first");
179
+ const code = await readFile(new URL("./team-autostart.mjs", import.meta.url), "utf8");
180
+ const text = await session.transport.bootstrap(code + "\nawait main();", JSON.stringify({ action }));
181
+ const result = JSON.parse(text.split(/\r?\n/).find((line) => line.startsWith("CODEFLOW_AUTOSTART "))?.slice(19) ?? "null");
182
+ if (!result || typeof result.enabled !== "boolean" || typeof result.supported !== "boolean" || typeof result.message !== "string") throw new HttpError(502, "The server returned an invalid automatic startup status");
183
+ return result;
184
+ }
185
+ async teamServerStatus(id) {
186
+ this.assertBrowse();
187
+ await this.load();
188
+ if (!this.hosts.some((host) => host.id === id)) throw new HttpError(404, "SSH connection not found");
189
+ const session = this.sessions.get(id);
190
+ if (!session || session.gateway.error) throw new HttpError(409, "Connect to this server to check its team server");
191
+ const code = await readFile(new URL("./team-status.mjs", import.meta.url), "utf8");
192
+ const text = await session.transport.bootstrap(code, "");
193
+ const result = JSON.parse(text.split(/\r?\n/).find((line) => line.startsWith("CODEFLOW_TEAM_STATUS "))?.slice(21) ?? "null");
194
+ if (!result || !["running", "not-installed", "unavailable"].includes(result.state)) throw new HttpError(502, "Could not read the team server status");
195
+ return result;
196
+ }
197
+ async removeTeamServer(id) {
198
+ this.assertBrowse();
199
+ await this.load();
200
+ if (this.teamPending.has(id)) throw new HttpError(409, "Wait for team server setup to finish");
201
+ const session = this.sessions.get(id);
202
+ if (!session || session.gateway.error) throw new HttpError(409, "Connect to this server before deleting its team server");
203
+ if (this.teamRemoving.has(id)) throw new HttpError(409, "Team server deletion is already in progress");
204
+ this.teamRemoving.add(id);
205
+ try {
206
+ const code = await readFile(new URL("./team-remove.mjs", import.meta.url), "utf8");
207
+ const text = await session.transport.bootstrap(code + '\nconsole.log("CODEFLOW_TEAM_REMOVED " + JSON.stringify(await removeTeamServer()));', "");
208
+ const result = JSON.parse(text.split(/\r?\n/).find((line) => line.startsWith("CODEFLOW_TEAM_REMOVED "))?.slice(22) ?? "null");
209
+ if (!result?.removed) throw new HttpError(502, "Team server deletion could not be verified");
210
+ const previous = this.teamSessions.get(id);
211
+ this.teamSessions.delete(id);
212
+ if (previous) {
213
+ await previous.gateway.close();
214
+ await previous.transport.close();
215
+ }
216
+ return { removed: true };
217
+ } finally {
218
+ this.teamRemoving.delete(id);
219
+ }
220
+ }
221
+ async forwardTeamServer(id, req, res, path, origin) {
222
+ this.assertBrowse();
223
+ if (this.teamRemoving.has(id)) throw new HttpError(409, "Wait for team server deletion to finish");
224
+ const url = new URL(path, origin), method = req.method ?? "GET";
225
+ const allowed = method === "GET" && /^\/api\/(projects|accounts|remote|team\/board)$/.test(url.pathname) || method === "POST" && /^\/api\/(projects|accounts|remote(?:\/tls)?)$/.test(url.pathname) || method === "PUT" && /^\/api\/(accounts\/[^/]+|team\/profile|team\/projects\/[^/]+\/repositories)$/.test(url.pathname);
226
+ if (!allowed) throw new HttpError(404, "Unknown team server setup operation");
227
+ if (url.pathname.startsWith("/api/team/")) url.searchParams.set("teamConnection", "local");
228
+ const session = this.teamSessions.get(id);
229
+ if (!session || !this.sessions.has(id)) throw new HttpError(409, "Open team server setup on this SSH connection first");
230
+ const body = method === "GET" ? void 0 : await readJson(req, 192 * 1024);
231
+ return session.gateway.forward(req, res, url.pathname + url.search, origin, body);
232
+ }
233
+ async stopTeamServer(id) {
234
+ this.assertBrowse();
235
+ if (this.teamPending.has(id)) throw new HttpError(409, "Wait for team server setup to finish");
236
+ const session = this.teamSessions.get(id);
237
+ if (!session) throw new HttpError(409, "Open team server setup before stopping it");
238
+ await session.gateway.json("/api/team-host-runtime/stop", "POST", {});
239
+ this.teamSessions.delete(id);
240
+ await session.gateway.close();
241
+ await session.transport.close();
242
+ }
243
+ directoryPending;
244
+ directoryCache;
245
+ assertBrowse() {
246
+ if (this.options.canBrowse && !this.options.canBrowse()) throw new HttpError(409, "Filesystem restrictions pause connected projects");
247
+ }
248
+ async projectDirectory() {
249
+ this.assertBrowse();
250
+ await this.load();
251
+ let remotes = this.directoryCache && this.directoryCache.until > Date.now() ? this.directoryCache.value : void 0;
252
+ if (!remotes) {
253
+ this.directoryPending ??= Promise.all([...this.sessions].map(async ([id, session]) => {
254
+ const host = this.hosts.find((host2) => host2.id === id);
255
+ try {
256
+ const rows = await session.gateway.json("/api/projects", "GET", void 0, 5e3);
257
+ if (!Array.isArray(rows) || rows.some((p) => !p || typeof p.id !== "string" || typeof p.name !== "string")) throw new Error("Invalid project list");
258
+ return { id, name: host.name, projects: rows.map(({ id: id2, name }) => ({ id: id2, name })) };
259
+ } catch {
260
+ return { id, name: host.name, projects: [], error: "Connection unavailable" };
261
+ }
262
+ })).finally(() => {
263
+ this.directoryPending = void 0;
264
+ });
265
+ remotes = await this.directoryPending;
266
+ this.directoryCache = { until: Date.now() + 5e3, value: remotes };
267
+ }
268
+ this.assertBrowse();
269
+ return [{ id: "local", name: "This computer", projects: (this.options.localProjects?.() ?? []).map(({ id, name }) => ({ id, name })) }, ...remotes.filter((host) => this.sessions.has(host.id)).sort((a, b) => this.hosts.findIndex((host) => host.id === a.id) - this.hosts.findIndex((host) => host.id === b.id))];
270
+ }
271
+ async context(id) {
272
+ this.assertBrowse();
273
+ await this.load();
274
+ const host = this.hosts.find((host2) => host2.id === id);
275
+ if (!host) throw new HttpError(404, "Computer not found");
276
+ const session = this.sessions.get(id);
277
+ return { kind: "ssh", id, name: host.name, connected: !!session, error: session?.gateway.error };
278
+ }
279
+ async forward(id, req, res, path, origin) {
280
+ this.assertBrowse();
281
+ const session = this.sessions.get(id);
282
+ if (!session) throw new HttpError(409, "This computer is disconnected. Reconnect it in Settings.");
283
+ return session.gateway.forward(req, res, path, origin);
284
+ }
285
+ async disconnect(id) {
286
+ await this.teamPending.get(id)?.catch(() => void 0);
287
+ const team = this.teamSessions.get(id);
288
+ this.teamSessions.delete(id);
289
+ if (team) {
290
+ await team.gateway.close();
291
+ await team.transport.close();
292
+ }
293
+ const session = this.sessions.get(id) ?? await this.pending.get(id)?.catch(() => void 0);
294
+ this.sessions.delete(id);
295
+ this.directoryCache = void 0;
296
+ if (session) {
297
+ await session.gateway.close();
298
+ await session.transport.close();
299
+ }
300
+ }
301
+ async remove(id) {
302
+ await this.disconnect(id);
303
+ await this.change(() => {
304
+ this.hosts = this.hosts.filter((host) => host.id !== id);
305
+ });
306
+ }
307
+ async stop(id) {
308
+ const session = this.sessions.get(id);
309
+ if (!session) throw new HttpError(409, "Connect to this server before stopping its backend");
310
+ await session.gateway.json("/api/ssh-runtime/stop", "POST", {});
311
+ await this.disconnect(id);
312
+ }
313
+ async close() {
314
+ this.closed = true;
315
+ await Promise.allSettled([.../* @__PURE__ */ new Set([...this.sessions.keys(), ...this.pending.keys()])].map((id) => this.disconnect(id)));
316
+ }
317
+ }
318
+ export {
319
+ SshProjects
320
+ };
@@ -0,0 +1,93 @@
1
+ // Fixed application code sent over SSH. User input selects only a named lifecycle action.
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ const unit = 'codeflow-team-server.service';
9
+ const marker = '# Managed by Code Flow team server setup';
10
+ const execute = promisify(execFile);
11
+ const quoteUnit = (value, command = false) => {
12
+ if (/[\x00-\x1f\x7f]/.test(value)) throw new Error('The service path contains unsupported control characters');
13
+ return '"' + value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', () => command ? '$$' : '$') + '"';
14
+ };
15
+ export function serviceUnit(root, node, path) {
16
+ return `${marker}
17
+ [Unit]
18
+ Description=Code Flow team server
19
+ StartLimitIntervalSec=0
20
+
21
+ [Service]
22
+ Type=simple
23
+ ExecStart=${quoteUnit(node, true)} ${quoteUnit(join(root, 'service-bootstrap.mjs'), true)} --team-server --service
24
+ WorkingDirectory=${quoteUnit(root)}
25
+ Environment=${quoteUnit('PATH=' + path)}
26
+ Restart=on-failure
27
+ RestartSec=10s
28
+ KillMode=mixed
29
+ TimeoutStopSec=45s
30
+ UMask=0077
31
+
32
+ [Install]
33
+ WantedBy=default.target
34
+ `;
35
+ }
36
+
37
+ export async function configureAutostart(action, options = {}) {
38
+ if (!['status', 'enable', 'disable'].includes(action)) throw new Error('Unknown automatic startup action');
39
+ const home = options.home ?? homedir(), platform = options.platform ?? process.platform;
40
+ const uid = String(options.uid ?? process.getuid?.());
41
+ const root = join(home, '.codeflow', 'team-server');
42
+ const path = options.path ?? process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin';
43
+ const node = options.node ?? process.execPath;
44
+ const run = options.run ?? (async (file, args) => {
45
+ const runtime = process.env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
46
+ return (await execute(file, args, { timeout: 10000, maxBuffer: 16384, env: { ...process.env, XDG_RUNTIME_DIR: runtime, DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS || `unix:path=${runtime}/bus` } })).stdout.trim();
47
+ });
48
+ if (platform !== 'linux') return { supported: false, enabled: false, message: 'Automatic startup currently supports Linux servers with systemd. This server can still be started from setup.' };
49
+ const systemctl = (...args) => run('systemctl', ['--user', '--no-ask-password', ...args]);
50
+ try { await systemctl('show-environment'); }
51
+ catch { return { supported: false, enabled: false, message: 'The systemd user service manager is unavailable. Ask the server administrator to enable systemd user services for this SSH account, then reconnect and check again.' }; }
52
+ const unitPath = join(home, '.config', 'systemd', 'user', unit);
53
+ const existing = await readFile(unitPath, 'utf8').catch(error => { if (error.code !== 'ENOENT') throw error; return ''; });
54
+ if (existing && !existing.startsWith(marker + '\n')) throw new Error('A service named codeflow-team-server.service already exists and is not managed by Code Flow. Ask the administrator to resolve the name conflict.');
55
+ let prepared = false;
56
+ try { await readFile(join(root, 'service-bootstrap.mjs')); await readFile(join(root, 'service-package.json')); prepared = true; } catch { /* older installation */ }
57
+ let linger = await run('loginctl', ['show-user', uid, '--property=Linger', '--value']).then(value => value.trim() === 'yes', () => false);
58
+ if (action === 'enable') {
59
+ if (!prepared) throw new Error('Stop the team server and set it up again with this Code Flow version before enabling automatic startup. Saved projects and accounts will be kept.');
60
+ if (!linger) {
61
+ await run('loginctl', ['--no-ask-password', 'enable-linger', uid]).catch(() => undefined);
62
+ linger = await run('loginctl', ['show-user', uid, '--property=Linger', '--value']).then(value => value.trim() === 'yes', () => false);
63
+ if (!linger) return { supported: true, enabled: false, unitEnabled: !!existing && await systemctl("is-enabled", unit).then(value => value.trim() === "enabled", () => false), message: 'An administrator must allow this account’s services to start before login. Run the command on the remote server, then enable automatic startup again.', command: `sudo loginctl enable-linger ${uid}` };
64
+ }
65
+ await mkdir(join(home, '.config', 'systemd', 'user'), { recursive: true, mode: 0o700 });
66
+ const temporary = unitPath + '.tmp';
67
+ await writeFile(temporary, serviceUnit(root, node, path), { mode: 0o600 });
68
+ await rename(temporary, unitPath);
69
+ await systemctl('daemon-reload');
70
+ // Enable for the next boot. Do not interrupt or duplicate the already-running server.
71
+ await systemctl('enable', unit);
72
+ } else if (action === 'disable' && existing) {
73
+ await systemctl('disable', unit);
74
+ // Linger is shared by all of the account's services; leave it in place.
75
+ }
76
+ const installed = !!existing || action === 'enable';
77
+ const unitEnabled = installed && await systemctl('is-enabled', unit).then(value => value.trim() === 'enabled', () => false);
78
+ const enabled = !!unitEnabled && linger && prepared;
79
+ return { supported: true, enabled, unitEnabled: !!unitEnabled, linger, message: enabled
80
+ ? 'Automatic startup is enabled and verified. The team server will start after reboot, without an SSH login.'
81
+ : unitEnabled ? 'The service is enabled, but startup before login is not ready. Enable automatic startup again to repair its configuration.'
82
+ : 'Automatic startup is off. The current team server keeps running; enable this to start it after reboot.' };
83
+ }
84
+
85
+ export async function main() {
86
+ try {
87
+ let text = ''; process.stdin.setEncoding('utf8');
88
+ for await (const chunk of process.stdin) { text += chunk; if (text.length > 8192) throw new Error('Automatic startup request is too large'); }
89
+ const body = JSON.parse(text);
90
+ const result = await configureAutostart(body.action);
91
+ process.stdout.write('CODEFLOW_AUTOSTART ' + JSON.stringify(result) + '\n');
92
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
93
+ }
@@ -0,0 +1,69 @@
1
+ // Fixed SSH lifecycle operation. The only removable directory is this account's
2
+ // dedicated team-server installation; repository paths are never followed.
3
+ import { readFile, lstat, rm, mkdir } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+
9
+ export async function removeTeamServer(options = {}) {
10
+ const home = options.home ?? homedir();
11
+ const root = join(home, ".codeflow", "team-server");
12
+ const exists = async path => lstat(path).catch(error => { if (error.code !== "ENOENT") throw error; return null; });
13
+ for (const path of [join(home, ".codeflow"), root]) {
14
+ const stat = await exists(path);
15
+ if (stat?.isSymbolicLink()) throw new Error("Team server storage is a symbolic link. Remove it manually after checking its target.");
16
+ }
17
+ const unitPath = join(home, ".config/systemd/user/codeflow-team-server.service");
18
+ const unit = await exists(unitPath);
19
+ if (!await exists(root)) {
20
+ if (unit) throw new Error("The startup service exists without team storage. Check it manually before deleting the installation.");
21
+ return { removed: true };
22
+ }
23
+ if (unit && (unit.isSymbolicLink() || !(await readFile(unitPath, "utf8")).startsWith("# Managed by Code Flow team server setup\n"))) throw new Error("The startup service is not managed by Code Flow. No team data was deleted.");
24
+ // Share bootstrap's atomic lock so setup cannot launch a backend during deletion.
25
+ const bootId = process.platform === "linux" ? await readFile("/proc/sys/kernel/random/boot_id", "utf8").then(value => value.trim()).catch(() => undefined) : undefined;
26
+ const lock = join(root, bootId ? `bootstrap.${bootId}.lock` : "bootstrap.lock");
27
+ try { await mkdir(lock, { mode: 0o700 }); }
28
+ catch (error) {
29
+ if (error.code === "EEXIST") throw new Error("Team server setup or deletion is already running. Wait, then retry.");
30
+ throw error;
31
+ }
32
+ try {
33
+ const runtime = JSON.parse(await readFile(join(root, "runtime.json"), "utf8"));
34
+ if (runtime?.protocol !== 1 || !Number.isInteger(runtime.port) || runtime.port < 1 || runtime.port > 65535
35
+ || !Number.isInteger(runtime.pid) || runtime.pid <= 0 || !/^[a-f0-9]{64}$/.test(runtime.token ?? "")
36
+ || !/^[a-f0-9]{32}$/.test(runtime.instance ?? "")) throw new Error("Cannot verify the team server process. No team data was deleted.");
37
+ const request = options.request ?? fetch;
38
+ const get = (path, method = "GET") => request(`http://127.0.0.1:${runtime.port}${path}`, {
39
+ method, headers: { authorization: `Bearer ${runtime.token}` }, signal: AbortSignal.timeout(5000), redirect: "error",
40
+ });
41
+ let identity;
42
+ try { const response = await get("/api/team-host-runtime"); if (response.ok) identity = await response.json(); } catch { /* A stopped installation can be removed once its PID is gone. */ }
43
+ if (identity) {
44
+ if (identity.instance !== runtime.instance) throw new Error("The server identity changed. No team data was deleted.");
45
+ const stopped = await get("/api/team-host-runtime/stop", "POST");
46
+ if (!stopped.ok) throw new Error("Finish running team work and close terminals before deleting the team server.");
47
+ }
48
+ const alive = options.alive ?? (pid => { try { process.kill(pid, 0); return true; } catch (error) { return error.code !== "ESRCH"; } });
49
+ // Do not force-kill a process, including a recycled PID.
50
+ for (let attempt = 0; alive(runtime.pid); attempt++) {
51
+ if (attempt >= 50) throw new Error("The team server is still shutting down. Wait, then retry deletion. No team data was deleted.");
52
+ await new Promise(resolve => setTimeout(resolve, 100));
53
+ }
54
+ if (unit) {
55
+ const execute = promisify(execFile);
56
+ const run = options.run ?? (async args => {
57
+ const uid = process.getuid?.();
58
+ const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
59
+ await execute("systemctl", ["--user", "--no-ask-password", ...args], { timeout: 10000, env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir, DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS || `unix:path=${runtimeDir}/bus` } });
60
+ });
61
+ await run(["disable", "--now", "codeflow-team-server.service"]);
62
+ await rm(unitPath);
63
+ await run(["daemon-reload"]);
64
+ }
65
+ await rm(root, { recursive: true });
66
+ } finally { await rm(lock, { recursive: true, force: true }); }
67
+
68
+ return { removed: true };
69
+ }