feat(cli): scaffold sense as TypeScript + esbuild bundle
- nerve create sense now generates src/index.ts and src/schema.ts - Adds package.json with esbuild build script - Runs pnpm install && pnpm build after scaffolding - Updates nerve-dev skill docs with new sense structure - Updates tests for new TypeScript scaffold Fixes #225
This commit is contained in:
@@ -5,8 +5,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildSenseIndexJs,
|
||||
buildSenseIndexTs,
|
||||
buildSenseMigrationSql,
|
||||
buildSensePackageJson,
|
||||
buildSenseSchemaTs,
|
||||
validateResourceName,
|
||||
} from "../commands/create.js";
|
||||
@@ -45,10 +46,28 @@ describe("buildSenseMigrationSql", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSenseIndexJs", () => {
|
||||
it("embeds sense id in stub", () => {
|
||||
const js = buildSenseIndexJs("my-sense");
|
||||
expect(js).toContain("my-sense");
|
||||
expect(js).toContain("export async function compute");
|
||||
describe("buildSensePackageJson", () => {
|
||||
it("includes esbuild script and sense name", () => {
|
||||
const pkg = JSON.parse(buildSensePackageJson("my-sense"));
|
||||
expect(pkg.name).toBe("nerve-sense-my-sense");
|
||||
expect(pkg.scripts.build).toContain("esbuild");
|
||||
expect(pkg.scripts.build).toContain("src/index.ts");
|
||||
expect(pkg.devDependencies.esbuild).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSenseIndexTs", () => {
|
||||
it("embeds sense id in stub with TypeScript types", () => {
|
||||
const ts = buildSenseIndexTs("my-sense");
|
||||
expect(ts).toContain("my-sense");
|
||||
expect(ts).toContain("export async function compute");
|
||||
expect(ts).toContain("LibSQLDatabase");
|
||||
expect(ts).toContain("Promise<SenseResult>");
|
||||
expect(ts).toContain('from "./schema.js"');
|
||||
});
|
||||
|
||||
it("imports the correct schema export", () => {
|
||||
const ts = buildSenseIndexTs("cpu-usage");
|
||||
expect(ts).toContain("cpuUsage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,21 +146,32 @@ describe("e2e create", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("create sense scaffolds index.js, schema.ts, and migration", { timeout: 10_000 }, async () => {
|
||||
fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-"));
|
||||
const nerveRoot = join(fakeHome, ".uncaged-nerve");
|
||||
it(
|
||||
"create sense scaffolds src/index.ts, src/schema.ts, package.json and migration",
|
||||
{ timeout: 60_000 },
|
||||
async () => {
|
||||
fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-"));
|
||||
const nerveRoot = join(fakeHome, ".uncaged-nerve");
|
||||
|
||||
await runTestCli(fakeHome, ["init", "--force", "--skip-install"]);
|
||||
await runTestCli(fakeHome, ["init", "--force", "--skip-install"]);
|
||||
|
||||
const sense = await runTestCli(fakeHome, ["create", "sense", "e2e-sense"]);
|
||||
expect(sense.exitCode).toBe(0);
|
||||
expect(sense.stdout).toContain("✅");
|
||||
const sense = await runTestCli(fakeHome, ["create", "sense", "e2e-sense"]);
|
||||
expect(sense.exitCode).toBe(0);
|
||||
expect(sense.stdout).toContain("✅");
|
||||
|
||||
const base = join(nerveRoot, "senses", "e2e-sense");
|
||||
expect(existsSync(join(base, "index.js"))).toBe(true);
|
||||
expect(existsSync(join(base, "schema.ts"))).toBe(true);
|
||||
expect(existsSync(join(base, "migrations", "0001_init.sql"))).toBe(true);
|
||||
});
|
||||
const base = join(nerveRoot, "senses", "e2e-sense");
|
||||
expect(existsSync(join(base, "package.json"))).toBe(true);
|
||||
expect(existsSync(join(base, "src", "index.ts"))).toBe(true);
|
||||
expect(existsSync(join(base, "src", "schema.ts"))).toBe(true);
|
||||
expect(existsSync(join(base, "migrations", "0001_init.sql"))).toBe(true);
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(base, "package.json"), "utf8"));
|
||||
expect(pkg.scripts.build).toContain("esbuild");
|
||||
|
||||
// pnpm install + build should produce index.js
|
||||
expect(existsSync(join(base, "index.js"))).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"create workflow exits 1 when directory exists without --force",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
@@ -133,12 +134,47 @@ export const ${exportName} = sqliteTable("${table}", {
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildSenseIndexJs(senseId: string): string {
|
||||
return `/**
|
||||
export function buildSensePackageJson(name: string): string {
|
||||
return `${JSON.stringify(
|
||||
{
|
||||
name: `nerve-sense-${name}`,
|
||||
private: true,
|
||||
type: "module",
|
||||
scripts: {
|
||||
build:
|
||||
"esbuild src/index.ts --bundle --platform=node --format=esm --outdir=. --out-extension:.js=.js --packages=external",
|
||||
},
|
||||
devDependencies: {
|
||||
esbuild: "^0.27.0",
|
||||
"drizzle-orm": "*",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
}
|
||||
|
||||
export function buildSenseIndexTs(senseId: string): string {
|
||||
const exportName = senseIdToSchemaExportName(senseId);
|
||||
return `import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
||||
|
||||
import { ${exportName} } from "./schema.js";
|
||||
|
||||
type SenseResult = {
|
||||
signal: { label: string; ts: number };
|
||||
workflow: null;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* ${senseId} — replace this stub with your sampling logic.
|
||||
* Returns non-null to emit a signal, null to stay silent.
|
||||
*/
|
||||
export async function compute(db, peers, options) {
|
||||
export async function compute(
|
||||
db: LibSQLDatabase,
|
||||
_peers: Record<string, LibSQLDatabase>,
|
||||
_options: { signal: AbortSignal },
|
||||
): Promise<SenseResult> {
|
||||
void ${exportName};
|
||||
return {
|
||||
signal: {
|
||||
label: "${senseId}",
|
||||
@@ -165,6 +201,17 @@ function writeFile(filePath: string, content: string): void {
|
||||
writeFileSync(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
function spawnAsync(cmd: string, args: string[], cwd: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(cmd, args, { cwd, stdio: "inherit" });
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${cmd} exited with code ${String(code)}`));
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
const createWorkflowCommand = defineCommand({
|
||||
meta: {
|
||||
name: "workflow",
|
||||
@@ -262,15 +309,30 @@ const createSenseCommand = defineCommand({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(join(senseDir, "src"), { recursive: true });
|
||||
mkdirSync(join(senseDir, "migrations"), { recursive: true });
|
||||
writeFile(join(senseDir, "index.js"), buildSenseIndexJs(args.name));
|
||||
writeFile(join(senseDir, "schema.ts"), buildSenseSchemaTs(args.name));
|
||||
writeFile(join(senseDir, "package.json"), buildSensePackageJson(args.name));
|
||||
writeFile(join(senseDir, "src", "index.ts"), buildSenseIndexTs(args.name));
|
||||
writeFile(join(senseDir, "src", "schema.ts"), buildSenseSchemaTs(args.name));
|
||||
writeFile(join(senseDir, "migrations", "0001_init.sql"), buildSenseMigrationSql(args.name));
|
||||
|
||||
process.stdout.write("✅ Sense scaffolded:\n");
|
||||
process.stdout.write(` ${join(senseDir, "index.js")}\n`);
|
||||
process.stdout.write(` ${join(senseDir, "schema.ts")}\n`);
|
||||
process.stdout.write(` ${join(senseDir, "package.json")}\n`);
|
||||
process.stdout.write(` ${join(senseDir, "src", "index.ts")}\n`);
|
||||
process.stdout.write(` ${join(senseDir, "src", "schema.ts")}\n`);
|
||||
process.stdout.write(` ${join(senseDir, "migrations", "0001_init.sql")}\n`);
|
||||
|
||||
process.stdout.write("\nInstalling sense dependencies and building…\n");
|
||||
try {
|
||||
await spawnAsync("pnpm", ["install", "--no-cache"], senseDir);
|
||||
await spawnAsync("pnpm", ["run", "build"], senseDir);
|
||||
process.stdout.write("✅ Build complete — index.js ready.\n");
|
||||
} catch {
|
||||
process.stdout.write(
|
||||
`⚠️ Build failed. Run manually:\n cd ${senseDir} && pnpm install --no-cache && pnpm run build\n`,
|
||||
);
|
||||
}
|
||||
|
||||
process.stdout.write("\n💡 Next steps:\n");
|
||||
process.stdout.write(" 1. Add to nerve.yaml under senses:\n");
|
||||
process.stdout.write(` ${args.name}:\n`);
|
||||
@@ -278,8 +340,11 @@ const createSenseCommand = defineCommand({
|
||||
process.stdout.write(" throttle: null\n");
|
||||
process.stdout.write(" timeout: 10s\n");
|
||||
process.stdout.write(" grace_period: null\n");
|
||||
process.stdout.write(` 2. Edit the scaffolded files to implement ${args.name}.\n`);
|
||||
process.stdout.write(" 3. Run `nerve start` to launch the daemon.\n");
|
||||
process.stdout.write(
|
||||
` 2. Edit ${join(senseDir, "src", "index.ts")} to implement ${args.name}.\n`,
|
||||
);
|
||||
process.stdout.write(` 3. Re-run \`pnpm run build\` in ${senseDir} after edits.\n`);
|
||||
process.stdout.write(" 4. Run `nerve start` to launch the daemon.\n");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user