babavoss 0.0.1 → 0.12.0

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 (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
package/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // babavoss: the framework. What a baba imports.
2
+ export { baba, effect, source, accept, resource, shape, manifestOf, isBaba, isSystem, nameOf, SYSTEM } from "./src/ecs/baba.ts";
3
+ export { system, declare, part } from "./src/ecs/system.ts";
4
+ export type { Decl, Entry, Handles, HandlesOf, Building, Maker, Part, Piece, Maybe, StepPiece, OnPiece, ActionPiece, QueryPiece, BindingOf, Scope, Reader, StateOf, ReaderOf, Act, Read } from "./src/ecs/system.ts";
5
+ export { not, added, changed, removed, select } from "./src/ecs/state.ts";
6
+ export type { AcceptDef, System, SystemData, Contract, ActOf, ReadOf, Refs, Baba, AppSpec, RoutesOf, Step, ActionDef, ResourceDef, EffectDef, QueryDef, SkillDef, Manifest, Call, Calls, Shape, SchemaOf, Of } from "./src/ecs/baba.ts";
7
+ export { route, matchRoute, type RouteParams, type RouteMatch } from "./src/desktop/routes.ts";
8
+ export { State, StateError } from "./src/ecs/state.ts";
9
+ export type { Entity, Component, Ask, Resource, Effect, Value, Args, Result, EventOf, MirrorEvent, Row, Values, Done, Failed, Job, Snapshot, Model, Not, Change, Select, Term } from "./src/ecs/state.ts";
10
+ export type { Delta, StateAnswer } from "./src/ecs/snapshot.ts";
11
+ export { applyDelta, isDelta } from "./src/ecs/snapshot.ts";
12
+ export { s, SchemaError, OPTIONAL } from "./src/kernel/schema.ts";
13
+ // Optional and ObjectOf are named by the types a system or spec exports: a baba that installs babavoss must be able to name them too.
14
+ export type { Schema, Infer, Optional, ObjectOf, Json } from "./src/kernel/schema.ts";
15
+ export type { Context, Fs, Http, Exec, ExecOptions, ExecEvent, Secrets, BabaConfig } from "./src/kernel/context.ts";
package/package.json CHANGED
@@ -1,8 +1,54 @@
1
1
  {
2
2
  "name": "babavoss",
3
- "version": "0.0.1",
4
- "description": "Baba Voss: a project's own program, run by voss. Coming soon.",
3
+ "version": "0.12.0",
4
+ "license": "Apache-2.0",
5
+ "repository": "github:alanremarc/babavoss",
5
6
  "homepage": "https://babavoss.org",
6
- "author": "Alan Remarc",
7
- "license": "Apache-2.0"
7
+ "description": "A framework for a project's OS: one application fitted to one project. TypeScript on Bun.",
8
+ "type": "module",
9
+ "bin": {
10
+ "voss": "bin/voss.ts"
11
+ },
12
+ "exports": {
13
+ ".": "./index.ts",
14
+ "./web": "./src/web/index.tsx",
15
+ "./test": "./src/test/index.ts",
16
+ "./bench": "./src/bench/index.ts",
17
+ "./prompt": "./src/prompt/index.ts",
18
+ "./prompt/jsx-runtime": "./src/prompt/jsx-runtime.ts",
19
+ "./prompt/jsx-dev-runtime": "./src/prompt/jsx-dev-runtime.ts",
20
+ "./desktop": "./src/desktop/index.ts",
21
+ "./kit": "./src/kit/index.ts",
22
+ "./spec": "./src/spec/index.ts",
23
+ "./prompt/register": "./src/build/mdx-register.ts"
24
+ },
25
+ "files": [
26
+ "bin",
27
+ "src",
28
+ "gui",
29
+ "index.ts",
30
+ "NOTICE"
31
+ ],
32
+ "engines": {
33
+ "bun": ">=1.4"
34
+ },
35
+ "scripts": {
36
+ "voss": "bun bin/voss.ts",
37
+ "test": "bun test",
38
+ "check": "bunx tsc --noEmit -p ../..",
39
+ "prepack": "bun ../../tools/release/pack.ts prepack",
40
+ "postpack": "bun ../../tools/release/pack.ts postpack"
41
+ },
42
+ "devDependencies": {
43
+ "@types/react-dom": "19"
44
+ },
45
+ "dependencies": {
46
+ "@mdx-js/mdx": "^3.1.1",
47
+ "@types/bun": "^1.4.2",
48
+ "@types/react": "19",
49
+ "react": "19",
50
+ "react-dom": "19",
51
+ "remark-gfm": "^4.0.1",
52
+ "typescript": "^7"
53
+ }
8
54
  }
@@ -0,0 +1,90 @@
1
+ import { bundleProject } from "../build/project.ts";
2
+ // voss baba check: the TypeScript checker over the project, once per
3
+ // config voss wrote (the backend and the views), with a hint under each
4
+ // error that names a guarded global or module.
5
+ import { dirname, join } from "node:path";
6
+ import { viewsConfigPath, writeEditorConfig } from "./config.ts";
7
+ import { advice, forbiddenGlobals, forbiddenModules } from "../build/guard.ts";
8
+ import type { Project } from "./project.ts";
9
+
10
+ // The checker's colors, removed only to read its lines.
11
+ const ansi = /\x1b\[[0-9;]*m/g;
12
+
13
+ // What an error line says when it hits the guard: a guard type's name, or a forbidden module in quotes.
14
+ const hints: [string, string][] = [
15
+ ...Object.values(forbiddenGlobals).map((r) => [`'${r.type}'`, advice(r)] as [string, string]),
16
+ ...Object.values(forbiddenModules).map((r) => [`'${r.type}'`, advice(r)] as [string, string]),
17
+ ...Object.entries(forbiddenModules).map(([spec, r]) => [`"${spec}"`, `${spec}: ${advice(r)}`] as [string, string]),
18
+ ];
19
+
20
+ /** The checker's output with a hint line under each error message that names the guard. */
21
+ export function annotate(out: string): string {
22
+ return out.split("\n").map((line) => {
23
+ const plain = line.replace(ansi, "");
24
+ if (/^\d+ /.test(plain)) return line; // a line of the source under the message
25
+ const hit = hints.find(([needle]) => plain.includes(needle));
26
+ return hit ? `${line}\n hint: ${hit[1]}` : line;
27
+ }).join("\n");
28
+ }
29
+
30
+ /** One diagnostic of the checker's pretty output: its file (empty for a config error) and its text. */
31
+ export interface Diagnostic { file: string; text: string }
32
+
33
+ /** Splits the checker's pretty output into diagnostics, leaving out its closing count. */
34
+ export function diagnostics(out: string): Diagnostic[] {
35
+ const found: Diagnostic[] = [];
36
+ for (const line of out.split("\n")) {
37
+ const plain = line.replace(ansi, "");
38
+ if (/^Found \d+ errors?\b/.test(plain)) break;
39
+ const head = /^(?:(.+?):\d+:\d+ - )?error TS\d+:/.exec(plain);
40
+ if (head) found.push({ file: head[1] ?? "", text: line });
41
+ else if (found.length) found[found.length - 1]!.text += "\n" + line;
42
+ }
43
+ return found.map((d) => ({ file: d.file, text: d.text.replace(/\n+$/, "") }));
44
+ }
45
+
46
+ // The project's own .ts files belong to the backend program, which reports
47
+ // them; the views program sees them through a view's imports and would
48
+ // report the same errors twice.
49
+ const backendFile = (file: string) => /\.[cm]?ts$/.test(file) && !file.endsWith(".d.ts") && !file.includes("node_modules/") && !file.startsWith("..");
50
+
51
+ export async function check(p: Project, stdout: (s: string) => void, stderr: (s: string) => void): Promise<number> {
52
+ const backend = await writeEditorConfig(p);
53
+ // The checker: the project's, else the one the framework carries.
54
+ let tsc: string | null = null;
55
+ for (const from of [p.baba, import.meta.dir]) {
56
+ try { tsc = join(dirname(Bun.resolveSync("typescript/package.json", from)), "bin", "tsc"); break; } catch { /* next */ }
57
+ }
58
+ if (!tsc) { stderr(`voss: no typescript for ${p.baba}; run \`voss baba install\`\n`); return 1; }
59
+ // An editor finds the project's tsconfig.json; views belong to the program it references.
60
+ const root = await Bun.file(join(p.baba, "tsconfig.json")).text().catch(() => "");
61
+ if (!root.includes("tsconfig.views.json")) stderr(`voss: ${p.baba}/tsconfig.json does not reference ./.voss/tsconfig.views.json, so an editor checks views without it; add "references": [{ "path": "./.voss/tsconfig.views.json" }]\n`);
62
+ let failed = false;
63
+ // The one runner of the specs: bun test finds it, and through it every systems/NAME/spec.ts.
64
+ if (!(await Bun.file(join(p.baba, "spec.test.ts")).exists())) { stdout(`specs: .baba/spec.test.ts is missing; it is one line, \`await proveAll(import.meta.dir);\` with proveAll from babavoss/test, so bun test proves every spec\n`); failed = true; }
65
+ for (const [name, config] of [["backend", backend], ["views", viewsConfigPath(p)]] as const) {
66
+ const proc = Bun.spawn([process.execPath, tsc, "--noEmit", "--pretty", "-p", config], { cwd: p.baba, env: { ...process.env, BUN_BE_BUN: "1" }, stdout: "pipe", stderr: "pipe" });
67
+ const [out, err, code] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
68
+ const all = diagnostics(out);
69
+ // The views program reaches the backend's files and the framework's source; each is checked under its own program,
70
+ // except what only a declaration emit finds: a type an editor cannot name (TS2742, TS2883), which the backend program never emits.
71
+ const emitOnly = (d: Diagnostic) => /error TS(2742|2883):/.test(d.text.replace(ansi, ""));
72
+ const found = name === "views" ? all.filter((d) => (!backendFile(d.file) || emitOnly(d)) && !d.file.startsWith("..") && !d.file.includes("node_modules/")) : all;
73
+ if (code === 0 || (all.length > 0 && found.length === 0)) { stdout(`${name}: clean\n`); continue; }
74
+ failed = true;
75
+ if (!found.length) {
76
+ // Output the parser does not know: shown whole.
77
+ stdout(`${name}: failed\n`);
78
+ if (out) stdout(annotate(out));
79
+ } else {
80
+ stdout(`${name}: ${found.length} ${found.length === 1 ? "error" : "errors"}\n\n${found.map((d) => annotate(d.text)).join("\n\n")}\n\n`);
81
+ }
82
+ if (err) stderr(err);
83
+ }
84
+ const mdx = [...new Bun.Glob("promptware/**/*.mdx").scanSync({ cwd: p.baba })];
85
+ if (mdx.length) {
86
+ try { await bundleProject(p); stdout("promptware MDX: clean\n"); }
87
+ catch (error) { failed = true; stderr(`promptware MDX: ${String(error)}\n`); }
88
+ }
89
+ return failed ? 1 : 0;
90
+ }
@@ -0,0 +1,261 @@
1
+ // What voss writes into a project's .baba: the editor config the one-line
2
+ // tsconfig.json extends, the views config and the guard, regenerated on
3
+ // every load and never edited.
4
+ import { join } from "node:path";
5
+ import { existsSync } from "node:fs";
6
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
7
+ import type { Project } from "./project.ts";
8
+ import { advice, forbiddenGlobals, forbiddenModules, type Refusal } from "../build/guard.ts";
9
+
10
+ export const VOSS = ".voss";
11
+
12
+ // Two programs, because TypeScript has one lib per program. By convention
13
+ // a .ts file is backend (the entry, the systems and what they import) and a
14
+ // .tsx file is the view and what it imports: the backend sees ECMAScript
15
+ // and the guard, the view sees ECMAScript and the DOM. Neither sees Bun or
16
+ // Node types. A helper the view shares with nothing backend is written as .tsx.
17
+
18
+ const options = {
19
+ target: "ESNext",
20
+ module: "Preserve",
21
+ moduleResolution: "bundler",
22
+ moduleDetection: "force",
23
+ allowImportingTsExtensions: true,
24
+ verbatimModuleSyntax: true,
25
+ noEmit: true,
26
+ strict: true,
27
+ skipLibCheck: true,
28
+ noUncheckedIndexedAccess: true,
29
+ types: [],
30
+ };
31
+
32
+ /** `@/` is the .baba root: how an app reaches the kit, `@/ui/button`. The views program states it relative to .voss/; the project's own tsconfig.json states it relative to .baba. */
33
+ const alias = { "@/*": ["../*"] };
34
+
35
+ const note = "Written by voss on every load. Edit .baba/tsconfig.json to extend it, not this file.";
36
+
37
+ /** The backend config, .voss/tsconfig.json: ECMAScript, the guard, every .ts of the project. */
38
+ export function editorConfig(): string {
39
+ return JSON.stringify({
40
+ "//": note,
41
+ // JSX is React's unless a file says otherwise: promptware, TSX that renders to Markdown, names its runtime with a pragma.
42
+ // No alias here: the project's tsconfig.json, which extends this, states `@/` relative to itself, and tools that read it resolve it there.
43
+ compilerOptions: { lib: ["ESNext"], ...options, jsx: "react-jsx", jsxImportSource: "react" },
44
+ include: ["guard.d.ts", "assets.d.ts", "../**/*.ts"],
45
+ // Tests run under bun test, with its own globals; they are not backend code.
46
+ exclude: ["../node_modules/**", "../.voss/types/**", "../**/*.test.ts"],
47
+ }, null, 2) + "\n";
48
+ }
49
+
50
+ /**
51
+ * The views config, .voss/tsconfig.views.json: ECMAScript and the DOM, JSX
52
+ * for React, every .tsx of the project. Composite, since the project's
53
+ * tsconfig.json references it: that is how an editor finds the program a
54
+ * view belongs to. Declarations it would emit go under .voss/types.
55
+ */
56
+ export function viewsConfig(): string {
57
+ const { noEmit: _, ...rest } = options;
58
+ // The whole DOM: a view runs in the browser, whose sandbox is its own.
59
+ return JSON.stringify({
60
+ "//": note,
61
+ compilerOptions: { lib: ["ESNext", "DOM", "DOM.Iterable"], ...rest, jsx: "react-jsx", jsxImportSource: "react", paths: alias, composite: true, declaration: true, emitDeclarationOnly: true, rootDir: "..", outDir: "./types/views" },
62
+ // A composite program lists every file it reaches: the backend modules a view imports types from too.
63
+ include: ["assets.d.ts", "../**/*.ts", "../**/*.tsx"],
64
+ exclude: ["../node_modules/**", "../.voss/types/**", "../**/*.test.ts", "../**/*.test.tsx"],
65
+ }, null, 2) + "\n";
66
+ }
67
+
68
+ const doc = (s: string) => `/** ${s} */`;
69
+ const quote = (s: string) => JSON.stringify(s);
70
+
71
+ /**
72
+ * .voss/guard.d.ts: the backend's state. A forbidden global or module has
73
+ * a type whose name and only member say the alternative, so the checker's
74
+ * error names it; the few pure Web APIs the runtime has are declared.
75
+ */
76
+ export function guardDeclarations(): string {
77
+ const types = new Map<string, Refusal>();
78
+ for (const r of [...Object.values(forbiddenGlobals), ...Object.values(forbiddenModules)]) types.set(r.type, r);
79
+ const lines = [
80
+ "// Written by voss on every load: what a baba's backend sees beyond ECMAScript.",
81
+ "// A baba has no OS; an effect's context is its door. See README.md.",
82
+ "",
83
+ ...[...types].map(([name, r]) => `${doc(advice(r))}\ninterface ${name} { readonly ${quote(advice(r))}: never }`),
84
+ "",
85
+ ...Object.entries(forbiddenGlobals).map(([name, r]) => `${doc(`Not in a baba: ${advice(r)}`)}\ndeclare var ${name}: ${r.type};`),
86
+ "",
87
+ ...Object.entries(forbiddenModules).map(([spec, r]) =>
88
+ `${doc(`${spec} is not for a baba: ${advice(r)}`)}\ndeclare module ${quote(spec)} {\n ${doc(advice(r))}\n const denied: ${r.type};\n export = denied;\n}`),
89
+ "",
90
+ pure,
91
+ ];
92
+ return lines.join("\n");
93
+ }
94
+
95
+ // The Web APIs of the runtime that are computation, not the OS. ECMAScript
96
+ // has none of them; the DOM lib would bring fetch and document with them.
97
+ const pure = `interface Console {
98
+ log(...data: unknown[]): void;
99
+ info(...data: unknown[]): void;
100
+ warn(...data: unknown[]): void;
101
+ error(...data: unknown[]): void;
102
+ debug(...data: unknown[]): void;
103
+ }
104
+ declare var console: Console;
105
+
106
+ declare function setTimeout(handler: (...args: unknown[]) => void, timeout?: number, ...args: unknown[]): number;
107
+ declare function clearTimeout(id: number | undefined): void;
108
+ declare function setInterval(handler: (...args: unknown[]) => void, timeout?: number, ...args: unknown[]): number;
109
+ declare function clearInterval(id: number | undefined): void;
110
+ declare function queueMicrotask(callback: () => void): void;
111
+ declare function structuredClone<T>(value: T): T;
112
+ declare function atob(data: string): string;
113
+ declare function btoa(data: string): string;
114
+
115
+ /** Cancellation, as the context's signal: aborted when the caller cancels a call. */
116
+ declare class AbortSignal {
117
+ readonly aborted: boolean;
118
+ readonly reason: unknown;
119
+ throwIfAborted(): void;
120
+ addEventListener(type: "abort", listener: () => void, options?: { once?: boolean }): void;
121
+ removeEventListener(type: "abort", listener: () => void): void;
122
+ static abort(reason?: unknown): AbortSignal;
123
+ static timeout(ms: number): AbortSignal;
124
+ }
125
+ declare class AbortController {
126
+ readonly signal: AbortSignal;
127
+ abort(reason?: unknown): void;
128
+ }
129
+
130
+ declare class TextEncoder {
131
+ readonly encoding: string;
132
+ encode(input?: string): Uint8Array;
133
+ }
134
+ declare class TextDecoder {
135
+ constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
136
+ readonly encoding: string;
137
+ decode(input?: ArrayBuffer | ArrayBufferView, options?: { stream?: boolean }): string;
138
+ }
139
+
140
+ declare class URLSearchParams {
141
+ constructor(init?: string | Record<string, string> | [string, string][] | URLSearchParams);
142
+ readonly size: number;
143
+ append(name: string, value: string): void;
144
+ delete(name: string, value?: string): void;
145
+ get(name: string): string | null;
146
+ getAll(name: string): string[];
147
+ has(name: string, value?: string): boolean;
148
+ set(name: string, value: string): void;
149
+ sort(): void;
150
+ toString(): string;
151
+ entries(): IterableIterator<[string, string]>;
152
+ keys(): IterableIterator<string>;
153
+ values(): IterableIterator<string>;
154
+ forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void;
155
+ [Symbol.iterator](): IterableIterator<[string, string]>;
156
+ }
157
+ declare class URL {
158
+ constructor(url: string | URL, base?: string | URL);
159
+ static canParse(url: string | URL, base?: string | URL): boolean;
160
+ static parse(url: string | URL, base?: string | URL): URL | null;
161
+ hash: string;
162
+ host: string;
163
+ hostname: string;
164
+ href: string;
165
+ readonly origin: string;
166
+ password: string;
167
+ pathname: string;
168
+ port: string;
169
+ protocol: string;
170
+ search: string;
171
+ readonly searchParams: URLSearchParams;
172
+ username: string;
173
+ toString(): string;
174
+ toJSON(): string;
175
+ }
176
+
177
+ interface Crypto {
178
+ getRandomValues<T extends ArrayBufferView | null>(array: T): T;
179
+ randomUUID(): \`\${string}-\${string}-\${string}-\${string}-\${string}\`;
180
+ }
181
+ declare var crypto: Crypto;
182
+
183
+ interface Performance { now(): number; readonly timeOrigin: number }
184
+ declare var performance: Performance;
185
+ `;
186
+
187
+ /** Files a view imports for their URL: the bundler emits them beside the view and the import is the URL. */
188
+ const urlAssets = ["png", "jpg", "jpeg", "gif", "webp", "avif", "svg", "ico", "woff", "woff2", "ttf", "otf", "mp3", "mp4", "webm", "wasm"];
189
+
190
+ /**
191
+ * .voss/assets.d.ts, for both programs: what a baba imports besides
192
+ * code. Text files as text, stylesheets for their effect, CSS modules as
193
+ * their class names, and images, fonts and media as the URL they are
194
+ * served at beside the view.
195
+ */
196
+ export function assetDeclarations(): string {
197
+ return [
198
+ "// Written by voss on every load: what a baba imports besides code.",
199
+ "// Text: `import guide from \"./guide.md\" with { type: \"text\" }`.",
200
+ ...["md", "txt"].map((ext) => `declare module "*.${ext}" {\n const text: string;\n export default text;\n}`),
201
+ `declare module "*.mdx" { const prompt: import("babavoss/prompt").Prompt; export default prompt; }`,
202
+ "// Every spec of the baba, found where it lives (systems/NAME/spec.ts, spec.ts): what the Maker's page runs.",
203
+ `declare module "babavoss:specs" { const specs: import("babavoss/spec").Spec<any, any>[]; export default specs; }`,
204
+ "// A stylesheet, bundled with the view and attached to its windows: `import \"./view.css\"`.",
205
+ `declare module "*.module.css" {\n const classes: Record<string, string>;\n export default classes;\n}`,
206
+ `declare module "*.css" {}`,
207
+ "// A file served beside the view; the import is its URL: `import pic from \"./pic.png\"`.",
208
+ ...urlAssets.map((ext) => `declare module "*.${ext}" {\n const url: string;\n export default url;\n}`),
209
+ "",
210
+ ].join("\n");
211
+ }
212
+
213
+ async function put(path: string, want: string): Promise<void> {
214
+ const f = Bun.file(path);
215
+ if (!(await f.exists()) || (await f.text()) !== want) await Bun.write(path, want);
216
+ }
217
+
218
+ /** The configs voss writes, by file name under .baba/.voss. */
219
+ export function configs(): Record<string, string> {
220
+ return { "tsconfig.json": editorConfig(), "tsconfig.views.json": viewsConfig(), "guard.d.ts": guardDeclarations(), "assets.d.ts": assetDeclarations() };
221
+ }
222
+
223
+ /** Writes .baba/.voss/tsconfig.json, tsconfig.views.json, guard.d.ts and assets.d.ts where they differ from what voss would write; returns the backend config's path. */
224
+ export async function writeEditorConfig(p: Project): Promise<string> {
225
+ await moveOldDir(p);
226
+ const dir = join(p.baba, VOSS);
227
+ await mkdir(dir, { recursive: true });
228
+ for (const [name, want] of Object.entries(configs())) await put(join(dir, name), want);
229
+ return join(dir, "tsconfig.json");
230
+ }
231
+
232
+ /**
233
+ * Voss's files lived in .baba/state until the baba's state took the name: a
234
+ * project that still has it is moved once, and the lines of its own
235
+ * tsconfig.json and .gitignore that pointed there follow.
236
+ */
237
+ export async function moveOldDir(p: Project): Promise<void> {
238
+ const old = join(p.baba, "state");
239
+ if (!existsSync(old) || existsSync(join(p.baba, VOSS))) return;
240
+ await rename(old, join(p.baba, VOSS));
241
+ const follow = async (name: string, from: RegExp, to: string) => {
242
+ const file = join(p.baba, name);
243
+ const text = await readFile(file, "utf8").catch(() => null);
244
+ if (text !== null && from.test(text)) await writeFile(file, text.replace(from, to));
245
+ };
246
+ await follow("tsconfig.json", /\.\/state\//g, `./${VOSS}/`);
247
+ await follow(".gitignore", /^state$/m, VOSS);
248
+ }
249
+
250
+ /** The views config's path, beside the backend one. */
251
+ export function viewsConfigPath(p: Project): string {
252
+ return join(p.baba, VOSS, "tsconfig.views.json");
253
+ }
254
+
255
+ /** The files `voss init` writes beside the entry. */
256
+ export const scaffold = {
257
+ // The editor finds this file; the backend program is its own, the views program is reached through the reference.
258
+ // The alias is stated here too, relative to .baba: tools that read only the project's own file, shadcn's CLI among them, find it where they look.
259
+ tsconfig: `{ "extends": "./${VOSS}/tsconfig.json", "compilerOptions": { "paths": { "@/*": ["./*"] } }, "references": [{ "path": "./${VOSS}/tsconfig.views.json" }] }\n`,
260
+ gitignore: `node_modules\n${VOSS}\n.env.local\n`,
261
+ };
@@ -0,0 +1,12 @@
1
+ // The nearest project: the closest directory at or above `from` with .baba/package.json.
2
+ import { dirname, join } from "node:path";
3
+
4
+ export async function findProject(from: string): Promise<string | null> {
5
+ let dir = from;
6
+ for (;;) {
7
+ if (await Bun.file(join(dir, ".baba", "package.json")).exists()) return dir;
8
+ const up = dirname(dir);
9
+ if (up === dir) return null;
10
+ dir = up;
11
+ }
12
+ }
@@ -0,0 +1,176 @@
1
+ // voss init: a .baba in a directory, its dependencies installed. One entry,
2
+ // one interface, one system to start from, and the promptware: what the baba tells
3
+ // agents, as code, versioned with the project.
4
+ import { basename, join, resolve } from "node:path";
5
+ import { mkdir } from "node:fs/promises";
6
+ import { scaffold } from "./config.ts";
7
+ import { kitDependencies, kitFiles } from "../kit/index.ts";
8
+ import { exists, PAGE_WORDS } from "./project.ts";
9
+
10
+ export interface InitOptions {
11
+ name?: string;
12
+ title?: string;
13
+ /** What package.json says for babavoss: a version, or link:babavoss from source. */
14
+ framework: string;
15
+ /** More dependencies to write: from source, the types the linked framework does not carry over. */
16
+ extra?: Record<string, string>;
17
+ install?: boolean;
18
+ }
19
+
20
+ export async function init(dir: string, o: InitOptions): Promise<{ name: string; dir: string }> {
21
+ dir = resolve(dir);
22
+ const baba = join(dir, ".baba");
23
+ if (await exists(join(baba, "package.json"))) throw new Error(`${dir} already has a .baba`);
24
+ const name = o.name ?? basename(dir).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
25
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`name ${JSON.stringify(name)}: lowercase letters, digits and dashes; give --name`);
26
+ if (PAGE_WORDS.has(name)) throw new Error(`name ${name}: the page's own address; give --name`);
27
+ await mkdir(join(baba, "systems", "hello"), { recursive: true });
28
+ const pkg = {
29
+ name,
30
+ private: true,
31
+ type: "module",
32
+ baba: { title: o.title ?? name },
33
+ dependencies: { babavoss: o.framework, ...kitDependencies, ...(o.extra ?? {}) },
34
+ };
35
+ await Bun.write(join(baba, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
36
+ await Bun.write(join(baba, "tsconfig.json"), scaffold.tsconfig);
37
+ await Bun.write(join(baba, ".gitignore"), scaffold.gitignore);
38
+ await Bun.write(join(baba, "index.ts"), entry);
39
+ await Bun.write(join(baba, "interface.tsx"), viewFile);
40
+ // The kit: shadcn's config, the bridge over voss's theme, the class helper; components arrive through the kit system, ui-add.
41
+ for (const [file, text] of Object.entries(kitFiles)) await Bun.write(join(baba, file), text);
42
+ await Bun.write(join(baba, "systems", "hello", "index.ts"), systemFile("hello"));
43
+ await Bun.write(join(baba, "systems", "hello", "spec.ts"), systemSpecFile("hello"));
44
+ await Bun.write(join(baba, "spec.test.ts"), specRunnerFile);
45
+ await Bun.write(join(baba, "promptware", "index.ts"), promptwareFile(o.title ?? name));
46
+ if (o.install !== false) await install(baba);
47
+ return { name, dir };
48
+ }
49
+
50
+ /** Bun's installer, run by the Bun that runs voss. */
51
+ export async function install(baba: string): Promise<void> {
52
+ // Its output is kept, not shown: the shell's stdout and stderr carry one line each; a failure says what it printed last.
53
+ const p = Bun.spawn([process.execPath, "install"], { cwd: baba, env: { ...process.env, BUN_BE_BUN: "1" }, stdout: "pipe", stderr: "pipe" });
54
+ const [out, err, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]);
55
+ if (code !== 0) throw new Error(`bun install failed in ${baba}: ${(err || out).trim().split("\n").slice(-5).join("; ")}`);
56
+ }
57
+
58
+ /** The promptware: what the baba tells agents, as code. The contract is not here: agents get it at `voss baba`. */
59
+ export const promptwareFile = (title: string) => `import { context, md } from "babavoss/prompt";
60
+
61
+ // What this baba tells agents: its section of the context file, its skills
62
+ // (one skill(...) per .tsx beside this file), its hooks. Code, rendered to
63
+ // Markdown and compiled by voss into CLAUDE.md and the skills.
64
+ export default [
65
+ context(md\`
66
+ ${title.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("$", "\\$")}: say here what this project is, in a sentence or two.
67
+
68
+ How to work on it: the conventions, the checks to run before a commit, what not to touch.
69
+
70
+ ## Rules
71
+
72
+ - Keep the tests green on every commit.
73
+ \`),
74
+ ];
75
+ `;
76
+
77
+ /** The entry: the baba, its systems listed, its promptware; `harness` for which harnesses get the files. */
78
+ export const entry = `import { baba } from "babavoss";
79
+ import { kit } from "babavoss/kit";
80
+ import hello from "./systems/hello/index.ts";
81
+ import promptware from "./promptware/index.ts";
82
+
83
+ // kit keeps the baba's components, under ui/, as objects: \`voss baba ui\` lists them, \`ui-add\` brings more from shadcn.
84
+ // The apps voss's shell offers; interface.tsx gives each its component.
85
+ export default baba({ systems: [hello, kit], apps: [{ key: "hello", title: "Hello", icon: "hand" }], promptware });
86
+ `;
87
+
88
+ /** A system: a data model, one action, one query, written as one expression: the declaration, then its pieces made from it. */
89
+ export const systemFile = (name: string, v = declared(name)) => `import { system, s } from "babavoss";
90
+
91
+ export default system({
92
+ name: ${JSON.stringify(name)},
93
+ model: {
94
+ greeting: { text: s.string(), at: s.integer() },
95
+ },
96
+ }, (${v}, p) => [
97
+ p.action("say", {
98
+ summary: "greet someone",
99
+ args: { name: s.string(), loud: s.optional(s.boolean()) },
100
+ positional: ["name"],
101
+ result: s.object({ entity: s.entity(), text: s.string() }),
102
+ run: (w, { name, loud }) => {
103
+ const text = loud ? \`Hello, \${name}!\`.toUpperCase() : \`Hello, \${name}!\`;
104
+ return { entity: w.spawn(${v}.greeting, { text, at: w.now }), text };
105
+ },
106
+ }),
107
+
108
+ p.query("greetings", {
109
+ summary: "every greeting, oldest first",
110
+ result: s.array(s.object({ text: s.string(), at: s.integer() })),
111
+ read: (w) => w.query(${v}.greeting).map((r) => r.greeting),
112
+ }),
113
+ ]);
114
+ `;
115
+ /** A system's spec, beside it: the system and the systems it reads, and a first scenario. */
116
+ export const systemSpecFile = (name: string, v = declared(name)) => `import { spec } from "babavoss/spec";
117
+ import ${v} from "./index.ts";
118
+
119
+ export default spec(${v}, {
120
+ scenarios: [{
121
+ name: "a greeting is kept",
122
+ intent: "someone greeted is in the greetings, in the words they were greeted with",
123
+ script: ({ fire, tick, check }) => [
124
+ fire("say", { name: "Ada" }),
125
+ tick(),
126
+ check("Ada is greeted", (t) => t.read("greetings").map((g) => g.text), ["Hello, Ada!"]),
127
+ ],
128
+ }],
129
+ });
130
+ `;
131
+
132
+ /** The one runner of the baba's specs: bun test finds it, and it proves every systems/NAME/spec.ts and spec.ts. */
133
+ export const specRunnerFile = `// Every spec of this baba under bun test: each system's systems/NAME/spec.ts,
134
+ // the whole baba's spec.ts, and the scenarios saved beside them. Verdicts land
135
+ // in .voss/spec, where the Maker shows them.
136
+ import { proveAll } from "babavoss/test";
137
+
138
+ await proveAll(import.meta.dir);
139
+ `;
140
+
141
+ /** A system's name as the template's name for the system inside its pieces: dashes become camel case; one the template or JavaScript already uses gets "System" after it. */
142
+ const TAKEN = new Set(["s", "p", "w", "r", "system", "name", "loud", "text", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield", "let", "static", "await"]);
143
+ const declared = (name: string) => { const v = name.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase()); return TAKEN.has(v) ? `${v}System` : v; };
144
+
145
+ /** The interface: a component for each app, here the state, live, and the actions. */
146
+ export const viewFile = `import { useState } from "react";
147
+ import { compose, useBabaState, Button, Input } from "babavoss/web";
148
+ import "./ui.css";
149
+ import baba from "./index.ts";
150
+ import hello from "./systems/hello/index.ts";
151
+
152
+ function Hello() {
153
+ const { w, fire, ready } = useBabaState(baba);
154
+ const [name, setName] = useState("World");
155
+ return (
156
+ <main style={{ maxWidth: 640, margin: "0 auto", padding: 24 }}>
157
+ <h1>{ready ? "Hello" : "…"}</h1>
158
+ <p><Input value={name} onChange={(e) => setName(e.target.value)} /> <Button onClick={() => void fire("say", { name })}>Say</Button></p>
159
+ <ul>{w.query(hello.greeting).map((r) => <li key={r.entity}>{r.greeting.text}</li>)}</ul>
160
+ </main>
161
+ );
162
+ }
163
+
164
+ export default compose<typeof baba>({ apps: { hello: Hello } });
165
+ `;
166
+
167
+ /** voss baba add-system: a system under .baba/systems/NAME, to be listed in the entry. */
168
+ export async function addSystem(baba: string, name: string): Promise<{ name: string; dir: string; next: string }> {
169
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`system name ${JSON.stringify(name)}: lowercase letters, digits and dashes`);
170
+ const dir = join(baba, "systems", name);
171
+ if (await exists(dir)) throw new Error(`${dir} already exists`);
172
+ await mkdir(dir, { recursive: true });
173
+ await Bun.write(join(dir, "index.ts"), systemFile(name));
174
+ await Bun.write(join(dir, "spec.ts"), systemSpecFile(name));
175
+ return { name, dir, next: `import it in ${join(baba, "index.ts")} and list it in baba({ systems: [...] })` };
176
+ }