create-syrinx-agent 4.4.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.
- package/LICENSE +22 -0
- package/README.md +91 -0
- package/dist/index.js +1100 -0
- package/dist/index.js.map +7 -0
- package/package.json +45 -0
- package/src/cli.ts +76 -0
- package/src/exit-codes.ts +20 -0
- package/src/generate.ts +35 -0
- package/src/help.ts +45 -0
- package/src/index.ts +16 -0
- package/src/options.test.ts +130 -0
- package/src/options.ts +176 -0
- package/src/providers/realtime.ts +31 -0
- package/src/providers/reasoner.ts +69 -0
- package/src/providers/stt.ts +68 -0
- package/src/providers/tts.ts +69 -0
- package/src/providers/types.ts +58 -0
- package/src/providers/vad-endpointing.ts +36 -0
- package/src/templates/agent-module.ts +188 -0
- package/src/templates/agents-md.ts +65 -0
- package/src/templates/cloudflare-worker.ts +172 -0
- package/src/templates/dev-server.ts +104 -0
- package/src/templates/env-example.ts +58 -0
- package/src/templates/fixture.ts +34 -0
- package/src/templates/package-json.ts +119 -0
- package/src/templates/tsconfig.ts +25 -0
- package/src/version.ts +5 -0
- package/src/write-project.test.ts +163 -0
- package/src/write-project.ts +59 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Builds the generated project's package.json — dependencies limited to
|
|
4
|
+
// exactly the chosen providers (LDT-21 requirement), plus the fixed
|
|
5
|
+
// infrastructure every combination needs: core, the CLI (for `syrinx turn`/
|
|
6
|
+
// `syrinx text` in the check:* scripts), and tsx (the --agent seam needs it
|
|
7
|
+
// for a .ts module — see AGENTS.md).
|
|
8
|
+
|
|
9
|
+
import type { ResolvedOptions } from "../options.js";
|
|
10
|
+
import { STT_STAGE_PROVIDERS } from "../providers/stt.js";
|
|
11
|
+
import { TTS_STAGE_PROVIDERS } from "../providers/tts.js";
|
|
12
|
+
import { REASONER_PROVIDERS } from "../providers/reasoner.js";
|
|
13
|
+
import { REALTIME_PROVIDERS } from "../providers/realtime.js";
|
|
14
|
+
import { VAD_SIDECAR_PROVIDERS, ENDPOINTING_SIDECAR_PROVIDERS } from "../providers/vad-endpointing.js";
|
|
15
|
+
|
|
16
|
+
const CORE_VERSION = "^4.3.0";
|
|
17
|
+
|
|
18
|
+
// @kuralle-syrinx/server-websocket (and any ws-based STT/TTS/realtime provider,
|
|
19
|
+
// transitively through @kuralle-syrinx/ws) ships raw TS as "main" and declares
|
|
20
|
+
// `ws` as a runtime dep but `@types/ws` only as a devDependency — devDeps of a
|
|
21
|
+
// dependency are never installed for the consumer, so a project that typechecks
|
|
22
|
+
// server-websocket's source needs its own `@types/ws` (mirrors
|
|
23
|
+
// examples/02-hello-voice-headless/package.json, which declares both directly).
|
|
24
|
+
function transportPackage(): Readonly<Record<string, string>> {
|
|
25
|
+
return { "@kuralle-syrinx/server-websocket": CORE_VERSION, ws: "^8.21.0" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function collectDependencies(opts: ResolvedOptions): Record<string, string> {
|
|
29
|
+
const deps: Record<string, string> = {
|
|
30
|
+
"@kuralle-syrinx/core": CORE_VERSION,
|
|
31
|
+
...transportPackage(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (opts.mode === "cascade") {
|
|
35
|
+
const stt = STT_STAGE_PROVIDERS[opts.stt];
|
|
36
|
+
const tts = TTS_STAGE_PROVIDERS[opts.tts];
|
|
37
|
+
const reasoner = REASONER_PROVIDERS[opts.reasoner];
|
|
38
|
+
if (stt) deps[stt.packageName] = stt.packageVersion;
|
|
39
|
+
if (tts) deps[tts.packageName] = tts.packageVersion;
|
|
40
|
+
// ReasoningBridge (the VoicePlugin wrapper any Reasoner needs in cascade mode) lives here.
|
|
41
|
+
deps["@kuralle-syrinx/aisdk"] = CORE_VERSION;
|
|
42
|
+
if (reasoner) {
|
|
43
|
+
deps[reasoner.packageName] = reasoner.packageVersion;
|
|
44
|
+
Object.assign(deps, reasoner.extraPackages);
|
|
45
|
+
}
|
|
46
|
+
const vad = opts.vad !== undefined ? VAD_SIDECAR_PROVIDERS[opts.vad] : undefined;
|
|
47
|
+
if (vad) deps[vad.packageName] = vad.packageVersion;
|
|
48
|
+
const eos = opts.endpointing !== undefined ? ENDPOINTING_SIDECAR_PROVIDERS[opts.endpointing] : undefined;
|
|
49
|
+
if (eos) deps[eos.packageName] = eos.packageVersion;
|
|
50
|
+
} else {
|
|
51
|
+
const realtime = REALTIME_PROVIDERS[opts.realtime];
|
|
52
|
+
// RealtimeBridge (the generic VoicePlugin wrapper around any RealtimeAdapter) lives here.
|
|
53
|
+
deps["@kuralle-syrinx/realtime"] = CORE_VERSION;
|
|
54
|
+
deps["@kuralle-syrinx/ws"] = CORE_VERSION;
|
|
55
|
+
if (realtime) deps[realtime.packageName] = realtime.packageVersion;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return deps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPackageJson(opts: ResolvedOptions): string {
|
|
62
|
+
const dependencies = collectDependencies(opts);
|
|
63
|
+
const agentSpec = "./src/agent.ts#createAgent";
|
|
64
|
+
|
|
65
|
+
const scripts: Record<string, string> = {
|
|
66
|
+
dev: `tsx scripts/dev-server.ts --agent ${agentSpec}`,
|
|
67
|
+
"check:typecheck": "tsc --noEmit",
|
|
68
|
+
"check:turn": `syrinx turn --in test/fixtures/smoke.wav --agent ${agentSpec} --json`,
|
|
69
|
+
};
|
|
70
|
+
const devDependencies: Record<string, string> = {
|
|
71
|
+
"@types/node": "^22.0.0",
|
|
72
|
+
"@types/ws": "^8.18.1",
|
|
73
|
+
tsx: "^4.20.0",
|
|
74
|
+
typescript: "^5.7.0",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// --runtime cloudflare additionally ships src/index.ts (the deploy artifact, a
|
|
78
|
+
// withVoice(Agent) Durable Object) + wrangler.jsonc alongside the SAME src/agent.ts
|
|
79
|
+
// + scripts/dev-server.ts used for local iteration and check:turn — the deploy
|
|
80
|
+
// artifact's own typecheck is covered by check:typecheck (tsconfig includes all of
|
|
81
|
+
// src/), but it is not live-verified (see AGENTS.md / the --transport telnyx warning).
|
|
82
|
+
if (opts.runtime === "cloudflare") {
|
|
83
|
+
dependencies["@kuralle-syrinx/cf-agents"] = CORE_VERSION;
|
|
84
|
+
dependencies["agents"] = "0.14.0";
|
|
85
|
+
// `agents` peer-depends on ai@^6.0.0, but its own optional @cloudflare/ai-chat
|
|
86
|
+
// dependency pulls a newer `ai` major transitively — without a direct pin here
|
|
87
|
+
// npm's resolver lands on the newer major and ERESOLVEs against agents' peer.
|
|
88
|
+
// examples/03-cf-agent-voice/package.json pins the same "ai": "^6.0.0" for the
|
|
89
|
+
// same reason, in both its cascaded AND realtime example classes.
|
|
90
|
+
dependencies["ai"] = "^6.0.0";
|
|
91
|
+
// Pinned exact, not a caret range: `wrangler`'s peer on @cloudflare/workers-types
|
|
92
|
+
// moved to v5 in newer releases while `agents`/`partyserver` still peer on v4,
|
|
93
|
+
// so a loose `^4.19.0` resolves npm to the newest 4.x wrangler and hits an
|
|
94
|
+
// ERESOLVE conflict. This exact pair is the one this monorepo's own
|
|
95
|
+
// pnpm-lock.yaml already resolves.
|
|
96
|
+
devDependencies["@cloudflare/workers-types"] = "^4.20260601.1";
|
|
97
|
+
devDependencies["wrangler"] = "4.97.0";
|
|
98
|
+
scripts["check:wrangler-dry-run"] = "wrangler deploy --dry-run";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pkg = {
|
|
102
|
+
name: opts.name,
|
|
103
|
+
version: "0.0.1",
|
|
104
|
+
private: true,
|
|
105
|
+
type: "module",
|
|
106
|
+
license: "MIT",
|
|
107
|
+
scripts,
|
|
108
|
+
dependencies: sortKeys({ ...dependencies, "@kuralle-syrinx/cli": CORE_VERSION }),
|
|
109
|
+
devDependencies: sortKeys(devDependencies),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sortKeys(obj: Record<string, string>): Record<string, string> {
|
|
116
|
+
const out: Record<string, string> = {};
|
|
117
|
+
for (const key of Object.keys(obj).sort()) out[key] = obj[key] as string;
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Generated project's tsconfig.json — mirrors examples/02-hello-voice-headless/tsconfig.json.
|
|
4
|
+
// --runtime cloudflare additionally needs @cloudflare/workers-types: src/index.ts's
|
|
5
|
+
// `Request`/`Response`/`fetch` globals come from there, not from a DOM lib (this
|
|
6
|
+
// tsconfig has no "DOM" in `lib`, matching the node-runtime file it sits beside).
|
|
7
|
+
|
|
8
|
+
import type { Runtime } from "../options.js";
|
|
9
|
+
|
|
10
|
+
export function buildTsconfig(runtime: Runtime): string {
|
|
11
|
+
const types = runtime === "cloudflare" ? ["node", "@cloudflare/workers-types"] : ["node"];
|
|
12
|
+
const tsconfig = {
|
|
13
|
+
compilerOptions: {
|
|
14
|
+
target: "ES2022",
|
|
15
|
+
module: "NodeNext",
|
|
16
|
+
moduleResolution: "NodeNext",
|
|
17
|
+
strict: true,
|
|
18
|
+
skipLibCheck: true,
|
|
19
|
+
noEmit: true,
|
|
20
|
+
types,
|
|
21
|
+
},
|
|
22
|
+
include: ["src/**/*.ts", "scripts/**/*.ts"],
|
|
23
|
+
};
|
|
24
|
+
return `${JSON.stringify(tsconfig, null, 2)}\n`;
|
|
25
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { resolveOptions } from "./options.js";
|
|
10
|
+
import { CliError } from "./exit-codes.js";
|
|
11
|
+
import { buildFileMap, writeProject } from "./write-project.js";
|
|
12
|
+
|
|
13
|
+
describe("buildFileMap — the cascade slice (deepgram/aisdk/cartesia/browser)", () => {
|
|
14
|
+
const opts = resolveOptions({ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "browser" }, ["x"]);
|
|
15
|
+
const files = buildFileMap(opts);
|
|
16
|
+
|
|
17
|
+
it("emits exactly the node-runtime file set — no Cloudflare artifacts", () => {
|
|
18
|
+
expect(files.map((f) => f.relPath).sort()).toEqual([
|
|
19
|
+
".env.example",
|
|
20
|
+
"AGENTS.md",
|
|
21
|
+
"package.json",
|
|
22
|
+
"scripts/dev-server.ts",
|
|
23
|
+
"src/agent.ts",
|
|
24
|
+
"test/fixtures/smoke.wav",
|
|
25
|
+
"tsconfig.json",
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("package.json depends on exactly the chosen providers, not the whole matrix", () => {
|
|
30
|
+
const pkgFile = files.find((f) => f.relPath === "package.json");
|
|
31
|
+
const pkg = JSON.parse(pkgFile?.content as string) as { dependencies: Record<string, string> };
|
|
32
|
+
expect(Object.keys(pkg.dependencies).sort()).toEqual(
|
|
33
|
+
[
|
|
34
|
+
"@ai-sdk/openai",
|
|
35
|
+
"@kuralle-syrinx/aisdk",
|
|
36
|
+
"@kuralle-syrinx/cartesia",
|
|
37
|
+
"@kuralle-syrinx/cli",
|
|
38
|
+
"@kuralle-syrinx/core",
|
|
39
|
+
"@kuralle-syrinx/deepgram",
|
|
40
|
+
"@kuralle-syrinx/server-websocket",
|
|
41
|
+
"ai",
|
|
42
|
+
"ws",
|
|
43
|
+
].sort(),
|
|
44
|
+
);
|
|
45
|
+
// Not present: any provider from a stage that wasn't chosen.
|
|
46
|
+
expect(pkg.dependencies).not.toHaveProperty("@kuralle-syrinx/elevenlabs");
|
|
47
|
+
expect(pkg.dependencies).not.toHaveProperty("@kuralle-syrinx/mastra");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it(".env.example names exactly the required keys for this combination", () => {
|
|
51
|
+
const envFile = files.find((f) => f.relPath === ".env.example");
|
|
52
|
+
const content = envFile?.content as string;
|
|
53
|
+
expect(content).toContain("DEEPGRAM_API_KEY=");
|
|
54
|
+
expect(content).toContain("CARTESIA_API_KEY=");
|
|
55
|
+
expect(content).toContain("CARTESIA_VOICE_ID=");
|
|
56
|
+
expect(content).toContain("OPENAI_API_KEY=");
|
|
57
|
+
expect(content).not.toContain("ELEVENLABS_API_KEY");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("src/agent.ts wires the chosen stage plugins with provider_stt endpointing", () => {
|
|
61
|
+
const agentFile = files.find((f) => f.relPath === "src/agent.ts");
|
|
62
|
+
const content = agentFile?.content as string;
|
|
63
|
+
expect(content).toContain("DeepgramSTTPlugin");
|
|
64
|
+
expect(content).toContain("CartesiaTTSPlugin");
|
|
65
|
+
expect(content).toContain("fromStreamText");
|
|
66
|
+
expect(content).toContain('endpointingOwner: "provider_stt"');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("AGENTS.md states plainly what this generator's checks cannot verify", () => {
|
|
70
|
+
const agentsMd = files.find((f) => f.relPath === "AGENTS.md");
|
|
71
|
+
const content = agentsMd?.content as string;
|
|
72
|
+
expect(content).toMatch(/CANNOT verify/);
|
|
73
|
+
expect(content).toMatch(/barge-in/);
|
|
74
|
+
expect(content).toMatch(/tsx/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("buildFileMap — --endpointing sets smart_turn ownership", () => {
|
|
79
|
+
it("switches endpointingOwner and registers the eos plugin", () => {
|
|
80
|
+
const opts = resolveOptions(
|
|
81
|
+
{ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "browser", endpointing: "pipecat-smart-turn" },
|
|
82
|
+
["x"],
|
|
83
|
+
);
|
|
84
|
+
const files = buildFileMap(opts);
|
|
85
|
+
const content = files.find((f) => f.relPath === "src/agent.ts")?.content as string;
|
|
86
|
+
expect(content).toContain('endpointingOwner: "smart_turn"');
|
|
87
|
+
expect(content).toContain("PipecatEOSPlugin");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("buildFileMap — realtime (speech-to-speech) mode", () => {
|
|
92
|
+
it("emits RealtimeBridge wiring, not STT/TTS stages", () => {
|
|
93
|
+
const opts = resolveOptions({ realtime: "grok" }, ["x"]);
|
|
94
|
+
const files = buildFileMap(opts);
|
|
95
|
+
const content = files.find((f) => f.relPath === "src/agent.ts")?.content as string;
|
|
96
|
+
expect(content).toContain("RealtimeBridge");
|
|
97
|
+
expect(content).toContain("fromGrokRealtime");
|
|
98
|
+
expect(content).toContain('endpointingOwner: "timer"');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("buildFileMap — --runtime cloudflare", () => {
|
|
103
|
+
it("additionally emits src/index.ts and wrangler.jsonc", () => {
|
|
104
|
+
const opts = resolveOptions(
|
|
105
|
+
{ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "browser", runtime: "cloudflare" },
|
|
106
|
+
["x"],
|
|
107
|
+
);
|
|
108
|
+
const files = buildFileMap(opts);
|
|
109
|
+
const relPaths = files.map((f) => f.relPath);
|
|
110
|
+
expect(relPaths).toContain("src/index.ts");
|
|
111
|
+
expect(relPaths).toContain("wrangler.jsonc");
|
|
112
|
+
const worker = files.find((f) => f.relPath === "src/index.ts")?.content as string;
|
|
113
|
+
expect(worker).toContain("withVoice");
|
|
114
|
+
expect(worker).toContain('transport: "edge"');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("refuses --runtime cloudflare --transport smartpbx (no such binding in cf-agents)", () => {
|
|
118
|
+
const opts = resolveOptions(
|
|
119
|
+
{ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "smartpbx", runtime: "cloudflare" },
|
|
120
|
+
["x"],
|
|
121
|
+
);
|
|
122
|
+
expect(() => buildFileMap(opts)).toThrow(CliError);
|
|
123
|
+
expect(() => buildFileMap(opts)).toThrow(/no smartpbx transport binding/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("buildFileMap — not-yet-implemented providers refuse cleanly", () => {
|
|
128
|
+
it("--reasoner kuralle", () => {
|
|
129
|
+
const opts = resolveOptions({ stt: "deepgram", reasoner: "kuralle", tts: "cartesia", transport: "browser" }, ["x"]);
|
|
130
|
+
expect(() => buildFileMap(opts)).toThrow(/--reasoner kuralle is not yet implemented/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("--endpointing vap", () => {
|
|
134
|
+
const opts = resolveOptions(
|
|
135
|
+
{ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "browser", endpointing: "vap" },
|
|
136
|
+
["x"],
|
|
137
|
+
);
|
|
138
|
+
expect(() => buildFileMap(opts)).toThrow(/--endpointing vap is not yet implemented/);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("writeProject", () => {
|
|
143
|
+
let root: string;
|
|
144
|
+
beforeEach(async () => {
|
|
145
|
+
root = await mkdtemp(join(tmpdir(), "create-syrinx-agent-write-"));
|
|
146
|
+
});
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
await rm(root, { recursive: true, maxRetries: 3, force: true }).catch(() => {});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("writes every file, including a binary fixture, to the target directory", async () => {
|
|
152
|
+
const opts = resolveOptions({ stt: "deepgram", reasoner: "aisdk", tts: "cartesia", transport: "browser" }, ["x"]);
|
|
153
|
+
const files = buildFileMap(opts);
|
|
154
|
+
const targetDir = join(root, "generated");
|
|
155
|
+
|
|
156
|
+
await writeProject(targetDir, files);
|
|
157
|
+
|
|
158
|
+
const pkgRaw = await readFile(join(targetDir, "package.json"), "utf8");
|
|
159
|
+
expect(JSON.parse(pkgRaw)).toHaveProperty("name");
|
|
160
|
+
const wav = await readFile(join(targetDir, "test/fixtures/smoke.wav"));
|
|
161
|
+
expect(wav.subarray(0, 4).toString("ascii")).toBe("RIFF");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Orchestrates file emission: builds the file map from a resolved combination,
|
|
4
|
+
// then either lists it (--dry-run) or writes it and (unless skipped) installs.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import type { ResolvedOptions } from "./options.js";
|
|
11
|
+
import { buildAgentModule } from "./templates/agent-module.js";
|
|
12
|
+
import { buildDevServer } from "./templates/dev-server.js";
|
|
13
|
+
import { buildPackageJson } from "./templates/package-json.js";
|
|
14
|
+
import { buildEnvExample } from "./templates/env-example.js";
|
|
15
|
+
import { buildAgentsMd } from "./templates/agents-md.js";
|
|
16
|
+
import { buildTsconfig } from "./templates/tsconfig.js";
|
|
17
|
+
import { buildSmokeWav } from "./templates/fixture.js";
|
|
18
|
+
import { buildCloudflareWorkerSource, buildWranglerJsonc } from "./templates/cloudflare-worker.js";
|
|
19
|
+
|
|
20
|
+
export interface ProjectFile {
|
|
21
|
+
readonly relPath: string;
|
|
22
|
+
readonly content: string | Buffer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildFileMap(opts: ResolvedOptions): readonly ProjectFile[] {
|
|
26
|
+
const files: ProjectFile[] = [
|
|
27
|
+
{ relPath: "package.json", content: buildPackageJson(opts) },
|
|
28
|
+
{ relPath: "tsconfig.json", content: buildTsconfig(opts.runtime) },
|
|
29
|
+
{ relPath: ".env.example", content: buildEnvExample(opts) },
|
|
30
|
+
{ relPath: "AGENTS.md", content: buildAgentsMd(opts) },
|
|
31
|
+
{ relPath: "src/agent.ts", content: buildAgentModule(opts) },
|
|
32
|
+
{ relPath: "scripts/dev-server.ts", content: buildDevServer(opts.transport) },
|
|
33
|
+
{ relPath: "test/fixtures/smoke.wav", content: buildSmokeWav() },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
if (opts.runtime === "cloudflare") {
|
|
37
|
+
files.push(
|
|
38
|
+
{ relPath: "src/index.ts", content: buildCloudflareWorkerSource(opts) },
|
|
39
|
+
{ relPath: "wrangler.jsonc", content: buildWranglerJsonc(opts) },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeProject(targetDir: string, files: readonly ProjectFile[]): Promise<void> {
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const abs = join(targetDir, file.relPath);
|
|
49
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
50
|
+
await writeFile(abs, file.content);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Runs `npm install` in targetDir. Errors surface to the caller (never swallowed). */
|
|
55
|
+
export function runNpmInstall(targetDir: string): void {
|
|
56
|
+
const result = spawnSync("npm", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
57
|
+
if (result.error) throw result.error;
|
|
58
|
+
if (result.status !== 0) throw new Error(`npm install exited with code ${String(result.status)}`);
|
|
59
|
+
}
|