@fungi.computer/bonsai 0.0.0-stage → 0.0.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 (132) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -2
  4. package/backend/background.ts +259 -0
  5. package/backend/change-copy.ts +99 -0
  6. package/backend/changes.ts +530 -0
  7. package/backend/diff.ts +228 -0
  8. package/backend/file-edits.ts +850 -0
  9. package/backend/files-copy.ts +361 -0
  10. package/backend/gills.ts +125 -0
  11. package/backend/git.ts +123 -0
  12. package/backend/graph-commit-copies.ts +321 -0
  13. package/backend/history.ts +178 -0
  14. package/backend/index.ts +144 -0
  15. package/backend/insights.ts +256 -0
  16. package/backend/merge.ts +999 -0
  17. package/backend/methods.ts +835 -0
  18. package/backend/policy.ts +160 -0
  19. package/backend/read-copy-page.ts +77 -0
  20. package/backend/repository-copy.ts +70 -0
  21. package/backend/reviews.ts +317 -0
  22. package/backend/service.ts +892 -0
  23. package/backend/state.ts +914 -0
  24. package/demo/koi-pond/README.md +17 -0
  25. package/demo/koi-pond/index.html +11 -0
  26. package/demo/koi-pond/package.json +13 -0
  27. package/demo/koi-pond/pnpm-lock.yaml +450 -0
  28. package/demo/koi-pond/src/fish.ts +32 -0
  29. package/demo/koi-pond/src/main.ts +24 -0
  30. package/demo/koi-pond/src/pond.ts +27 -0
  31. package/docs/media/bonsai-insights.png +0 -0
  32. package/docs/media/overview-phone.png +0 -0
  33. package/docs/media/overview.png +0 -0
  34. package/package.json +152 -3
  35. package/scripts/agent.sh +55 -0
  36. package/scripts/demo.sh +233 -0
  37. package/scripts/rebuild-copies.mjs +34 -0
  38. package/scripts/release.mjs +52 -0
  39. package/scripts/runtime-proof.mjs +299 -0
  40. package/scripts/seed-demo.mjs +724 -0
  41. package/scripts/seed-step.mjs +37 -0
  42. package/scripts/shed-terminal-proof.mjs +774 -0
  43. package/shared/contract.ts +404 -0
  44. package/shared/hooks.ts +208 -0
  45. package/shared/insights.ts +90 -0
  46. package/shared/picker-options.ts +23 -0
  47. package/shared/presentation.ts +21 -0
  48. package/shared/repository-deletion.ts +4 -0
  49. package/test/apps-build-install.test.ts +302 -0
  50. package/test/background-runtime.test.ts +206 -0
  51. package/test/browser/board.browser.mjs +52 -0
  52. package/test/browser/capture.browser.mjs +1264 -0
  53. package/test/browser/edit-in-inkcap.browser.mjs +83 -0
  54. package/test/browser/repository-settings-fixture.tsx +108 -0
  55. package/test/browser/repository-settings.browser.mjs +136 -0
  56. package/test/browser/repository-settings.html +10 -0
  57. package/test/browser/visual-fixture.tsx +667 -0
  58. package/test/browser/visual.html +10 -0
  59. package/test/browser/vite.config.mjs +9 -0
  60. package/test/change-page.test.ts +916 -0
  61. package/test/changes.test.ts +1063 -0
  62. package/test/checks-workflow-template.test.mjs +234 -0
  63. package/test/diff.test.ts +70 -0
  64. package/test/edit-in-inkcap.test.ts +227 -0
  65. package/test/file-edits.test.ts +2714 -0
  66. package/test/files-read-copy-calls.test.ts +374 -0
  67. package/test/fork-adoption.test.ts +111 -0
  68. package/test/gills.test.ts +125 -0
  69. package/test/git-binding.fixture.ts +30 -0
  70. package/test/git-push.config.jsonc +9 -0
  71. package/test/git-push.native.mjs +413 -0
  72. package/test/git-push.worker.ts +280 -0
  73. package/test/graph-commit-read-copy-calls.test.ts +401 -0
  74. package/test/home-page.test.ts +170 -0
  75. package/test/hooks.test.ts +250 -0
  76. package/test/insight-data.test.ts +51 -0
  77. package/test/insight-layout.test.mjs +230 -0
  78. package/test/insights.test.ts +154 -0
  79. package/test/merge-background.test.ts +756 -0
  80. package/test/merge-conflict.test.ts +337 -0
  81. package/test/merge-declaration.test.ts +46 -0
  82. package/test/merge-repro.test.ts +561 -0
  83. package/test/merge-template-build.test.mjs +234 -0
  84. package/test/network-layout.test.ts +200 -0
  85. package/test/outbox.test.ts +424 -0
  86. package/test/parts.test.ts +86 -0
  87. package/test/pkt-line.test.ts +54 -0
  88. package/test/policy.test.ts +172 -0
  89. package/test/read-copy-calls.test.ts +691 -0
  90. package/test/read-copy-composer.test.ts +388 -0
  91. package/test/read-copy-lifetimes.test.ts +214 -0
  92. package/test/read-copy-rpc.config.jsonc +12 -0
  93. package/test/read-copy-rpc.native.mjs +47 -0
  94. package/test/read-copy-rpc.worker.ts +92 -0
  95. package/test/recent-activity.test.ts +62 -0
  96. package/test/repository-deletion.test.ts +500 -0
  97. package/test/seed-resume.test.mjs +91 -0
  98. package/test/storyboard-save-https-git.fixture.mjs +119 -0
  99. package/test/storyboard-save-template.test.mjs +545 -0
  100. package/test/ui-errors.test.ts +976 -0
  101. package/test/workflow-store.test.ts +115 -0
  102. package/tsconfig.json +36 -0
  103. package/turbo.jsonc +25 -0
  104. package/vite.config.ts +19 -0
  105. package/vitest.config.ts +5 -0
  106. package/web/api.ts +224 -0
  107. package/web/app-build-panel.tsx +171 -0
  108. package/web/board.tsx +259 -0
  109. package/web/bonsai.css +642 -0
  110. package/web/change-page.tsx +715 -0
  111. package/web/change-rows.tsx +135 -0
  112. package/web/code-tab.tsx +267 -0
  113. package/web/commit-tree.tsx +152 -0
  114. package/web/format.ts +54 -0
  115. package/web/home-page.tsx +274 -0
  116. package/web/index.html +17 -0
  117. package/web/insight-data.ts +76 -0
  118. package/web/insight-tiles.tsx +534 -0
  119. package/web/insights.tsx +250 -0
  120. package/web/issues-panel.tsx +305 -0
  121. package/web/main.tsx +72 -0
  122. package/web/network-graph.tsx +647 -0
  123. package/web/overview.tsx +339 -0
  124. package/web/pages.tsx +469 -0
  125. package/web/parts.tsx +381 -0
  126. package/web/public/favicon.svg +1 -0
  127. package/web/repo-navigation.tsx +256 -0
  128. package/web/repo-settings.tsx +145 -0
  129. package/web/router.tsx +151 -0
  130. package/web/transport.ts +91 -0
  131. package/web/views.tsx +151 -0
  132. package/web/working-now.tsx +98 -0
@@ -0,0 +1,91 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { test } from "node:test";
7
+ import { seedDemo } from "../scripts/seed-demo.mjs";
8
+
9
+ test("public seed requests identify the client honestly", async (t) => {
10
+ const work = await mkdtemp(join(tmpdir(), "koi-ua-"));
11
+ t.after(() => rm(work, { recursive: true, force: true }));
12
+ const agentKeysFile = join(work, "keys.json");
13
+ await writeFile(
14
+ agentKeysFile,
15
+ JSON.stringify({ morel: "test", chanterelle: "test", enoki: "test" }),
16
+ { mode: 0o600 },
17
+ );
18
+ let headers;
19
+ const stopped = new Error("request captured");
20
+ t.mock.method(globalThis, "fetch", async (_url, options) => {
21
+ headers = options.headers;
22
+ throw stopped;
23
+ });
24
+ await assert.rejects(
25
+ seedDemo({
26
+ origin: "https://oyster.test",
27
+ work,
28
+ agentKeysFile,
29
+ phase: "agents",
30
+ }),
31
+ stopped,
32
+ );
33
+ assert.equal(headers["User-Agent"], "oyster-koi-seed/1.0");
34
+ });
35
+
36
+ test("resume skips two pushed steps without consuming the staged third edit", async (t) => {
37
+ const { pushSeedStep } = await import("../scripts/seed-step.mjs");
38
+ const work = await mkdtemp(join(tmpdir(), "koi-resume-"));
39
+ t.after(() => rm(work, { recursive: true, force: true }));
40
+ const remote = join(work, ".git", "seed-remote.git");
41
+ const commands = [];
42
+ const git = async (args) => {
43
+ commands.push(args);
44
+ return execFileSync("git", args, {
45
+ cwd: work,
46
+ encoding: "utf8",
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ }).trim();
49
+ };
50
+ await git(["init", "-q", "-b", "main"]);
51
+ await git(["config", "user.name", "Morel"]);
52
+ await git(["config", "user.email", "morel@test.local"]);
53
+ await writeFile(join(work, "README.md"), "base\n");
54
+ await git(["add", "README.md"]);
55
+ await git(["commit", "-qm", "base"]);
56
+ const baseHead = await git(["rev-parse", "HEAD"]);
57
+ await git(["init", "--bare", "-q", remote]);
58
+ for (const message of ["palette", "ripples"]) {
59
+ await writeFile(join(work, "README.md"), message + "\n");
60
+ await git(["add", "README.md"]);
61
+ await git(["commit", "-qm", message]);
62
+ }
63
+ await git(["push", "-q", remote, "main"]);
64
+ const secondHead = await git(["rev-parse", "HEAD"]);
65
+ await writeFile(join(work, "README.md"), "third pending documentation\n");
66
+ await git(["add", "README.md"]);
67
+ commands.length = 0;
68
+ const options = {
69
+ git,
70
+ baseHead,
71
+ remote,
72
+ lastPushAt: Date.now(),
73
+ pushIntervalMinutes: 0.002,
74
+ };
75
+ assert.equal(await pushSeedStep({ ...options, message: "palette" }), null);
76
+ assert.equal(await pushSeedStep({ ...options, message: "ripples" }), null);
77
+ assert.equal(await git(["rev-parse", "HEAD"]), secondHead);
78
+ assert.equal(await git(["diff", "--cached", "--name-only"]), "README.md");
79
+ assert.equal(commands.filter(([cmd]) => cmd === "push").length, 0);
80
+ const third = await pushSeedStep({ ...options, message: "documentation" });
81
+ assert.ok(
82
+ third.at >= options.lastPushAt + 120,
83
+ "remaining real gap must elapse",
84
+ );
85
+ assert.equal(await git(["log", "-1", "--format=%s"]), "documentation");
86
+ assert.equal(
87
+ await pushSeedStep({ ...options, message: "documentation" }),
88
+ null,
89
+ );
90
+ assert.equal(commands.filter(([cmd]) => cmd === "push").length, 1);
91
+ });
@@ -0,0 +1,119 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { once } from "node:events";
4
+ import { createServer } from "node:https";
5
+ import { mkdtemp, readFile } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import process from "node:process";
9
+ import { URL } from "node:url";
10
+
11
+ /**
12
+ * A real git smart-HTTP server over real TLS (self-signed), proxying to the
13
+ * maintained `git http-backend` CGI program against a directory of real bare
14
+ * repositories. workflowGitPassResultSchema requires an `https:` remote
15
+ * (packages/agar/src/computer-contracts.ts); this gives Shed's real `run`
16
+ * handler and git's real client something genuine to fetch and push against,
17
+ * rather than a stand-in local path.
18
+ */
19
+ export async function startHttpsGitServer(reposRoot) {
20
+ const certDir = await mkdtemp(join(tmpdir(), "storyboard-save-tls-"));
21
+ const keyPath = join(certDir, "key.pem");
22
+ const certPath = join(certDir, "cert.pem");
23
+ execFileSync(
24
+ "openssl",
25
+ [
26
+ "req",
27
+ "-x509",
28
+ "-newkey",
29
+ "rsa:2048",
30
+ "-keyout",
31
+ keyPath,
32
+ "-out",
33
+ certPath,
34
+ "-days",
35
+ "1",
36
+ "-nodes",
37
+ "-subj",
38
+ "/CN=127.0.0.1",
39
+ ],
40
+ { stdio: ["ignore", "pipe", "pipe"] },
41
+ );
42
+ const key = await readFile(keyPath);
43
+ const cert = await readFile(certPath);
44
+ function cgi(request, response) {
45
+ const url = new URL(request.url, "https://localhost");
46
+ const child = spawn("git", ["http-backend"], {
47
+ env: {
48
+ ...process.env,
49
+ GIT_PROJECT_ROOT: reposRoot,
50
+ GIT_HTTP_EXPORT_ALL: "1",
51
+ PATH_INFO: url.pathname,
52
+ QUERY_STRING: url.search.replace(/^\?/u, ""),
53
+ REQUEST_METHOD: request.method ?? "GET",
54
+ CONTENT_TYPE: request.headers["content-type"] ?? "",
55
+ CONTENT_LENGTH: request.headers["content-length"] ?? "0",
56
+ SERVER_PROTOCOL: "HTTP/1.1",
57
+ GATEWAY_INTERFACE: "CGI/1.1",
58
+ REMOTE_ADDR: "127.0.0.1",
59
+ },
60
+ });
61
+ request.pipe(child.stdin);
62
+ let headerBuffer = Buffer.alloc(0);
63
+ let headersSent = false;
64
+ child.stdout.on("data", (chunk) => {
65
+ if (headersSent) {
66
+ response.write(chunk);
67
+ return;
68
+ }
69
+ headerBuffer = Buffer.concat([headerBuffer, chunk]);
70
+ const crlf = headerBuffer.indexOf("\r\n\r\n");
71
+ const lf = headerBuffer.indexOf("\n\n");
72
+ const separatorAt = crlf !== -1 ? crlf : lf;
73
+ const separatorLength = crlf !== -1 ? 4 : 2;
74
+ if (separatorAt === -1) return;
75
+ const headerText = headerBuffer.slice(0, separatorAt).toString("utf8");
76
+ const body = headerBuffer.slice(separatorAt + separatorLength);
77
+ let status = 200;
78
+ const headers = {};
79
+ for (const line of headerText.split(/\r?\n/u)) {
80
+ const colon = line.indexOf(":");
81
+ if (colon === -1) continue;
82
+ const name = line.slice(0, colon).trim();
83
+ const value = line.slice(colon + 1).trim();
84
+ if (name.toLowerCase() === "status")
85
+ status = parseInt(value, 10) || 200;
86
+ else headers[name] = value;
87
+ }
88
+ response.writeHead(status, headers);
89
+ headersSent = true;
90
+ if (body.length > 0) response.write(body);
91
+ });
92
+ child.on("error", (cause) => {
93
+ response.destroy(cause);
94
+ });
95
+ child.on("close", () => {
96
+ if (!headersSent) response.writeHead(502);
97
+ response.end();
98
+ });
99
+ }
100
+ const server = createServer({ key, cert }, cgi);
101
+ server.listen(0, "127.0.0.1");
102
+ await once(server, "listening");
103
+ const { port } = server.address();
104
+ return {
105
+ origin: `https://127.0.0.1:${port}`,
106
+ close: () =>
107
+ new Promise((resolve) => {
108
+ server.closeAllConnections();
109
+ server.close(() => resolve());
110
+ }),
111
+ };
112
+ }
113
+
114
+ export function initBareRepo(reposRoot, name) {
115
+ const dir = join(reposRoot, `${name}.git`);
116
+ execFileSync("git", ["init", "--bare", "-q", dir]);
117
+ execFileSync("git", ["-C", dir, "config", "http.receivepack", "true"]);
118
+ return dir;
119
+ }