@dharta/cli 0.6.77
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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/agent.d.ts +17 -0
- package/dist/agent.js +134 -0
- package/dist/agent.js.map +1 -0
- package/dist/api-error.d.ts +3 -0
- package/dist/api-error.js +80 -0
- package/dist/api-error.js.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +19 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +1978 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +131 -0
- package/dist/commands/agents.js +439 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/analytics.d.ts +25 -0
- package/dist/commands/analytics.js +166 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/api.d.ts +19 -0
- package/dist/commands/api.js +278 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/appearance.d.ts +19 -0
- package/dist/commands/appearance.js +63 -0
- package/dist/commands/appearance.js.map +1 -0
- package/dist/commands/artifact.d.ts +33 -0
- package/dist/commands/artifact.js +235 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/audit-log.d.ts +32 -0
- package/dist/commands/audit-log.js +102 -0
- package/dist/commands/audit-log.js.map +1 -0
- package/dist/commands/auth.d.ts +47 -0
- package/dist/commands/auth.js +117 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/caps.d.ts +82 -0
- package/dist/commands/caps.js +213 -0
- package/dist/commands/caps.js.map +1 -0
- package/dist/commands/create.d.ts +22 -0
- package/dist/commands/create.js +76 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +45 -0
- package/dist/commands/deploy.js +892 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +33 -0
- package/dist/commands/dev.js +655 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/directories.d.ts +60 -0
- package/dist/commands/directories.js +83 -0
- package/dist/commands/directories.js.map +1 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.js +123 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/embed-keys.d.ts +61 -0
- package/dist/commands/embed-keys.js +165 -0
- package/dist/commands/embed-keys.js.map +1 -0
- package/dist/commands/env.d.ts +59 -0
- package/dist/commands/env.js +300 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/files.d.ts +14 -0
- package/dist/commands/files.js +84 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/git-credential.d.ts +1 -0
- package/dist/commands/git-credential.js +70 -0
- package/dist/commands/git-credential.js.map +1 -0
- package/dist/commands/kartas.d.ts +101 -0
- package/dist/commands/kartas.js +289 -0
- package/dist/commands/kartas.js.map +1 -0
- package/dist/commands/keys.d.ts +33 -0
- package/dist/commands/keys.js +141 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +78 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +14 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/members.d.ts +32 -0
- package/dist/commands/members.js +77 -0
- package/dist/commands/members.js.map +1 -0
- package/dist/commands/model-keys.d.ts +22 -0
- package/dist/commands/model-keys.js +222 -0
- package/dist/commands/model-keys.js.map +1 -0
- package/dist/commands/open.d.ts +29 -0
- package/dist/commands/open.js +119 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/parity.d.ts +15 -0
- package/dist/commands/parity.js +73 -0
- package/dist/commands/parity.js.map +1 -0
- package/dist/commands/rollback.d.ts +21 -0
- package/dist/commands/rollback.js +26 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/schedules.d.ts +52 -0
- package/dist/commands/schedules.js +231 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/scopes.d.ts +3 -0
- package/dist/commands/scopes.js +21 -0
- package/dist/commands/scopes.js.map +1 -0
- package/dist/commands/sessions.d.ts +53 -0
- package/dist/commands/sessions.js +142 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/setup.d.ts +28 -0
- package/dist/commands/setup.js +949 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/status.d.ts +19 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +120 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/usage.d.ts +3 -0
- package/dist/commands/usage.js +37 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/webhooks.d.ts +47 -0
- package/dist/commands/webhooks.js +156 -0
- package/dist/commands/webhooks.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/completions.d.ts +13 -0
- package/dist/completions.js +120 -0
- package/dist/completions.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +38 -0
- package/dist/context.js.map +1 -0
- package/dist/device-flow.d.ts +9 -0
- package/dist/device-flow.js +127 -0
- package/dist/device-flow.js.map +1 -0
- package/dist/harness-preflight.d.ts +24 -0
- package/dist/harness-preflight.js +252 -0
- package/dist/harness-preflight.js.map +1 -0
- package/dist/harnesses.d.ts +11 -0
- package/dist/harnesses.js +112 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/login-offer.d.ts +9 -0
- package/dist/login-offer.js +36 -0
- package/dist/login-offer.js.map +1 -0
- package/dist/manifest.d.ts +15 -0
- package/dist/manifest.js +142 -0
- package/dist/manifest.js.map +1 -0
- package/dist/money.d.ts +1 -0
- package/dist/money.js +16 -0
- package/dist/money.js.map +1 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.js +20 -0
- package/dist/output.js.map +1 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/runtime.js +95 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scaffold.d.ts +16 -0
- package/dist/scaffold.js +74 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/setup-scan.d.ts +21 -0
- package/dist/setup-scan.js +127 -0
- package/dist/setup-scan.js.map +1 -0
- package/dist/templates.d.ts +14 -0
- package/dist/templates.js +65 -0
- package/dist/templates.js.map +1 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +116 -0
- package/dist/validators.js.map +1 -0
- package/dist/version-check.d.ts +25 -0
- package/dist/version-check.js +68 -0
- package/dist/version-check.js.map +1 -0
- package/node_modules/@dharta/sdk/LICENSE +21 -0
- package/node_modules/@dharta/sdk/README.md +87 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
- package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/errors.js +51 -0
- package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
- package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/http.js +136 -0
- package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
- package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/index.js +21 -0
- package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
- package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/resources.js +150 -0
- package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
- package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
- package/node_modules/@dharta/sdk/dist/types.js +59 -0
- package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
- package/node_modules/@dharta/sdk/package.json +32 -0
- package/node_modules/@dharta/sdk/src/errors.ts +58 -0
- package/node_modules/@dharta/sdk/src/http.ts +166 -0
- package/node_modules/@dharta/sdk/src/index.ts +36 -0
- package/node_modules/@dharta/sdk/src/resources.ts +197 -0
- package/node_modules/@dharta/sdk/src/types.ts +179 -0
- package/package.json +38 -0
- package/src/agent.ts +147 -0
- package/src/api-error.ts +88 -0
- package/src/browser.ts +20 -0
- package/src/cli.ts +2339 -0
- package/src/commands/agents.ts +635 -0
- package/src/commands/analytics.ts +181 -0
- package/src/commands/api.ts +297 -0
- package/src/commands/appearance.ts +79 -0
- package/src/commands/artifact.ts +331 -0
- package/src/commands/audit-log.ts +125 -0
- package/src/commands/auth.ts +186 -0
- package/src/commands/caps.ts +302 -0
- package/src/commands/create.ts +114 -0
- package/src/commands/deploy.ts +1122 -0
- package/src/commands/dev.ts +792 -0
- package/src/commands/directories.ts +136 -0
- package/src/commands/doctor.ts +138 -0
- package/src/commands/embed-keys.ts +237 -0
- package/src/commands/env.ts +369 -0
- package/src/commands/files.ts +124 -0
- package/src/commands/git-credential.ts +78 -0
- package/src/commands/kartas.ts +439 -0
- package/src/commands/keys.ts +206 -0
- package/src/commands/login.ts +115 -0
- package/src/commands/logs.ts +111 -0
- package/src/commands/members.ts +109 -0
- package/src/commands/model-keys.ts +278 -0
- package/src/commands/open.ts +173 -0
- package/src/commands/parity.ts +96 -0
- package/src/commands/rollback.ts +45 -0
- package/src/commands/schedules.ts +290 -0
- package/src/commands/scopes.ts +22 -0
- package/src/commands/sessions.ts +225 -0
- package/src/commands/setup.ts +1176 -0
- package/src/commands/status.ts +94 -0
- package/src/commands/update.ts +157 -0
- package/src/commands/usage.ts +41 -0
- package/src/commands/webhooks.ts +200 -0
- package/src/commands/whoami.ts +24 -0
- package/src/completions.ts +168 -0
- package/src/config.ts +143 -0
- package/src/context.ts +59 -0
- package/src/device-flow.ts +174 -0
- package/src/harness-preflight.ts +293 -0
- package/src/harnesses.ts +117 -0
- package/src/index.ts +3 -0
- package/src/login-offer.ts +49 -0
- package/src/manifest.ts +153 -0
- package/src/money.ts +15 -0
- package/src/output.ts +22 -0
- package/src/runtime.ts +127 -0
- package/src/scaffold.ts +102 -0
- package/src/setup-scan.ts +154 -0
- package/src/templates.ts +85 -0
- package/src/validators.ts +124 -0
- package/src/version-check.ts +75 -0
- package/templates/blank/CLAUDE.md +21 -0
- package/templates/blank/dharta.toml +11 -0
- package/templates/faq-agent/CLAUDE.md +51 -0
- package/templates/faq-agent/dharta.toml +11 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
// `dharta dev` (RFC 0013 CLI-4): the consumer preview. Spawns the pinned
|
|
2
|
+
// dharta-runtime serving this folder behind the same consumer session API
|
|
3
|
+
// production serves, then drives it as a plain HTTP/SSE client — the
|
|
4
|
+
// REPL has no privileged channel, so what works here works for the
|
|
5
|
+
// widget (D4). Running the native harness CLI in the folder is the authoring mode;
|
|
6
|
+
// this is the preview mode.
|
|
7
|
+
import { promises as fs } from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as net from "node:net";
|
|
10
|
+
import * as readline from "node:readline";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import TOML from "@iarna/toml";
|
|
13
|
+
import { resolveRuntime, spawnRuntime } from "../runtime.js";
|
|
14
|
+
import { buildContext } from "../context.js";
|
|
15
|
+
import { fetchRemoteEnv, prepareReleaseDir } from "./parity.js";
|
|
16
|
+
import { parseDotenvFile } from "./env.js";
|
|
17
|
+
import { describeFolder } from "../setup-scan.js";
|
|
18
|
+
import { hasAgentMarker } from "../harnesses.js";
|
|
19
|
+
import { normalizeNonBlank } from "../validators.js";
|
|
20
|
+
// `dharta dev [path]` in a multi-agent repo (RFC 0024 DEP-8): with no path
|
|
21
|
+
// given, serve cwd if it is itself an agent; else the sole agent folder
|
|
22
|
+
// directly below it; else ask which one. An explicit path always wins (the
|
|
23
|
+
// caller skips this). Shallow by design - dev previews one local agent.
|
|
24
|
+
export async function resolveAgentDir(dir, log) {
|
|
25
|
+
if (hasAgentMarker(dir))
|
|
26
|
+
return dir;
|
|
27
|
+
let subs = [];
|
|
28
|
+
try {
|
|
29
|
+
subs = (await fs.readdir(dir, { withFileTypes: true }))
|
|
30
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && hasAgentMarker(path.join(dir, e.name)))
|
|
31
|
+
.map((e) => e.name)
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return dir; // unreadable: serve as-is, miniDoctor reports the real problem
|
|
36
|
+
}
|
|
37
|
+
if (subs.length === 1) {
|
|
38
|
+
log(`Running the only agent here: ./${subs[0]}`);
|
|
39
|
+
return path.join(dir, subs[0]);
|
|
40
|
+
}
|
|
41
|
+
if (subs.length > 1) {
|
|
42
|
+
throw new Error(`Multiple agents here (${subs.join(", ")}). Pick one, e.g. dharta dev ./${subs[0]}`);
|
|
43
|
+
}
|
|
44
|
+
return dir; // no agent here or one level down - serve cwd (miniDoctor warns)
|
|
45
|
+
}
|
|
46
|
+
export async function dev(opts = {}) {
|
|
47
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
48
|
+
const write = opts.write ?? ((text) => process.stdout.write(text));
|
|
49
|
+
const fetchImpl = (opts.fetchImpl ?? globalThis.fetch).bind(globalThis);
|
|
50
|
+
if (opts.port !== undefined && (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535)) {
|
|
51
|
+
throw new Error("--port must be an integer between 1 and 65535.");
|
|
52
|
+
}
|
|
53
|
+
const normalizedDir = opts.dir === undefined ? undefined : normalizeNonBlank(opts.dir, "--dir");
|
|
54
|
+
const normalizedUser = opts.user === undefined ? undefined : normalizeNonBlank(opts.user, "--user");
|
|
55
|
+
const normalizedMessage = opts.message === undefined ? undefined : normalizeNonBlank(opts.message, "--message");
|
|
56
|
+
if (opts.includeSecrets && !opts.envRemote)
|
|
57
|
+
throw new Error("--include-secrets requires --env remote.");
|
|
58
|
+
let dir = path.resolve(normalizedDir ?? process.cwd());
|
|
59
|
+
if (normalizedDir !== undefined) {
|
|
60
|
+
await validateExplicitDevDir(dir);
|
|
61
|
+
}
|
|
62
|
+
// No explicit path given: pick the agent to preview (cwd, sole subdir, or
|
|
63
|
+
// ask) - RFC 0024 DEP-8. An explicit --dir / [path] is served as-is.
|
|
64
|
+
if (normalizedDir === undefined)
|
|
65
|
+
dir = await resolveAgentDir(dir, log);
|
|
66
|
+
let childEnv;
|
|
67
|
+
if (opts.release || opts.envRemote) {
|
|
68
|
+
const ctx = await buildContext({ fetch: opts.fetchImpl });
|
|
69
|
+
const inferOpts = { dir: normalizedDir, project: opts.project, fetchImpl: opts.fetchImpl };
|
|
70
|
+
if (opts.release) {
|
|
71
|
+
const release = await prepareReleaseDir(ctx, opts.release, inferOpts);
|
|
72
|
+
log(`Running release v${release.version} (${release.commitSha.slice(0, 8)}) - the exact tree prod serves. ` +
|
|
73
|
+
"Edits to it are NOT deploys; hot reload applies to this copy only.");
|
|
74
|
+
dir = release.dir;
|
|
75
|
+
}
|
|
76
|
+
if (opts.envRemote) {
|
|
77
|
+
const remote = await fetchRemoteEnv(ctx, { ...inferOpts, includeSecrets: opts.includeSecrets });
|
|
78
|
+
const names = Object.keys(remote.values);
|
|
79
|
+
childEnv = { ...process.env, ...remote.values };
|
|
80
|
+
log(`Bound ${names.length} prod env var${names.length === 1 ? "" : "s"}: ${names.join(", ") || "(none)"}`);
|
|
81
|
+
if (remote.withheld.length > 0) {
|
|
82
|
+
log(`${remote.withheld.length} secret value(s) withheld (${remote.withheld.join(", ")}) - ` +
|
|
83
|
+
"calls through them will fail; --include-secrets with env:secrets:read fetches them (audited).");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const agentName = (await describeFolder(dir, ".")).name ?? path.basename(dir);
|
|
88
|
+
log(`Dharta dev mode: ${agentName}`);
|
|
89
|
+
log("");
|
|
90
|
+
for (const warning of await miniDoctor(dir))
|
|
91
|
+
log(`[doctor] ${warning}`);
|
|
92
|
+
let baseUrl = opts.attach;
|
|
93
|
+
let child = null;
|
|
94
|
+
let spawnError = null;
|
|
95
|
+
let runtimeOutput = null;
|
|
96
|
+
let killTurn = null; // set per-turn: aborts a wedged/crashed turn
|
|
97
|
+
if (!baseUrl) {
|
|
98
|
+
const runtime = await resolveRuntime();
|
|
99
|
+
const port = opts.port ?? (await freePort());
|
|
100
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
101
|
+
log(`✓ Starting agent runtime (${runtime.description})…`);
|
|
102
|
+
child = spawnRuntime(runtime, ["dev-serve", dir, "--host", "127.0.0.1", "--port", String(port)], {
|
|
103
|
+
stdio: "pipe",
|
|
104
|
+
env: childEnv,
|
|
105
|
+
});
|
|
106
|
+
// A spawn error (binary missing, not executable) never fires `exit` —
|
|
107
|
+
// capture it so waitForHealthy fails fast with the remedy, not a
|
|
108
|
+
// stack trace after a 120s timeout.
|
|
109
|
+
child.on("error", (err) => {
|
|
110
|
+
spawnError = new Error(`Could not launch the agent runtime (${err.message}).\n` +
|
|
111
|
+
"Run `dharta doctor` to diagnose; DHARTA_RUNTIME=/path/to/checkout is the escape hatch.");
|
|
112
|
+
});
|
|
113
|
+
runtimeOutput = forwardRuntimeOutput(child, { stream: !!opts.verbose, onFatal: () => killTurn?.() });
|
|
114
|
+
}
|
|
115
|
+
// The runtime must die with us, including on plain `kill` — an
|
|
116
|
+
// orphaned uvicorn keeps the port bound.
|
|
117
|
+
const onSignal = (signal) => {
|
|
118
|
+
if (child)
|
|
119
|
+
child.kill("SIGTERM");
|
|
120
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
121
|
+
};
|
|
122
|
+
process.once("SIGINT", onSignal);
|
|
123
|
+
process.once("SIGTERM", onSignal);
|
|
124
|
+
let rl = null;
|
|
125
|
+
try {
|
|
126
|
+
try {
|
|
127
|
+
await waitForHealthy(baseUrl, fetchImpl, child, opts.healthTimeoutMs ?? 120_000, () => spawnError);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// The remedial message says "see [runtime] output" - make that
|
|
131
|
+
// true even when the runtime stream was quieted by default.
|
|
132
|
+
runtimeOutput?.dump();
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
log("✓ Agent ready");
|
|
136
|
+
log(` Local agent API: ${baseUrl}/v1`);
|
|
137
|
+
if (opts.verbose) {
|
|
138
|
+
log(" Point @dharta/widget or your frontend at it.");
|
|
139
|
+
log(" Preview parity is code + config, not infrastructure: no microVM, no per-user");
|
|
140
|
+
log(" workspaces, and turns run on YOUR harness credentials (prod injects its own).");
|
|
141
|
+
}
|
|
142
|
+
// The agent turn spawns `claude` on YOUR credentials; with none it exits 1.
|
|
143
|
+
// Hard-stop here rather than drop into a chat that can only fail (spawn
|
|
144
|
+
// path only - an attached runtime carries its own credentials).
|
|
145
|
+
if (child && !process.env.ANTHROPIC_API_KEY && !process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
146
|
+
log("");
|
|
147
|
+
log("! Error: No model credential found. Two options -");
|
|
148
|
+
log(" export ANTHROPIC_API_KEY=<your Anthropic API key>");
|
|
149
|
+
log(" OR");
|
|
150
|
+
log(" export CLAUDE_CODE_OAUTH_TOKEN=<Claude Code token from `claude setup-token`>");
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const session = await createSession(baseUrl, fetchImpl, normalizedUser);
|
|
155
|
+
if (normalizedUser)
|
|
156
|
+
log(`Simulating end user '${normalizedUser}'.`);
|
|
157
|
+
rl = readline.createInterface({
|
|
158
|
+
input: opts.input ?? process.stdin,
|
|
159
|
+
output: undefined,
|
|
160
|
+
terminal: false,
|
|
161
|
+
});
|
|
162
|
+
// SINGLE consumer for all input: turn text and approval answers pull
|
|
163
|
+
// from the same iterator. A second listener (rl.once("line")) would
|
|
164
|
+
// race the for-await loop — interactively the approval answer leaks
|
|
165
|
+
// back in as a user turn; with piped input it deadlocks outright.
|
|
166
|
+
const lines = rl[Symbol.asyncIterator]();
|
|
167
|
+
const ask = async (question) => {
|
|
168
|
+
write(question);
|
|
169
|
+
const { value, done } = await lines.next();
|
|
170
|
+
return done ? null : value; // null: input exhausted, nobody can answer
|
|
171
|
+
};
|
|
172
|
+
const idleMs = opts.turnIdleMs ?? 60_000;
|
|
173
|
+
// Run one turn but never hang: abort if the runtime crashes (onFatal) or
|
|
174
|
+
// goes silent for idleMs, then surface the buffered runtime output. The
|
|
175
|
+
// idle timer pauses while we wait on a human (approval prompt).
|
|
176
|
+
const runTurnWatched = async (turnText, prefix) => {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
let idle;
|
|
179
|
+
let produced = false; // did the agent write any content this turn?
|
|
180
|
+
const arm = () => {
|
|
181
|
+
clearTimeout(idle);
|
|
182
|
+
idle = setTimeout(() => controller.abort(), idleMs);
|
|
183
|
+
};
|
|
184
|
+
const tracked = (t) => {
|
|
185
|
+
if (t && !produced) {
|
|
186
|
+
produced = true;
|
|
187
|
+
if (prefix)
|
|
188
|
+
write(prefix); // print "<agent>: " only once real output arrives
|
|
189
|
+
}
|
|
190
|
+
arm();
|
|
191
|
+
write(t);
|
|
192
|
+
};
|
|
193
|
+
const pausedAsk = async (q) => {
|
|
194
|
+
clearTimeout(idle); // don't time out while waiting on the human
|
|
195
|
+
const answer = await ask(q);
|
|
196
|
+
arm();
|
|
197
|
+
return answer;
|
|
198
|
+
};
|
|
199
|
+
killTurn = () => controller.abort();
|
|
200
|
+
arm();
|
|
201
|
+
const aborted = new Promise((_, reject) => {
|
|
202
|
+
controller.signal.addEventListener("abort", () => reject(new Error("turn aborted")), { once: true });
|
|
203
|
+
});
|
|
204
|
+
const turn = runTurn(baseUrl, fetchImpl, session, turnText, { write: tracked, log, ask: pausedAsk }, controller.signal);
|
|
205
|
+
try {
|
|
206
|
+
// Race the turn against the abort. A real fetch unblocks its body read
|
|
207
|
+
// on signal, but racing also covers a stream that wedges open without
|
|
208
|
+
// ever erroring (the crashed-subprocess shape).
|
|
209
|
+
await Promise.race([turn, aborted]);
|
|
210
|
+
if (!produced) {
|
|
211
|
+
// The turn ended without crashing or stalling, but said nothing - the
|
|
212
|
+
// runtime swallowed the cause (often the missing credential above).
|
|
213
|
+
runtimeOutput?.dump();
|
|
214
|
+
log("[no response] the agent produced no output - see the note above, or rerun with `dharta dev --verbose`.");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
if (controller.signal.aborted) {
|
|
219
|
+
turn.catch(() => { }); // detach the abandoned read
|
|
220
|
+
runtimeOutput?.dump();
|
|
221
|
+
throw new Error("No response - the agent runtime errored or went silent (see [runtime] output above). " +
|
|
222
|
+
"A missing model credential (ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN) is the usual cause.");
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
clearTimeout(idle);
|
|
228
|
+
killTurn = null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
if (normalizedMessage !== undefined) {
|
|
232
|
+
await runTurnWatched(normalizedMessage, "");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
log(" Chat with your agent. Type 'exit' to quit; edits to the harness instruction file hot-reload.");
|
|
236
|
+
for (;;) {
|
|
237
|
+
write("\nUser: ");
|
|
238
|
+
const { value, done } = await lines.next();
|
|
239
|
+
if (done)
|
|
240
|
+
break;
|
|
241
|
+
const text = value.trim();
|
|
242
|
+
if (text === "")
|
|
243
|
+
continue;
|
|
244
|
+
if (text === "exit" || text === "quit")
|
|
245
|
+
break;
|
|
246
|
+
try {
|
|
247
|
+
await runTurnWatched(text, `\n${agentName}: `);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
// One failed turn must not kill the session (or the runtime).
|
|
251
|
+
log(`[error] ${err instanceof Error ? err.message : String(err)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
rl?.close();
|
|
257
|
+
process.removeListener("SIGINT", onSignal);
|
|
258
|
+
process.removeListener("SIGTERM", onSignal);
|
|
259
|
+
if (child)
|
|
260
|
+
child.kill("SIGTERM");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function validateExplicitDevDir(dir) {
|
|
264
|
+
try {
|
|
265
|
+
const stat = await fs.stat(dir);
|
|
266
|
+
if (!stat.isDirectory()) {
|
|
267
|
+
throw new Error(`Cannot run dev in ${dir}: expected a directory.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error.code === "ENOENT") {
|
|
272
|
+
throw new Error(`Cannot run dev in ${dir}: directory does not exist.`);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// --- mini-doctor -----------------------------------------------------------
|
|
278
|
+
// The dev-start warnings half of declare-or-bind (DEV_PROD_PARITY §7):
|
|
279
|
+
// the same conditions fail the push-time build, so nothing said here is
|
|
280
|
+
// a surprise later. Full [env] tooling (env list/set/diff) is CLI-8.
|
|
281
|
+
export async function miniDoctor(dir) {
|
|
282
|
+
const warnings = [];
|
|
283
|
+
const karta = await readOptionalLocalFile(path.join(dir, "dharta.toml"), "dharta.toml");
|
|
284
|
+
if (karta.warning)
|
|
285
|
+
warnings.push(karta.warning);
|
|
286
|
+
const kartaToml = karta.content;
|
|
287
|
+
if (kartaToml === null) {
|
|
288
|
+
warnings.push("dharta.toml is missing - the push-time build will reject this folder. `dharta setup` scaffolds one.");
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = TOML.parse(kartaToml);
|
|
293
|
+
const env = parsed.env;
|
|
294
|
+
if (env) {
|
|
295
|
+
const localEnv = await readOptionalLocalFile(path.join(dir, ".env"), ".env");
|
|
296
|
+
if (localEnv.warning)
|
|
297
|
+
warnings.push(localEnv.warning);
|
|
298
|
+
const dotenv = parseDotenvFile(localEnv.content);
|
|
299
|
+
for (const [name, spec] of Object.entries(env)) {
|
|
300
|
+
const required = typeof spec === "object" && spec !== null &&
|
|
301
|
+
spec.required === true;
|
|
302
|
+
if (required && !(name in process.env) && !(name in dotenv)) {
|
|
303
|
+
warnings.push(`Required env var ${name} is unbound - add it to .env (the push-time build fails on this too).`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
warnings.push("dharta.toml could not be parsed - fix it before pushing.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const localMcp = await readOptionalLocalFile(path.join(dir, ".mcp.json"), ".mcp.json");
|
|
313
|
+
if (localMcp.warning)
|
|
314
|
+
warnings.push(localMcp.warning);
|
|
315
|
+
const mcp = localMcp.content;
|
|
316
|
+
if (mcp !== null && /localhost|127\.0\.0\.1/.test(mcp)) {
|
|
317
|
+
warnings.push(".mcp.json points at localhost - that endpoint won't exist in production. " +
|
|
318
|
+
"Reference ${VAR} and bind it per environment (declare-or-bind).");
|
|
319
|
+
}
|
|
320
|
+
return warnings;
|
|
321
|
+
}
|
|
322
|
+
async function readOptionalLocalFile(file, label) {
|
|
323
|
+
try {
|
|
324
|
+
return { content: await fs.readFile(file, "utf8") };
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const code = error.code;
|
|
328
|
+
if (code === "ENOENT")
|
|
329
|
+
return { content: null };
|
|
330
|
+
if (code === "EISDIR") {
|
|
331
|
+
return { content: null, warning: `Cannot read ${label} at ${file}: expected a file, got a directory.` };
|
|
332
|
+
}
|
|
333
|
+
return { content: null, warning: `Cannot read ${label} at ${file}: ${error.message}` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// --- runtime lifecycle -----------------------------------------------------
|
|
337
|
+
// TOCTOU between this probe and the child binding is accepted: the
|
|
338
|
+
// loser exits immediately and waitForHealthy reports it within ~500ms.
|
|
339
|
+
function freePort() {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const srv = net.createServer();
|
|
342
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
343
|
+
const address = srv.address();
|
|
344
|
+
srv.close(() => typeof address === "object" && address
|
|
345
|
+
? resolve(address.port)
|
|
346
|
+
: reject(new Error("could not allocate a local port")));
|
|
347
|
+
});
|
|
348
|
+
srv.on("error", reject);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// Quiet by default: the runtime's banner/uvicorn lines are noise in
|
|
352
|
+
// the REPL, but they are exactly what explains a failed start - so
|
|
353
|
+
// always buffer, stream only with --verbose, and dump the buffer when
|
|
354
|
+
// startup fails.
|
|
355
|
+
function forwardRuntimeOutput(child, { stream: streamLive, onFatal }) {
|
|
356
|
+
const buffered = [];
|
|
357
|
+
const emit = (line) => process.stderr.write(`[runtime] ${line}\n`);
|
|
358
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
359
|
+
let pending = "";
|
|
360
|
+
const handle = (line) => {
|
|
361
|
+
if (!line.trim())
|
|
362
|
+
return;
|
|
363
|
+
// An unhandled Python traceback means the in-flight turn crashed; the SSE
|
|
364
|
+
// stream can wedge open, so signal the turn to abort rather than hang on
|
|
365
|
+
// bytes that will never come.
|
|
366
|
+
if (onFatal && /Traceback \(most recent call last\)/.test(line))
|
|
367
|
+
onFatal();
|
|
368
|
+
if (streamLive)
|
|
369
|
+
emit(line);
|
|
370
|
+
else {
|
|
371
|
+
buffered.push(line);
|
|
372
|
+
if (buffered.length > 200)
|
|
373
|
+
buffered.shift();
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
stream?.on("data", (chunk) => {
|
|
377
|
+
pending += chunk.toString("utf8");
|
|
378
|
+
const lines = pending.split("\n");
|
|
379
|
+
pending = lines.pop() ?? "";
|
|
380
|
+
lines.forEach(handle);
|
|
381
|
+
});
|
|
382
|
+
stream?.on("end", () => handle(pending));
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
dump() {
|
|
386
|
+
buffered.forEach(emit);
|
|
387
|
+
buffered.length = 0;
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function waitForHealthy(baseUrl, fetchImpl, child, timeoutMs, spawnError = () => null) {
|
|
392
|
+
const deadline = Date.now() + timeoutMs;
|
|
393
|
+
let lastDetail = "";
|
|
394
|
+
let exited = false;
|
|
395
|
+
child?.on("exit", () => {
|
|
396
|
+
exited = true;
|
|
397
|
+
});
|
|
398
|
+
while (Date.now() < deadline) {
|
|
399
|
+
const failed = spawnError();
|
|
400
|
+
if (failed)
|
|
401
|
+
throw failed;
|
|
402
|
+
if (exited) {
|
|
403
|
+
throw new Error("The agent runtime exited before it became ready (see [runtime] output above). " +
|
|
404
|
+
"Run `dharta doctor` to diagnose; DHARTA_RUNTIME=/path/to/checkout is the escape hatch.");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const res = await fetchImpl(`${baseUrl}/healthz`);
|
|
408
|
+
if (res.ok)
|
|
409
|
+
return;
|
|
410
|
+
if (res.status === 503) {
|
|
411
|
+
const body = (await res.json().catch(() => null));
|
|
412
|
+
lastDetail = body?.detail ?? "";
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// not listening yet
|
|
417
|
+
}
|
|
418
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
419
|
+
}
|
|
420
|
+
throw new Error(`The agent runtime did not become ready within ${Math.round(timeoutMs / 1000)}s` +
|
|
421
|
+
(lastDetail ? ` (${lastDetail})` : "") +
|
|
422
|
+
". Run `dharta doctor` to diagnose.");
|
|
423
|
+
}
|
|
424
|
+
// --- protocol client -------------------------------------------------------
|
|
425
|
+
async function createSession(baseUrl, fetchImpl, endUserId) {
|
|
426
|
+
const res = await fetchAgentApi(baseUrl, fetchImpl, "/v1/sessions", "create a session", {
|
|
427
|
+
method: "POST",
|
|
428
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
|
|
429
|
+
body: JSON.stringify({ metadata: endUserId ? { end_user_id: endUserId } : {} }),
|
|
430
|
+
});
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
throw new Error(`Could not create a session (HTTP ${res.status}). See the [runtime] output above for the cause.`);
|
|
433
|
+
}
|
|
434
|
+
const body = await readAgentJson(res, "create a session");
|
|
435
|
+
if (typeof body.id !== "string" || body.id.length === 0) {
|
|
436
|
+
throw new Error("Could not create a session: the agent API response did not include a session id.");
|
|
437
|
+
}
|
|
438
|
+
return body.id;
|
|
439
|
+
}
|
|
440
|
+
export async function runTurn(baseUrl, fetchImpl, sessionId, text, io, signal) {
|
|
441
|
+
const res = await fetchAgentApi(baseUrl, fetchImpl, `/v1/sessions/${sessionId}/messages`, "send the message", {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
Accept: "text/event-stream",
|
|
446
|
+
"Idempotency-Key": randomUUID(),
|
|
447
|
+
},
|
|
448
|
+
body: JSON.stringify({ text, stream: true }),
|
|
449
|
+
signal,
|
|
450
|
+
});
|
|
451
|
+
if (!res.ok || !res.body) {
|
|
452
|
+
throw new Error(`Turn failed (HTTP ${res.status}). Retry the message; if it persists, check ` +
|
|
453
|
+
"the [runtime] output (dharta dev) or `dharta logs [slug]` (deployed).");
|
|
454
|
+
}
|
|
455
|
+
const pending = await renderEventStream(parseSse(res.body), io);
|
|
456
|
+
await resolvePendingInputs(baseUrl, fetchImpl, sessionId, pending, io);
|
|
457
|
+
}
|
|
458
|
+
// Approval loop: the harness pauses on tool permissions; the decision
|
|
459
|
+
// goes back over the same API the widget uses. The PUT's response is
|
|
460
|
+
// itself a batch of events (it may pause again).
|
|
461
|
+
async function resolvePendingInputs(baseUrl, fetchImpl, sessionId, first, io) {
|
|
462
|
+
let pending = first;
|
|
463
|
+
let abortHintShown = false;
|
|
464
|
+
while (pending) {
|
|
465
|
+
const prompt = pending.message ?? `The agent wants to use ${pending.tool ?? "a tool"}`;
|
|
466
|
+
const answer = await io.ask(`\n${prompt} - approve? [y=once / s=session / n=deny] `);
|
|
467
|
+
if (answer === null && !abortHintShown) {
|
|
468
|
+
// No TTY / piped input ran out: nobody can answer, so the tool is denied
|
|
469
|
+
// and the turn stalls. Name the bounded-authority fix once instead of
|
|
470
|
+
// leaving the failure unexplained (issue #857).
|
|
471
|
+
io.log(`[approval] "${pending.tool ?? "a tool"}" is not on the .claude/settings.json allowlist and this ` +
|
|
472
|
+
`run is non-interactive, so it was denied. Allow the tools your agent needs in .claude/settings.json ` +
|
|
473
|
+
`(bounded authority), run \`dharta dev\` interactively to approve as you go, or set ` +
|
|
474
|
+
`runtime.permission_mode = "autonomous" in dharta.jsonc.`);
|
|
475
|
+
abortHintShown = true;
|
|
476
|
+
}
|
|
477
|
+
const decision = answer === null
|
|
478
|
+
? "deny"
|
|
479
|
+
: answer.trim().toLowerCase() === "y"
|
|
480
|
+
? "approve_once"
|
|
481
|
+
: answer.trim().toLowerCase() === "s"
|
|
482
|
+
? "approve_session"
|
|
483
|
+
: "deny";
|
|
484
|
+
const res = await fetchAgentApi(baseUrl, fetchImpl, `/v1/sessions/${sessionId}/inputs/${pending.request_id}`, "deliver the decision", {
|
|
485
|
+
method: "PUT",
|
|
486
|
+
headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
|
|
487
|
+
body: JSON.stringify({ decision }),
|
|
488
|
+
});
|
|
489
|
+
if (!res.ok) {
|
|
490
|
+
throw new Error(`Could not deliver the decision (HTTP ${res.status}). Re-send the message to retry.`);
|
|
491
|
+
}
|
|
492
|
+
const body = await readAgentJson(res, "deliver the decision");
|
|
493
|
+
pending = null;
|
|
494
|
+
const renderer = makeRenderer(io);
|
|
495
|
+
for (const raw of body.events ?? []) {
|
|
496
|
+
const next = renderer(raw);
|
|
497
|
+
if (next)
|
|
498
|
+
pending = next;
|
|
499
|
+
}
|
|
500
|
+
renderer.finish();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function fetchAgentApi(baseUrl, fetchImpl, path, operation, init) {
|
|
504
|
+
try {
|
|
505
|
+
return await fetchImpl(`${baseUrl}${path}`, init);
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
if (init?.signal?.aborted)
|
|
509
|
+
throw error;
|
|
510
|
+
throw new Error(`Could not ${operation}: could not reach the agent API at ${baseUrl} (${formatCause(error)}). ` +
|
|
511
|
+
"For local preview, rerun with `dharta dev --verbose` or run `dharta doctor`; " +
|
|
512
|
+
"for deployed agents, check `dharta logs [slug]`.");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function readAgentJson(res, operation) {
|
|
516
|
+
try {
|
|
517
|
+
return (await res.json());
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
throw new Error(`Could not ${operation}: the agent API returned invalid JSON (${formatCause(error)}). ` +
|
|
521
|
+
"Retry the command; if it persists, check the agent runtime output or `dharta logs [slug]`.");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function formatCause(error) {
|
|
525
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
526
|
+
}
|
|
527
|
+
async function renderEventStream(events, io) {
|
|
528
|
+
const renderer = makeRenderer(io);
|
|
529
|
+
let pending = null;
|
|
530
|
+
for await (const { data } of events) {
|
|
531
|
+
const next = renderer(data);
|
|
532
|
+
if (next)
|
|
533
|
+
pending = next;
|
|
534
|
+
}
|
|
535
|
+
renderer.finish();
|
|
536
|
+
return pending;
|
|
537
|
+
}
|
|
538
|
+
// Mirrors the Python CLIRenderer's contract: text events carry the
|
|
539
|
+
// CUMULATIVE text per part id; print only the delta.
|
|
540
|
+
function makeRenderer(io) {
|
|
541
|
+
const textByPart = new Map();
|
|
542
|
+
let lineOpen = false;
|
|
543
|
+
const render = (event) => {
|
|
544
|
+
const data = event.data ?? {};
|
|
545
|
+
switch (event.type) {
|
|
546
|
+
case "text": {
|
|
547
|
+
const part = data.part;
|
|
548
|
+
if (!part || typeof part.text !== "string")
|
|
549
|
+
return null;
|
|
550
|
+
const id = part.id ?? "__default__";
|
|
551
|
+
const previous = textByPart.get(id) ?? "";
|
|
552
|
+
const delta = part.text.startsWith(previous)
|
|
553
|
+
? part.text.slice(previous.length)
|
|
554
|
+
: part.text;
|
|
555
|
+
if (delta) {
|
|
556
|
+
io.write(delta);
|
|
557
|
+
lineOpen = true;
|
|
558
|
+
}
|
|
559
|
+
textByPart.set(id, part.text);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
case "tool_use": {
|
|
563
|
+
const part = data.part;
|
|
564
|
+
if (lineOpen) {
|
|
565
|
+
io.write("\n");
|
|
566
|
+
lineOpen = false;
|
|
567
|
+
}
|
|
568
|
+
io.log(`[tool] ${part?.tool ?? "tool"}`);
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
case "input_required":
|
|
572
|
+
if (lineOpen) {
|
|
573
|
+
io.write("\n");
|
|
574
|
+
lineOpen = false;
|
|
575
|
+
}
|
|
576
|
+
return data;
|
|
577
|
+
case "error": {
|
|
578
|
+
if (lineOpen) {
|
|
579
|
+
io.write("\n");
|
|
580
|
+
lineOpen = false;
|
|
581
|
+
}
|
|
582
|
+
io.log(`[error] ${JSON.stringify(data)}`);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
default:
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
render.finish = () => {
|
|
590
|
+
if (lineOpen)
|
|
591
|
+
io.write("\n");
|
|
592
|
+
};
|
|
593
|
+
return render;
|
|
594
|
+
}
|
|
595
|
+
// Minimal SSE parser: "event:"/"data:" lines, frames separated by a
|
|
596
|
+
// blank line (LF or CRLF). Enough for the runtime's encode_sse output.
|
|
597
|
+
export async function* parseSse(body) {
|
|
598
|
+
let buffer = "";
|
|
599
|
+
const decoder = new TextDecoder();
|
|
600
|
+
const chunks = typeof body.getReader === "function"
|
|
601
|
+
? webStreamChunks(body)
|
|
602
|
+
: body;
|
|
603
|
+
for await (const chunk of chunks) {
|
|
604
|
+
buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
605
|
+
// Normalize CRLF so a \r\n-emitting server doesn't buffer forever.
|
|
606
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
607
|
+
let sep;
|
|
608
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
609
|
+
const frame = buffer.slice(0, sep);
|
|
610
|
+
buffer = buffer.slice(sep + 2);
|
|
611
|
+
const parsed = parseFrame(frame);
|
|
612
|
+
if (parsed)
|
|
613
|
+
yield parsed;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const tail = parseFrame(buffer);
|
|
617
|
+
if (tail)
|
|
618
|
+
yield tail;
|
|
619
|
+
}
|
|
620
|
+
async function* webStreamChunks(stream) {
|
|
621
|
+
const reader = stream.getReader();
|
|
622
|
+
try {
|
|
623
|
+
for (;;) {
|
|
624
|
+
const { done, value } = await reader.read();
|
|
625
|
+
if (done)
|
|
626
|
+
return;
|
|
627
|
+
if (value)
|
|
628
|
+
yield value;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
reader.releaseLock();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function parseFrame(frame) {
|
|
636
|
+
let event = "";
|
|
637
|
+
const dataLines = [];
|
|
638
|
+
for (const line of frame.split("\n")) {
|
|
639
|
+
if (line.startsWith("event:"))
|
|
640
|
+
event = line.slice(6).trim();
|
|
641
|
+
else if (line.startsWith("data:"))
|
|
642
|
+
dataLines.push(line.slice(5).trim());
|
|
643
|
+
}
|
|
644
|
+
if (!event && dataLines.length === 0)
|
|
645
|
+
return null;
|
|
646
|
+
let data = {};
|
|
647
|
+
try {
|
|
648
|
+
data = JSON.parse(dataLines.join("\n"));
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// non-JSON data: leave empty
|
|
652
|
+
}
|
|
653
|
+
return { event, data };
|
|
654
|
+
}
|
|
655
|
+
//# sourceMappingURL=dev.js.map
|