babavoss 0.0.1 → 0.0.2
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.
- package/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The shell's exit statuses over the core's closed set of codes (door/core.ts):
|
|
2
|
+
// each code with its exit status and whether the same call may succeed if
|
|
3
|
+
// tried again. Every error the shell answers goes through here, whatever
|
|
4
|
+
// raised it.
|
|
5
|
+
import { CODES as RETRY, DoorError, codeOf, errorOf, type Code, type ErrorBody } from "../door/core.ts";
|
|
6
|
+
|
|
7
|
+
/** 0 ok; 1 the baba or the daemon failed; 2 the arguments; 3 time ran out; 4 gone or not found. */
|
|
8
|
+
const EXIT: Record<Code, number> = {
|
|
9
|
+
failed: 1, result: 1, down: 1, busy: 1, timeout: 1, unknown: 1,
|
|
10
|
+
usage: 2, args: 2, noaction: 2, noquery: 2, name: 2, owner: 2, noproject: 2,
|
|
11
|
+
entity: 4, gone: 4, nokey: 4,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Each code's exit status and retry. */
|
|
15
|
+
export const CODES = Object.fromEntries((Object.keys(RETRY) as Code[]).map((c) => [c, [EXIT[c], RETRY[c]] as const])) as { [C in Code]: readonly [number, boolean] };
|
|
16
|
+
export type { Code };
|
|
17
|
+
export type ErrorLine = ErrorBody;
|
|
18
|
+
|
|
19
|
+
export { codeOf };
|
|
20
|
+
export const exitOf = (code: Code) => EXIT[code];
|
|
21
|
+
|
|
22
|
+
/** A shell error: a code of the set, with the call's usage line or idempotency key when they help. */
|
|
23
|
+
export class ShellError extends DoorError {}
|
|
24
|
+
|
|
25
|
+
/** Anything thrown, as the one error line. */
|
|
26
|
+
export const errorLine = (err: unknown): ErrorLine => errorOf(err);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Positionals: the args a shell fills from bare words, in order. Pure.
|
|
2
|
+
import type { Json } from "../kernel/schema.ts";
|
|
3
|
+
|
|
4
|
+
/** Each positional is an args property holding a scalar, or an array of scalars in last place. */
|
|
5
|
+
export function checkPositional(args: Json, names: string[]): string[] {
|
|
6
|
+
const props = (args.properties ?? {}) as Record<string, Json>;
|
|
7
|
+
const scalar = (j: Json | undefined) => {
|
|
8
|
+
const t = j?.type;
|
|
9
|
+
return t === undefined || ["string", "number", "integer", "boolean"].includes(Array.isArray(t) ? String(t[0]) : String(t));
|
|
10
|
+
};
|
|
11
|
+
names.forEach((n, i) => {
|
|
12
|
+
const p = props[n];
|
|
13
|
+
if (!p) throw new Error(`positional ${JSON.stringify(n)} is not one of the args`);
|
|
14
|
+
if (names.indexOf(n) !== i) throw new Error(`positional ${JSON.stringify(n)} is listed twice`);
|
|
15
|
+
const array = p.type === "array";
|
|
16
|
+
if (array && i !== names.length - 1) throw new Error(`positional ${JSON.stringify(n)} is an array, so it must come last`);
|
|
17
|
+
if (!scalar(array ? (p.items as Json | undefined) : p)) throw new Error(`positional ${JSON.stringify(n)} must be a scalar or an array of scalars`);
|
|
18
|
+
});
|
|
19
|
+
return [...names];
|
|
20
|
+
}
|
package/src/shell/run.ts
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// The shell: words to a baba, answers to a person or a script.
|
|
2
|
+
//
|
|
3
|
+
// voss baba the index: every call with an example, as JSON
|
|
4
|
+
// voss baba NAME '{…}' | [WORD …] [--flag VALUE …] [--key K] fire an action or read a query of the nearest baba
|
|
5
|
+
// voss baba NAME --schema one call's args and result schemas, as JSON
|
|
6
|
+
// voss BABA NAME … the same, for a remembered baba, from anywhere
|
|
7
|
+
// voss baba start | stop | status the baba as a child of the root's daemon
|
|
8
|
+
// voss baba state [ENTITY | COMPONENT [KEY] | RESOURCE] [--where k=v] [--deep] [--limit N] [--all]
|
|
9
|
+
// voss baba follow ENTITY | COMPONENT KEY [--until C | C.field=value] [--for 30s] [--changes]
|
|
10
|
+
// voss baba follow QUERY [args…] --changed [--for 30s]
|
|
11
|
+
// voss baba run [FILE | LINE …] | raw spawn JSON | add E COMPONENT [JSON] | remove E COMPONENT … | despawn E | set RESOURCE JSON
|
|
12
|
+
// voss baba check | install | add-system NAME | promptware … | serve [HOST:PORT]
|
|
13
|
+
// voss init [DIR] | babas … | gui [HOST:PORT] | service …
|
|
14
|
+
//
|
|
15
|
+
// Stdout is one line of JSON per answer (`--pretty` indents it); stderr is
|
|
16
|
+
// one line, {"error":{"code","message","usage"?,"retry"}}, and nothing else.
|
|
17
|
+
// The exit status is the code's (codes.ts). Human text only for --help, and
|
|
18
|
+
// the contract listing is `voss baba --help`. The index, the schemas, follow,
|
|
19
|
+
// watch, state, the budget and the error shape are the core's (door/core.ts),
|
|
20
|
+
// which the MCP server answers too.
|
|
21
|
+
import { DEFAULT_ADDRESS, hostPort } from "./address.ts";
|
|
22
|
+
import { appendFileSync } from "node:fs";
|
|
23
|
+
import { thisBuild, buildName } from "../kernel/build.ts";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { Registry } from "../baba/registry.ts";
|
|
26
|
+
import { findProject } from "../baba/find.ts";
|
|
27
|
+
import { openProject, type Project } from "../baba/project.ts";
|
|
28
|
+
import { Node, NodeError, manifestPath, readManifest, type Described } from "../baba/node.ts";
|
|
29
|
+
import { serve } from "../http/server.ts";
|
|
30
|
+
import { words, callArgs, usage, describeArgs, UsageError } from "./args.ts";
|
|
31
|
+
import { init, install, addSystem } from "../baba/init.ts";
|
|
32
|
+
import { check } from "../baba/check.ts";
|
|
33
|
+
import { planProject, syncProject, outputOf, sources } from "../promptware/disk.ts";
|
|
34
|
+
import { report } from "../promptware/sync.ts";
|
|
35
|
+
import * as service from "./service.ts";
|
|
36
|
+
import { connect, daemon, type Client, type Daemon } from "./client.ts";
|
|
37
|
+
import { runMcp } from "../mcp/main.ts";
|
|
38
|
+
import { ShellError, errorLine, exitOf } from "./codes.ts";
|
|
39
|
+
import { errorOf } from "../door/core.ts";
|
|
40
|
+
import { BUDGET, budget, index, schemaOf, exampleCall } from "../door/core.ts";
|
|
41
|
+
import { state, follow, batch, rawOp, type Io } from "./state.ts";
|
|
42
|
+
import type { Manifest } from "../ecs/baba.ts";
|
|
43
|
+
import type { Snapshot } from "../ecs/state.ts";
|
|
44
|
+
|
|
45
|
+
export interface Shell {
|
|
46
|
+
cwd: string;
|
|
47
|
+
rootDir: string;
|
|
48
|
+
/** What a new project's package.json depends on for the framework: a version, or link:babavoss from source. */
|
|
49
|
+
framework: string;
|
|
50
|
+
/** From source, the types a linked framework does not carry over; nothing when published. */
|
|
51
|
+
extra?: Record<string, string>;
|
|
52
|
+
stdout: (s: string) => void;
|
|
53
|
+
stderr: (s: string) => void;
|
|
54
|
+
/** The whole of standard input, for `run` and `--NAME @-`. */
|
|
55
|
+
stdin?: () => Promise<string>;
|
|
56
|
+
/** Whether standard input is a terminal: `run` with no lines then refuses rather than wait. */
|
|
57
|
+
stdinTTY?: boolean;
|
|
58
|
+
/** Ctrl-C: calls the handler on each interrupt until the returned function unsubscribes. */
|
|
59
|
+
interrupt?: (handler: () => void) => () => void;
|
|
60
|
+
/** Whether rootDir is the one the system service runs on, so `start` may install the service when no daemon answers. */
|
|
61
|
+
service?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Only the one project, for serving it alone; the root's directory still holds the GUI's assets. */
|
|
65
|
+
class OneProject extends Registry {
|
|
66
|
+
constructor(rootDir: string, private project: string) { super(rootDir); }
|
|
67
|
+
override async list() { return [this.project]; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A server's log lines: to $VOSS_LOG when set, else as {"log"} lines on stdout; never on stderr. */
|
|
71
|
+
function logTo(sh: Shell): (l: string) => void {
|
|
72
|
+
const file = process.env.VOSS_LOG;
|
|
73
|
+
if (file) return (l) => { try { appendFileSync(file, `${new Date().toISOString()} ${l}\n`); } catch { /* a log that cannot be written is dropped */ } };
|
|
74
|
+
return (l) => sh.stdout(JSON.stringify({ log: l }) + "\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rootUsage = `Usage: voss BABA NAME …
|
|
78
|
+
|
|
79
|
+
Babas:
|
|
80
|
+
baba the nearest baba above the working directory
|
|
81
|
+
NAME a remembered baba, from anywhere
|
|
82
|
+
|
|
83
|
+
voss init [DIR] [--name N] [--title T] [--no-install]
|
|
84
|
+
a .baba in DIR, the working directory by default, remembered
|
|
85
|
+
voss babas add [DIR] remember a baba (the nearest one by default)
|
|
86
|
+
voss babas remove DIR forget it
|
|
87
|
+
voss babas list the babas, by name and directory
|
|
88
|
+
voss gui [HOST:PORT] serve the GUI and the API, ${DEFAULT_ADDRESS} by default;
|
|
89
|
+
started inside a baba, remembers it first
|
|
90
|
+
voss service install [HOST:PORT] | remove | restart | status
|
|
91
|
+
the GUI kept running by the system, at login and after a crash
|
|
92
|
+
|
|
93
|
+
A baba runs as a child of the daemon (\`voss gui\`, kept by \`voss service\`):
|
|
94
|
+
\`voss baba start\` opens it there, installing the service first if need be;
|
|
95
|
+
\`stop\` retires it, \`status\` says how it is.
|
|
96
|
+
|
|
97
|
+
A baba answers actions and queries, by name. \`voss baba\` answers the index,
|
|
98
|
+
every call with an example to copy, as JSON; \`voss baba NAME --schema\` one
|
|
99
|
+
call's args and result schemas; \`voss baba --help\` the same as a listing.
|
|
100
|
+
The canonical spelling of a call is its args as one JSON word:
|
|
101
|
+
voss baba NAME '{"field":"value"}'. Positionals and flags work too: the
|
|
102
|
+
<positionals> as bare words, in order, and every argument as --name VALUE; a
|
|
103
|
+
flag wins. --name=VALUE spells a value starting with --; a boolean is --flag,
|
|
104
|
+
--flag=false or --no-flag; an object argument takes JSON. A secret is never on
|
|
105
|
+
the command line: --name @- reads it from stdin. --key K on an action is its
|
|
106
|
+
idempotency key: the same call with the same key answers the same without
|
|
107
|
+
running again. An action runs inside the next round and answers what it
|
|
108
|
+
returned; a query reads between rounds and answers its value. \`voss baba
|
|
109
|
+
state\` summarises the state; \`follow\` waits on an entity or a query; \`run\`
|
|
110
|
+
sends lines of calls in order.
|
|
111
|
+
|
|
112
|
+
Stdout is one line of JSON per answer; --pretty indents it. An answer over
|
|
113
|
+
20000 characters goes to .baba/.voss/out/ and the line says where, with its
|
|
114
|
+
head; --max N sets the limit, --max 0 lifts it. An error is one line on
|
|
115
|
+
stderr, {"error":{"code","message","usage"?,"retry"}}, and the exit status
|
|
116
|
+
says its kind: 1 the baba or the daemon failed, 2 the arguments, 3 time ran
|
|
117
|
+
out, 4 gone or not found. --help after any word explains it.
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const builtinHelp: Record<string, string> = {
|
|
121
|
+
state: "the state: state (a summary) | state --all | state ENTITY | state COMPONENT [KEY] [--where k=v] [--deep] [--limit N] | state RESOURCE",
|
|
122
|
+
raw: "the state's own writes: raw spawn JSON | add ENTITY COMPONENT [JSON] | remove ENTITY COMPONENT … | despawn ENTITY | set RESOURCE JSON",
|
|
123
|
+
run: "calls in order, one answer per line: run [FILE | LINE …] or lines on stdin, each {\"NAME\": args, \"key\"?: K}; \"$N.field\" is line N's answer",
|
|
124
|
+
follow: "follow ENTITY | COMPONENT KEY [--until COMPONENT | COMPONENT.field=value] [--for 30s] [--changes]; follow QUERY [args…] --changed [--for 30s]",
|
|
125
|
+
start: "the baba, opened by the daemon and remembered: start",
|
|
126
|
+
stop: "the baba retired, its state written, and forgotten: stop",
|
|
127
|
+
status: "whether it runs, its generation and where: status",
|
|
128
|
+
check: "the TypeScript checker over the baba",
|
|
129
|
+
manifest: "the baba loaded once here and let go, so .baba/.voss/manifest.json exists before it runs under the daemon",
|
|
130
|
+
install: "its dependencies, with Bun",
|
|
131
|
+
"add-system": "a system under .baba/systems/NAME: add-system NAME",
|
|
132
|
+
serve: "the GUI for this baba alone: serve [HOST:PORT]",
|
|
133
|
+
mcp: "an MCP server over stdio for agents, a client of the running voss",
|
|
134
|
+
promptware: "the harness files: promptware compile | sync | output PATH",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* An action called as a harness's hook: the answer in the harness's own
|
|
139
|
+
* protocol. `{ decision: "block", reason }` is exit 2 with the reason on
|
|
140
|
+
* stderr, which Claude Code shows the agent and stops on; `{ context }` is
|
|
141
|
+
* told to the agent; anything else lets the event through. A baba that does
|
|
142
|
+
* not answer lets through too, unless `strict`.
|
|
143
|
+
*/
|
|
144
|
+
async function asHook(sh: Shell, call: () => Promise<unknown>, event: string, strict: boolean): Promise<number> {
|
|
145
|
+
let r: { decision?: string; reason?: string; context?: string } | null;
|
|
146
|
+
try { r = (await call()) as typeof r; } catch (err) {
|
|
147
|
+
const line = errorOf(err);
|
|
148
|
+
if (strict) { sh.stderr(`the baba did not answer the hook: ${line.message}\n`); return 2; }
|
|
149
|
+
sh.stdout(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: `the baba did not answer its ${event} hook: ${line.message}` } }) + "\n");
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (r && r.decision === "block") { sh.stderr(`${r.reason ?? "blocked by the baba"}\n`); return 2; }
|
|
153
|
+
const out: Record<string, unknown> = { hookEventName: event };
|
|
154
|
+
if (event === "PreToolUse" && r?.decision === "allow") { out.permissionDecision = "allow"; if (r.reason) out.permissionDecisionReason = r.reason; }
|
|
155
|
+
if (r?.context) out.additionalContext = r.context;
|
|
156
|
+
if (Object.keys(out).length > 1) sh.stdout(JSON.stringify({ hookSpecificOutput: out }) + "\n");
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The words every command shares: --pretty, --max N, and --json, accepted and ignored for one release. */
|
|
161
|
+
function globals(argv: string[]): { ws: string[]; pretty: boolean; max: number } {
|
|
162
|
+
const ws: string[] = [];
|
|
163
|
+
let pretty = false;
|
|
164
|
+
let max = BUDGET;
|
|
165
|
+
const limit = (v: string | undefined) => { if (v === undefined || !/^\d+$/.test(v)) throw new UsageError(`--max takes a number of characters, 0 for none; got ${v ?? "nothing"}`); return Number(v); };
|
|
166
|
+
for (let i = 0; i < argv.length; i++) {
|
|
167
|
+
const w = argv[i]!;
|
|
168
|
+
if (w === "--") { ws.push(...argv.slice(i)); break; }
|
|
169
|
+
if (w === "--pretty") pretty = true;
|
|
170
|
+
else if (w === "--json") { /* the old way to ask for JSON: it is the only way now */ }
|
|
171
|
+
else if (w === "--max") max = limit(argv[++i]);
|
|
172
|
+
else if (w.startsWith("--max=")) max = limit(w.slice(6));
|
|
173
|
+
else ws.push(w);
|
|
174
|
+
}
|
|
175
|
+
return { ws, pretty, max };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function run(argv: string[], sh: Shell): Promise<number> {
|
|
179
|
+
const reg = new Registry(sh.rootDir);
|
|
180
|
+
// Every answer is JSON; the promptware block below still reads this.
|
|
181
|
+
const json = true;
|
|
182
|
+
let callUsage: string | undefined;
|
|
183
|
+
let outDir: string | undefined;
|
|
184
|
+
const fail = (err: unknown) => {
|
|
185
|
+
const line = errorLine(err);
|
|
186
|
+
if (callUsage && (line.code === "args" || line.code === "usage") && !line.usage) line.usage = callUsage;
|
|
187
|
+
sh.stderr(JSON.stringify({ error: { code: line.code, message: line.message, ...(line.usage ? { usage: line.usage } : {}), retry: line.retry, ...(line.key ? { key: line.key } : {}) } }) + "\n");
|
|
188
|
+
return exitOf(line.code);
|
|
189
|
+
};
|
|
190
|
+
let g: ReturnType<typeof globals>;
|
|
191
|
+
try { g = globals(argv); } catch (err) { return fail(err); }
|
|
192
|
+
let ws = g.ws;
|
|
193
|
+
const help = ws.at(-1) === "--help" || ws.at(-1) === "-h";
|
|
194
|
+
if (help) ws = ws.slice(0, -1);
|
|
195
|
+
const [node, ...rest] = ws;
|
|
196
|
+
|
|
197
|
+
const print = (v: unknown) => sh.stdout(JSON.stringify(v ?? null, null, g.pretty ? 2 : undefined) + "\n");
|
|
198
|
+
/** One answer, at once: whole when it fits, else written under .baba/.voss/out/ and its marker printed. */
|
|
199
|
+
const out = (v: unknown, name = "answer"): void => print(budget(v, { dir: outDir, name, limit: g.max }));
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (!node) { sh.stdout(rootUsage); return 0; }
|
|
203
|
+
if (node === "--version" || node === "-v") { print(thisBuild); return 0; }
|
|
204
|
+
|
|
205
|
+
if (node === "init") {
|
|
206
|
+
if (help) { sh.stdout(rootUsage); return 0; }
|
|
207
|
+
let dir: string | undefined;
|
|
208
|
+
let installing = true;
|
|
209
|
+
const opts: Record<string, string> = {};
|
|
210
|
+
for (let i = 0; i < rest.length; i++) {
|
|
211
|
+
const w = rest[i]!;
|
|
212
|
+
if (w === "--name" || w === "--title") { const v = rest[++i]; if (v === undefined || v.startsWith("--")) throw new UsageError(`${w} needs a value`); opts[w.slice(2)] = v; }
|
|
213
|
+
else if (w === "--no-install") installing = false;
|
|
214
|
+
else if (w.startsWith("--")) throw new UsageError(`init takes --name, --title and --no-install, not ${w}`);
|
|
215
|
+
else if (dir === undefined) dir = w;
|
|
216
|
+
else throw new UsageError(`init takes one directory, got ${w}`);
|
|
217
|
+
}
|
|
218
|
+
const made = await init(dir ?? sh.cwd, { name: opts.name, title: opts.title, framework: sh.framework, extra: sh.extra, install: installing });
|
|
219
|
+
await reg.add(made.dir);
|
|
220
|
+
await out(made);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (node === "babas") {
|
|
225
|
+
const [cmd, arg] = rest;
|
|
226
|
+
if (help || !cmd) { sh.stdout(rootUsage); return 0; }
|
|
227
|
+
if (cmd === "add") {
|
|
228
|
+
const dir = arg ?? (await findProject(sh.cwd));
|
|
229
|
+
if (!dir) throw new ShellError("noproject", "no .baba here or above; give a directory");
|
|
230
|
+
const p = await openProject(dir);
|
|
231
|
+
await reg.add(p.dir);
|
|
232
|
+
await out({ name: p.name, dir: p.dir });
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (cmd === "remove" && arg) {
|
|
236
|
+
if (!(await reg.remove(arg))) throw new ShellError("noproject", `${arg} is not remembered`);
|
|
237
|
+
await out({ removed: arg });
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
if (cmd === "list") {
|
|
241
|
+
const list = [];
|
|
242
|
+
for (const dir of await reg.list()) {
|
|
243
|
+
try { const p = await openProject(dir); list.push({ name: p.name, title: p.title, dir }); }
|
|
244
|
+
catch (err) { list.push({ name: null, title: null, dir, error: err instanceof Error ? err.message : String(err) }); }
|
|
245
|
+
}
|
|
246
|
+
await out(list);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
throw new UsageError("babas: add, remove or list");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (node === "service") {
|
|
253
|
+
const [cmd, ...more] = rest;
|
|
254
|
+
if (help || !cmd) { sh.stdout(rootUsage); return 0; }
|
|
255
|
+
if (cmd === "install") {
|
|
256
|
+
const [host, port] = hostPort(more[0]);
|
|
257
|
+
const st = await service.install(`${host}:${port}`);
|
|
258
|
+
await out({ ...st, hint: `the GUI is kept running at ${st.url}` });
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
if (cmd === "remove") { await out(await service.remove()); return 0; }
|
|
262
|
+
if (cmd === "restart") { await out(await service.restart()); return 0; }
|
|
263
|
+
if (cmd === "status") { await out(await service.status()); return 0; }
|
|
264
|
+
throw new UsageError("service: install, remove, restart or status");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (node === "gui") {
|
|
268
|
+
const near = await findProject(sh.cwd);
|
|
269
|
+
if (near) await reg.add(near);
|
|
270
|
+
const [host, port] = hostPort(rest[0]);
|
|
271
|
+
// Under the service, this very process is the one it keeps: then a clean exit is a restart.
|
|
272
|
+
const st = sh.service ? await service.status() : null;
|
|
273
|
+
const kept = !!st?.installed && st.running && st.pid === process.pid;
|
|
274
|
+
const server = await serve(reg, host, port, logTo(sh), { kept, init: { framework: sh.framework, extra: sh.extra } });
|
|
275
|
+
await out({ url: `http://${server.hostname}:${server.port}/`, build: buildName(thisBuild), ...(st?.installed ? {} : { hint: "to keep it running, `voss service install`" }) });
|
|
276
|
+
return -1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A project: `baba` is the nearest one, anything else a remembered name.
|
|
280
|
+
let project: Project;
|
|
281
|
+
if (node === "baba") {
|
|
282
|
+
const dir = await findProject(sh.cwd);
|
|
283
|
+
if (!dir) throw new ShellError("noproject", "no .baba here or above; `voss babas list` names the known ones");
|
|
284
|
+
project = await openProject(dir);
|
|
285
|
+
} else {
|
|
286
|
+
let found: Project | null = null;
|
|
287
|
+
for (const dir of await reg.list()) {
|
|
288
|
+
try { const p = await openProject(dir); if (p.name === node) { found = p; break; } } catch { /* a stale entry answers under list */ }
|
|
289
|
+
}
|
|
290
|
+
if (!found) throw new ShellError("noproject", `no baba ${node}; \`voss babas list\` names the known ones`);
|
|
291
|
+
project = found;
|
|
292
|
+
}
|
|
293
|
+
outDir = join(project.baba, ".voss", "out");
|
|
294
|
+
const [name, ...args] = rest;
|
|
295
|
+
const prog = `voss ${node}`;
|
|
296
|
+
if (name === "fire" && help) { sh.stdout(`Usage: ${prog} ${builtinHelp.run}\n`); return 0; }
|
|
297
|
+
if (name && Object.hasOwn(builtinHelp, name) && help) { sh.stdout(`Usage: ${prog} ${builtinHelp[name]}\n`); return 0; }
|
|
298
|
+
if (name === "check") {
|
|
299
|
+
let text = "";
|
|
300
|
+
const code = await check(project, (s) => { text += s; }, (s) => { text += s; });
|
|
301
|
+
await out({ ok: code === 0, output: text });
|
|
302
|
+
if (code !== 0) throw new ShellError("failed", "the check found errors; the output says which");
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
if (name === "manifest") {
|
|
306
|
+
// The baba loaded once, in this process, and let go: what every load writes, .baba/.voss/manifest.json, is there
|
|
307
|
+
// for the index, --schema and the promptware before the baba ever runs under the daemon, as in a fresh worktree.
|
|
308
|
+
const lines: string[] = [];
|
|
309
|
+
const n = new Node(project, (l) => lines.push(l));
|
|
310
|
+
try { await n.start(); } finally { await n.stop(); }
|
|
311
|
+
const m = await readManifest(project);
|
|
312
|
+
await out({ manifest: manifestPath(project), systems: m?.systems.map((c) => c.name) ?? [], actions: Object.keys(m?.actions ?? {}).length, queries: Object.keys(m?.queries ?? {}).length, log: lines.slice(-5) });
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
if (name === "install") { await install(project.baba); await out({ installed: project.baba }); return 0; }
|
|
316
|
+
if (name === "serve") {
|
|
317
|
+
if (args.length > 1) throw new UsageError(`serve takes one HOST:PORT`);
|
|
318
|
+
const [host, port] = hostPort(args[0]);
|
|
319
|
+
const server = await serve(new OneProject(sh.rootDir, project.dir), host, port, logTo(sh), { address: false });
|
|
320
|
+
await out({ url: `http://${server.hostname}:${server.port}/${project.name}/` });
|
|
321
|
+
return -1;
|
|
322
|
+
}
|
|
323
|
+
if (name === "mcp") return await runMcp(project, sh.rootDir);
|
|
324
|
+
if (name === "add-system") {
|
|
325
|
+
const w = words(args, ["name"]);
|
|
326
|
+
if (typeof w.name !== "string" || !w.name) throw new UsageError(`add-system takes a NAME`);
|
|
327
|
+
await out(await addSystem(project.baba, w.name));
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
if (name === "start") return await start(project, prog, reg, sh, out);
|
|
331
|
+
if (name === "stop") {
|
|
332
|
+
await reg.remove(project.dir);
|
|
333
|
+
const d = await daemon(sh.rootDir);
|
|
334
|
+
if (d) await d.refresh();
|
|
335
|
+
await out({ stopped: true });
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
if (name === "status") {
|
|
339
|
+
const c = await connect(project, sh.rootDir, prog).catch(downOnly);
|
|
340
|
+
await out(c instanceof NodeError ? { running: false, name: project.name, hint: `${prog} start` } : statusOf(await c.describe(), c.where));
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The state is the daemon's; what only reads the contract does with the manifest the last load wrote.
|
|
345
|
+
const c = await connect(project, sh.rootDir, prog).catch(downOnly);
|
|
346
|
+
const down = c instanceof NodeError ? c : null;
|
|
347
|
+
const d = c instanceof NodeError ? null : await c.describe();
|
|
348
|
+
// Down, the promptware compiles from a manifest of the code as it is now: the baba is loaded once in this process and let
|
|
349
|
+
// go, as \`manifest\` does, so a fresh worktree has one and an edit to the promptware is in it. Busy or broken, the last one stands.
|
|
350
|
+
let loadError: string | null = null;
|
|
351
|
+
if (down && name === "promptware") {
|
|
352
|
+
const n = new Node(project, () => {});
|
|
353
|
+
try { await n.start(); } catch (err) { loadError = err instanceof Error ? err.message : String(err); } finally { await n.stop().catch(() => {}); }
|
|
354
|
+
}
|
|
355
|
+
const m = d ? d.manifest : await readManifest(project);
|
|
356
|
+
if (!m && loadError) throw new ShellError("failed", `${project.name} did not load: ${loadError}`);
|
|
357
|
+
if (!m) { if (down) throw down; throw new ShellError("down", `${project.name} is not running${d?.error ? `: ${d.error}` : ""}`); }
|
|
358
|
+
if (name === "promptware") {
|
|
359
|
+
// Running, the baba's promptware system syncs, and the answer waits for it; down, the same compiler runs here over the files.
|
|
360
|
+
const [cmd, path] = args;
|
|
361
|
+
if (cmd === "sync" && d && !d.error && "promptware-sync" in m.actions) {
|
|
362
|
+
const live = c as Client;
|
|
363
|
+
const syncs = async () => await live.read("promptware-plan", {}) as { syncs: number; error: string | null };
|
|
364
|
+
const before = (await syncs()).syncs;
|
|
365
|
+
await live.fire("promptware-sync", {});
|
|
366
|
+
for (let i = 0; i < 200; i++) {
|
|
367
|
+
const p = await syncs();
|
|
368
|
+
if (p.syncs > before) { if (p.error) return fail(`promptware sync: ${p.error}`); out(p); return 0; }
|
|
369
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
370
|
+
}
|
|
371
|
+
return fail("promptware sync: asked, not finished within 10s; promptware-plan says how it went");
|
|
372
|
+
}
|
|
373
|
+
const input = sources(project.dir, m, project.title);
|
|
374
|
+
if (cmd === "compile") { out(report(await planProject(project.dir, input))); return 0; }
|
|
375
|
+
if (cmd === "sync") { out(await syncProject(project.dir, input)); return 0; }
|
|
376
|
+
if (cmd === "output" && path) { const o = await outputOf(project.dir, input, path); out(json ? o : { path: o.path, status: o.status }); if (!json) sh.stdout(o.text); return 0; }
|
|
377
|
+
sh.stdout(`Usage: ${prog} promptware compile | sync | output PATH\n`);
|
|
378
|
+
return help ? 0 : 2;
|
|
379
|
+
}
|
|
380
|
+
if (!name) {
|
|
381
|
+
if (help) { sh.stdout(contract(prog, project.title, m, d?.error ?? null, down ? `down · ${prog} start` : (c as Client).where)); return 0; }
|
|
382
|
+
out(index(m, { via: down || !d ? "down" : (c as Client).where, ...(d ? { gen: d.gen, error: d.error } : {}), prog }), "index");
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
const action = m.actions[name];
|
|
386
|
+
const query = m.queries[name];
|
|
387
|
+
const def = action ?? query;
|
|
388
|
+
if (def) callUsage = [`${prog} ${name}`, ...usage(def.args, def.positional ?? []), ...(action ? ["[--key K]"] : [])].join(" ");
|
|
389
|
+
if (def && args.length === 1 && args[0] === "--schema") { out(schemaOf(m, name, prog), `${name}-schema`); return 0; }
|
|
390
|
+
if (def && help) {
|
|
391
|
+
const positional = def.positional ?? [];
|
|
392
|
+
const lines = describeArgs(def.args, positional);
|
|
393
|
+
const flagsToo = positional.length ? `\nA <positional> is also accepted as --name VALUE.\n` : "";
|
|
394
|
+
const key = action ? `\n--key K makes it idempotent: a retry with the same key answers the same without running again.\n` : "";
|
|
395
|
+
const example = exampleCall(name, def, prog).cli;
|
|
396
|
+
sh.stdout(`Usage: ${callUsage}\n\n${action ? "action" : "query"} of ${def.owner}${def.summary ? `: ${def.summary}` : ""}\n${lines.length ? "\n" + lines.join("\n") + "\n" : ""}${flagsToo}${key}\n${prog} ${name} --schema answers its schemas as JSON. The args as one JSON word are the canonical spelling:\n\n ${example}\n`);
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
// A hook with the baba down answers in the hook's protocol: let through with a note, or block when strict.
|
|
400
|
+
if (down && args.includes("--hook")) return await asHook(sh, () => Promise.reject(down), args[args.indexOf("--hook") + 1] ?? "", args.includes("--strict=true"));
|
|
401
|
+
if (down) throw down;
|
|
402
|
+
const client = c as Client;
|
|
403
|
+
const io: Io = { out, fit: (v, n) => budget(v, { dir: outDir, name: n, limit: g.max }), outDir, cwd: sh.cwd, stdin: sh.stdin, stdinTTY: sh.stdinTTY, interrupt: sh.interrupt };
|
|
404
|
+
if (name === "state") return await state(client, m, args, io);
|
|
405
|
+
if (name === "follow") return await follow(client, m, args, io);
|
|
406
|
+
if (name === "run" || name === "fire") return await batch(client, m, args, io);
|
|
407
|
+
if (name === "raw") return await rawOp(client, args, io);
|
|
408
|
+
if (!def) throw new ShellError("noaction", `${project.name} has no action or query ${name}; ${Object.keys(m.actions).length + Object.keys(m.queries).length ? `it has: ${[...Object.keys(m.actions), ...Object.keys(m.queries)].join(", ")}` : "it has none"}`);
|
|
409
|
+
const { args: a, extra } = await callArgs(args, def, sh.stdin, action ? ["key", "hook", "strict"] : []);
|
|
410
|
+
if (extra.hook !== undefined) return await asHook(sh, () => client.fire(name, a, { key: extra.key, coerce: true }), extra.hook, extra.strict === "true");
|
|
411
|
+
await out(query ? await client.read(name, a) : await client.fire(name, a, { key: extra.key, coerce: true }), name);
|
|
412
|
+
return 0;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
// A hook that could not reach its action answers in the hook's protocol, whatever went wrong on the way.
|
|
415
|
+
const ws = g.ws;
|
|
416
|
+
if (ws.includes("--hook")) return await asHook(sh, () => Promise.reject(err), ws[ws.indexOf("--hook") + 1] ?? "", ws.includes("--strict=true"));
|
|
417
|
+
return fail(err);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A baba the daemon does not run answers `down`; anything else is an error to raise. */
|
|
422
|
+
const downOnly = (err: unknown): NodeError => { if (err instanceof NodeError && err.code === "down") return err; throw err; };
|
|
423
|
+
|
|
424
|
+
/** What `status` and `start` answer: from describe, and where. */
|
|
425
|
+
function statusOf(d: Described, via: string) {
|
|
426
|
+
return { running: true, name: d.name, title: d.title, gen: d.gen, view: d.view, error: d.error, via };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** start: remembered, the daemon up (the service installed for it if need be), the baba opened there and answering. */
|
|
430
|
+
async function start(project: Project, prog: string, reg: Registry, sh: Shell, out: (v: unknown) => void): Promise<number> {
|
|
431
|
+
await reg.add(project.dir);
|
|
432
|
+
let d: Daemon | null = await daemon(sh.rootDir);
|
|
433
|
+
if (!d) {
|
|
434
|
+
if (!sh.service || (process.platform !== "darwin" && process.platform !== "linux")) throw new ShellError("down", `no daemon answers for ${sh.rootDir}; run \`voss gui\` with this root, then ${prog} start again`);
|
|
435
|
+
const st = await service.install((await service.status()).address ?? DEFAULT_ADDRESS);
|
|
436
|
+
for (const until = Date.now() + 60_000; !d && Date.now() < until;) { await Bun.sleep(250); d = await daemon(sh.rootDir); }
|
|
437
|
+
if (!d) throw new ShellError("down", `the daemon did not answer within 60 s; its log is ${st.log}`);
|
|
438
|
+
}
|
|
439
|
+
const r = await d.refresh();
|
|
440
|
+
const e = r.errors.find((x) => x.dir === project.dir);
|
|
441
|
+
if (e) throw new ShellError("failed", `${project.name} did not load: ${e.error}`);
|
|
442
|
+
for (const until = Date.now() + 10_000; ;) {
|
|
443
|
+
try {
|
|
444
|
+
const c = await connect(project, sh.rootDir, prog);
|
|
445
|
+
await out(statusOf(await c.describe(), c.where));
|
|
446
|
+
return 0;
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (!(err instanceof NodeError) || Date.now() > until) throw err;
|
|
449
|
+
await Bun.sleep(100);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** `voss baba` with no name: the contract, and where the state is. */
|
|
455
|
+
function contract(prog: string, title: string, m: Manifest, error: string | null, where: string): string {
|
|
456
|
+
const pad = (s: string) => s.padEnd(18);
|
|
457
|
+
const lines = [`${title}`, "", `Usage: ${prog} NAME '{…}' [--key K] | ${prog} NAME [WORD …] [--flag VALUE | @- …] | ${prog} NAME --schema`, ""];
|
|
458
|
+
const section = (head: string, rows: [string, string][]) => { if (rows.length) lines.push(`${head}:`, ...rows.map(([n, s]) => ` ${pad(n)} ${s}`), ""); };
|
|
459
|
+
section("Actions", Object.entries(m.actions).map(([n, a]) => [[n, ...usage(a.args, a.positional)].join(" "), `${a.owner}${a.summary ? ` · ${a.summary}` : ""}`]));
|
|
460
|
+
section("Queries", Object.entries(m.queries).map(([n, q]) => [[n, ...usage(q.args, q.positional)].join(" "), `${q.owner}${q.summary ? ` · ${q.summary}` : ""}`]));
|
|
461
|
+
section("Components", Object.entries(m.components).map(([n, c]) => [n, c.owner]));
|
|
462
|
+
section("Resources", Object.entries(m.resources).map(([n, r]) => [n, r.owner]));
|
|
463
|
+
section("Effects", Object.entries(m.effects).map(([n, e]) => [n, `${e.owner}${e.every ? ` · every ${e.every} ms` : ""}${e.retry ? ` · retry ${e.retry} ms` : ""}${e.summary ? ` · ${e.summary}` : ""}`]));
|
|
464
|
+
section("The state", Object.entries(builtinHelp).filter(([k]) => ["state", "follow", "run", "raw"].includes(k)).map(([k, h]) => [k, h]));
|
|
465
|
+
section("Voss", Object.entries(builtinHelp).filter(([k]) => ["start", "stop", "status", "mcp", "check", "manifest", "install", "add-system", "serve", "promptware"].includes(k)).map(([k, h]) => [k, h]));
|
|
466
|
+
lines.push(`State: ${where}${error ? ` · the last load failed: ${error}` : ""}`);
|
|
467
|
+
return lines.join("\n") + "\n";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export type { Snapshot };
|