diff --git a/package.json b/package.json index c3a541a..19b1666 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,11 @@ "types": "./dist/commands/index.d.ts", "import": "./dist/commands/index.js", "require": "./dist/commands/index.cjs" + }, + "./bt": { + "types": "./dist/bt/index.d.ts", + "import": "./dist/bt/index.js", + "require": "./dist/bt/index.cjs" } }, "files": [ diff --git a/src/bt/index.ts b/src/bt/index.ts new file mode 100644 index 0000000..597742b --- /dev/null +++ b/src/bt/index.ts @@ -0,0 +1,14 @@ +export { + Task, + Scheduled, + Running, + Succeeded, + Failed, + Cancelled, + TERMINAL_TAGS, + ChildOf, +} from "./task"; +export type { TaskKind } from "./task"; + +export { TaskRunner } from "./runner"; +export type { LeafHandler, TerminalHandler } from "./runner"; diff --git a/src/bt/runner.ts b/src/bt/runner.ts new file mode 100644 index 0000000..1b8da00 --- /dev/null +++ b/src/bt/runner.ts @@ -0,0 +1,287 @@ +import type { World, Entity } from "../index"; +import { query } from "../query"; +import { + Task, + Scheduled, + Running, + Succeeded, + Failed, + Cancelled, + TERMINAL_TAGS, + ChildOf, +} from "./task"; + +// ── Types ───────────────────────────────────────────── +/** Callback invoked for each leaf task that becomes Scheduled. */ +export type LeafHandler = (world: World, entity: Entity) => void; + +/** Callback invoked when a task reaches a terminal status. */ +export type TerminalHandler = ( + world: World, + entity: Entity, + status: "succeeded" | "failed" | "cancelled", +) => void; + +// ── Helpers ─────────────────────────────────────────── +function terminalStatus( + world: World, + entity: Entity, +): "succeeded" | "failed" | "cancelled" | null { + if (world.has(entity, Succeeded)) return "succeeded"; + if (world.has(entity, Failed)) return "failed"; + if (world.has(entity, Cancelled)) return "cancelled"; + return null; +} + +function isTerminal(world: World, entity: Entity): boolean { + return terminalStatus(world, entity) !== null; +} + +function clearStatus(world: World, entity: Entity): void { + for (const tag of TERMINAL_TAGS) { + if (world.has(entity, tag)) world.remove(entity, tag); + } + if (world.has(entity, Running)) world.remove(entity, Running); +} + +function childrenOf(world: World, parent: Entity): Entity[] { + return world.getRelatedTo(parent, ChildOf); +} + +function parentOf(world: World, child: Entity): Entity | null { + return world.getRelated(child, ChildOf) ?? null; +} + +// ── TaskRunner ──────────────────────────────────────── +/** + * Push-based behaviour-tree runner. + * + * Only tasks tagged with `Scheduled` are processed each tick. + * When a task finishes, it notifies its parent, which may schedule + * the next child (sequential), aggregate results (parallel), or + * propagate upward. + * + * Leaves are dispatched to a user-provided `onLeaf` callback. + * Terminal results are dispatched to a user-provided `onTerminal` callback. + * + * @example + * ```ts + * const runner = new TaskRunner(world); + * runner.onLeaf = (world, entity) => { + * // do the leaf's work, then call: + * runner.succeed(entity); + * }; + * + * // Each frame: + * runner.tick(); + * ``` + */ +export class TaskRunner { + private _world: World; + + /** Called when a leaf task becomes Scheduled. */ + onLeaf: LeafHandler = () => {}; + + /** Called when any task reaches a terminal status. */ + onTerminal: TerminalHandler = () => {}; + + constructor(world: World) { + this._world = world; + } + + // ── Public API ──────────────────────────────────── + + /** + * Process all Scheduled tasks. + * + * Call once per frame. Only entities with `Scheduled` are touched. + */ + tick(): void { + const scheduled = [...this._world.query(query(Task, Scheduled))]; + for (const entity of scheduled) { + this._world.remove(entity, Scheduled); + this._execute(entity); + } + } + + /** Mark a leaf task as succeeded and propagate upward. */ + succeed(entity: Entity): void { + this._finish(entity, Succeeded); + } + + /** Mark a leaf task as failed and propagate upward. */ + fail(entity: Entity): void { + this._finish(entity, Failed); + } + + /** Cancel a task and all its descendants. */ + cancel(entity: Entity): void { + this._cancelTree(entity); + } + + /** Schedule a task for execution next tick. */ + schedule(entity: Entity): void { + if (this._world.has(entity, Task)) { + this._world.add(entity, Scheduled); + } + } + + /** Reset a task to idle (removes all status tags). */ + reset(entity: Entity): void { + clearStatus(this._world, entity); + this._world.remove(entity, Scheduled); + } + + // ── Internal execution ──────────────────────────── + + private _execute(entity: Entity): void { + const t = this._world.get(entity, Task); + + switch (t.kind) { + case "leaf": + this._executeLeaf(entity); + break; + case "sequential": + this._executeSequential(entity); + break; + case "parallel": + this._executeParallel(entity); + break; + case "random": + this._executeRandom(entity); + break; + } + } + + private _executeLeaf(entity: Entity): void { + this._world.add(entity, Running); + this.onLeaf(this._world, entity); + } + + private _executeSequential(entity: Entity): void { + const children = childrenOf(this._world, entity); + + // Find the first non-terminal child + for (const child of children) { + if (isTerminal(this._world, child)) { + const status = terminalStatus(this._world, child)!; + if (status === "failed" || status === "cancelled") { + this._finish(entity, status === "cancelled" ? Cancelled : Failed); + return; + } + // succeeded — continue to next child + continue; + } + + // Found a child that hasn't run yet — schedule it + this._world.add(child, Scheduled); + return; + } + + // All children succeeded + this._finish(entity, Succeeded); + } + + private _executeParallel(entity: Entity): void { + const children = childrenOf(this._world, entity); + let allDone = true; + + for (const child of children) { + if (isTerminal(this._world, child)) { + const status = terminalStatus(this._world, child)!; + if (status === "failed" || status === "cancelled") { + this._finish(entity, status === "cancelled" ? Cancelled : Failed); + return; + } + // succeeded — this child is done + continue; + } + + allDone = false; + + // Schedule if not already running or scheduled + if ( + !this._world.has(child, Running) && + !this._world.has(child, Scheduled) + ) { + this._world.add(child, Scheduled); + } + } + + if (allDone) { + this._finish(entity, Succeeded); + } + } + + private _executeRandom(entity: Entity): void { + const children = childrenOf(this._world, entity); + + // Check if any child already reached a terminal + for (const child of children) { + if (isTerminal(this._world, child)) { + const status = terminalStatus(this._world, child)!; + this._finish( + entity, + status === "succeeded" + ? Succeeded + : status === "cancelled" + ? Cancelled + : Failed, + ); + return; + } + } + + // Pick a random child that isn't already running + const eligible = children.filter( + (c) => !this._world.has(c, Running) && !this._world.has(c, Scheduled), + ); + + if (eligible.length > 0) { + const pick = eligible[Math.floor(Math.random() * eligible.length)]; + this._world.add(pick, Scheduled); + } + // If no eligible children (all running), wait for one to finish + } + + // ── Completion propagation ──────────────────────── + + private _finish( + entity: Entity, + tag: typeof Succeeded | typeof Failed | typeof Cancelled, + ): void { + if (!this._world.has(entity, Task)) return; + + clearStatus(this._world, entity); + this._world.add(entity, tag); + + const status = terminalStatus(this._world, entity)!; + this.onTerminal(this._world, entity, status); + + // Notify parent + const parent = parentOf(this._world, entity); + if (parent) { + this._world.add(parent, Scheduled); + } + } + + private _cancelTree(entity: Entity): void { + if (!this._world.has(entity, Task)) return; + + // Cancel children first + for (const child of childrenOf(this._world, entity)) { + this._cancelTree(child); + } + + // Cancel this node + clearStatus(this._world, entity); + this._world.add(entity, Cancelled); + this.onTerminal(this._world, entity, "cancelled"); + + // Notify parent + const parent = parentOf(this._world, entity); + if (parent) { + this._world.add(parent, Scheduled); + } + } +} diff --git a/src/bt/task.ts b/src/bt/task.ts new file mode 100644 index 0000000..cf3db0f --- /dev/null +++ b/src/bt/task.ts @@ -0,0 +1,44 @@ +import { defineComponent } from "../component"; +import { defineRelationship } from "../relationship"; + +// ── Task component ──────────────────────────────────── +/** + * Core component for behaviour-tree tasks. + * + * `kind` determines how the task evaluates its children: + * - `"leaf"` — terminal node; external logic drives it to completion. + * - `"sequential"` — runs children one at a time, left to right. + * Succeeds when all children succeed; fails when any child fails. + * - `"parallel"` — schedules all children at once. + * Succeeds when all children succeed; fails when any child fails. + * - `"random"` — picks one child at random each time it's scheduled. + * Succeeds/fails with that child's result. + */ +export const Task = defineComponent("task", { + kind: "leaf" as "leaf" | "sequential" | "parallel" | "random", +}); + +export type TaskKind = typeof Task.type["kind"]; + +// ── Status tags (zero-size — presence is the signal) ── +/** A task that should be executed this tick. */ +export const Scheduled = defineComponent("scheduled", {}); + +/** A task that is currently executing (multi-frame leaves). */ +export const Running = defineComponent("running", {}); + +/** The task completed successfully. */ +export const Succeeded = defineComponent("succeeded", {}); + +/** The task failed. */ +export const Failed = defineComponent("failed", {}); + +/** The task was cancelled externally. */ +export const Cancelled = defineComponent("cancelled", {}); + +/** All terminal status tags. */ +export const TERMINAL_TAGS = [Succeeded, Failed, Cancelled] as const; + +// ── Relationship ────────────────────────────────────── +/** Parent → child edge in the task tree. */ +export const ChildOf = defineRelationship("taskChild"); diff --git a/test/bt.test.ts b/test/bt.test.ts new file mode 100644 index 0000000..409eade --- /dev/null +++ b/test/bt.test.ts @@ -0,0 +1,539 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { World, type Entity } from "../src/index"; +import { + Task, + Scheduled, + Running, + Succeeded, + Failed, + Cancelled, + ChildOf, + TaskRunner, +} from "../src/bt/index"; + +// ── Helpers ───────────────────────────────────────── +function makeLeaf(world: World, parent?: Entity): Entity { + const e = world.spawn(); + world.add(e, Task, { kind: "leaf" }); + if (parent) world.relate(e, ChildOf, parent); + return e; +} + +function makeSequential(world: World, parent?: Entity): Entity { + const e = world.spawn(); + world.add(e, Task, { kind: "sequential" }); + if (parent) world.relate(e, ChildOf, parent); + return e; +} + +function makeParallel(world: World, parent?: Entity): Entity { + const e = world.spawn(); + world.add(e, Task, { kind: "parallel" }); + if (parent) world.relate(e, ChildOf, parent); + return e; +} + +function makeRandom(world: World, parent?: Entity): Entity { + const e = world.spawn(); + world.add(e, Task, { kind: "random" }); + if (parent) world.relate(e, ChildOf, parent); + return e; +} + +// ── Leaf tasks ────────────────────────────────────── +describe("Leaf tasks", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("calls onLeaf when a leaf is scheduled and ticked", () => { + const leaf = makeLeaf(world); + const calls: Entity[] = []; + + runner.onLeaf = (_w, e) => calls.push(e); + runner.schedule(leaf); + runner.tick(); + + expect(calls).toEqual([leaf]); + }); + + it("marks leaf as Running after tick", () => { + const leaf = makeLeaf(world); + runner.schedule(leaf); + runner.tick(); + + expect(world.has(leaf, Running)).toBe(true); + expect(world.has(leaf, Scheduled)).toBe(false); + }); + + it("succeed() marks leaf as Succeeded", () => { + const leaf = makeLeaf(world); + runner.schedule(leaf); + runner.tick(); + runner.succeed(leaf); + + expect(world.has(leaf, Succeeded)).toBe(true); + expect(world.has(leaf, Running)).toBe(false); + }); + + it("fail() marks leaf as Failed", () => { + const leaf = makeLeaf(world); + runner.schedule(leaf); + runner.tick(); + runner.fail(leaf); + + expect(world.has(leaf, Failed)).toBe(true); + expect(world.has(leaf, Running)).toBe(false); + }); + + it("cancel() marks leaf as Cancelled", () => { + const leaf = makeLeaf(world); + runner.schedule(leaf); + runner.tick(); + runner.cancel(leaf); + + expect(world.has(leaf, Cancelled)).toBe(true); + expect(world.has(leaf, Running)).toBe(false); + }); + + it("onTerminal is called when leaf finishes", () => { + const leaf = makeLeaf(world); + const terminals: { entity: Entity; status: string }[] = []; + + runner.onTerminal = (_w, e, s) => terminals.push({ entity: e, status: s }); + runner.schedule(leaf); + runner.tick(); + runner.succeed(leaf); + + expect(terminals).toEqual([{ entity: leaf, status: "succeeded" }]); + }); + + it("reset() clears all status tags", () => { + const leaf = makeLeaf(world); + runner.schedule(leaf); + runner.tick(); + runner.succeed(leaf); + + runner.reset(leaf); + + expect(world.has(leaf, Succeeded)).toBe(false); + expect(world.has(leaf, Running)).toBe(false); + expect(world.has(leaf, Scheduled)).toBe(false); + }); +}); + +// ── Sequential ────────────────────────────────────── +describe("Sequential tasks", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("runs children one at a time in order", () => { + const seq = makeSequential(world); + const a = makeLeaf(world, seq); + const b = makeLeaf(world, seq); + const c = makeLeaf(world, seq); + + const leafCalls: Entity[] = []; + runner.onLeaf = (_w, e) => leafCalls.push(e); + + runner.schedule(seq); + runner.tick(); // schedules first child + expect(world.has(a, Scheduled)).toBe(true); + expect(world.has(b, Scheduled)).toBe(false); + expect(world.has(c, Scheduled)).toBe(false); + + runner.tick(); // runs a + runner.succeed(a); + + // parent should be re-scheduled + runner.tick(); // schedules next child + expect(world.has(b, Scheduled)).toBe(true); + + runner.tick(); // runs b + runner.succeed(b); + + runner.tick(); // schedules c + expect(world.has(c, Scheduled)).toBe(true); + + runner.tick(); // runs c + runner.succeed(c); + + // parent should now be scheduled and succeed + runner.tick(); + expect(world.has(seq, Succeeded)).toBe(true); + }); + + it("fails immediately when a child fails", () => { + const seq = makeSequential(world); + const a = makeLeaf(world, seq); + const b = makeLeaf(world, seq); + + runner.schedule(seq); + runner.tick(); // schedules a + runner.tick(); // runs a + runner.fail(a); + + // parent re-scheduled + runner.tick(); // sees a failed → seq fails + expect(world.has(seq, Failed)).toBe(true); + // b was never touched + expect(world.has(b, Scheduled)).toBe(false); + expect(world.has(b, Running)).toBe(false); + }); + + it("succeeds when all children succeed", () => { + const seq = makeSequential(world); + const a = makeLeaf(world, seq); + const b = makeLeaf(world, seq); + + runner.schedule(seq); + runner.tick(); // schedules a + runner.tick(); // runs a + runner.succeed(a); + runner.tick(); // schedules b + runner.tick(); // runs b + runner.succeed(b); + runner.tick(); // seq succeeds + + expect(world.has(seq, Succeeded)).toBe(true); + }); + + it("propagates terminal to grandparent", () => { + const root = makeSequential(world); + const child = makeSequential(world, root); + const leaf = makeLeaf(world, child); + + const terminals: Entity[] = []; + runner.onTerminal = (_w, e) => terminals.push(e); + + runner.schedule(root); + runner.tick(); // schedules child + runner.tick(); // schedules leaf + runner.tick(); // runs leaf + runner.succeed(leaf); + runner.tick(); // child succeeds + runner.tick(); // root succeeds + + expect(terminals).toEqual([leaf, child, root]); + expect(world.has(root, Succeeded)).toBe(true); + }); + + it("empty sequential succeeds immediately", () => { + const seq = makeSequential(world); + + runner.schedule(seq); + runner.tick(); + + expect(world.has(seq, Succeeded)).toBe(true); + }); +}); + +// ── Parallel ──────────────────────────────────────── +describe("Parallel tasks", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("schedules all children at once", () => { + const par = makeParallel(world); + const a = makeLeaf(world, par); + const b = makeLeaf(world, par); + const c = makeLeaf(world, par); + + runner.schedule(par); + runner.tick(); + + expect(world.has(a, Scheduled)).toBe(true); + expect(world.has(b, Scheduled)).toBe(true); + expect(world.has(c, Scheduled)).toBe(true); + }); + + it("succeeds when all children succeed", () => { + const par = makeParallel(world); + const a = makeLeaf(world, par); + const b = makeLeaf(world, par); + + runner.schedule(par); + runner.tick(); // schedules both + + runner.tick(); // runs a + runner.succeed(a); + + // par is re-scheduled, but b is still running + runner.tick(); // par sees a done, b still needs scheduling + // b should be scheduled (it was removed from Scheduled when ticked) + // Actually: b was scheduled in first tick, then ticked in second tick + // Let me trace more carefully... + + // After first tick: a=Scheduled, b=Scheduled, par=no status + // Second tick processes Scheduled: a and b both get ticked + // a runs, b runs. Both are Running. + // We succeed a → par gets Scheduled + // Third tick: par sees a=Succeeded, b=Running → waits + // We need to succeed b too + runner.succeed(b); + // par gets Scheduled again + runner.tick(); // par sees both done → succeeds + + expect(world.has(par, Succeeded)).toBe(true); + }); + + it("fails immediately when any child fails", () => { + const par = makeParallel(world); + const a = makeLeaf(world, par); + const b = makeLeaf(world, par); + + runner.schedule(par); + runner.tick(); // schedules both + runner.tick(); // runs a + runner.tick(); // runs b + runner.fail(a); + + // par re-scheduled + runner.tick(); // sees a failed → par fails + expect(world.has(par, Failed)).toBe(true); + }); + + it("empty parallel succeeds immediately", () => { + const par = makeParallel(world); + + runner.schedule(par); + runner.tick(); + + expect(world.has(par, Succeeded)).toBe(true); + }); +}); + +// ── Random ────────────────────────────────────────── +describe("Random tasks", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("picks one child and succeeds/fails with it", () => { + const rand = makeRandom(world); + const a = makeLeaf(world, rand); + const b = makeLeaf(world, rand); + + runner.schedule(rand); + runner.tick(); + + // Exactly one child should be scheduled + const scheduled = [world.has(a, Scheduled), world.has(b, Scheduled)]; + expect(scheduled.filter(Boolean)).toHaveLength(1); + + const picked = world.has(a, Scheduled) ? a : b; + runner.tick(); // runs picked leaf + runner.succeed(picked); + + runner.tick(); // random sees child done → succeeds + expect(world.has(rand, Succeeded)).toBe(true); + }); + + it("fails when picked child fails", () => { + const rand = makeRandom(world); + const a = makeLeaf(world, rand); + + runner.schedule(rand); + runner.tick(); // schedules a (only child) + runner.tick(); // runs a + runner.fail(a); + runner.tick(); // random fails + + expect(world.has(rand, Failed)).toBe(true); + }); + + it("empty random does nothing (no children to pick)", () => { + const rand = makeRandom(world); + + runner.schedule(rand); + runner.tick(); + + // No children, so no status change + expect(world.has(rand, Succeeded)).toBe(false); + expect(world.has(rand, Failed)).toBe(false); + }); +}); + +// ── Cancel ────────────────────────────────────────── +describe("Cancel", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("cancels all descendants", () => { + const root = makeSequential(world); + const child = makeParallel(world, root); + const a = makeLeaf(world, child); + const b = makeLeaf(world, child); + + runner.schedule(root); + runner.tick(); // schedules child + runner.tick(); // schedules a, b + runner.tick(); // runs a + runner.tick(); // runs b + + runner.cancel(root); + + expect(world.has(root, Cancelled)).toBe(true); + expect(world.has(child, Cancelled)).toBe(true); + expect(world.has(a, Cancelled)).toBe(true); + expect(world.has(b, Cancelled)).toBe(true); + }); + + it("cancel propagates to parent", () => { + const root = makeSequential(world); + const child = makeSequential(world, root); + const leaf = makeLeaf(world, child); + + runner.schedule(root); + runner.tick(); // schedules child + runner.tick(); // schedules leaf + runner.tick(); // runs leaf + + runner.cancel(leaf); + + // leaf cancelled → child re-scheduled → child sees leaf cancelled → child cancelled + runner.tick(); // child processes cancelled leaf + expect(world.has(child, Cancelled)).toBe(true); + + // child cancelled → root re-scheduled → root sees child cancelled → root cancelled + runner.tick(); + expect(world.has(root, Cancelled)).toBe(true); + }); +}); + +// ── Multi-frame leaves ────────────────────────────── +describe("Multi-frame leaves", () => { + let world: World; + let runner: TaskRunner; + + beforeEach(() => { + world = new World(); + runner = new TaskRunner(world); + }); + + it("leaf stays Running across ticks until explicitly finished", () => { + const leaf = makeLeaf(world); + + runner.schedule(leaf); + runner.tick(); + + expect(world.has(leaf, Running)).toBe(true); + + // Tick again — leaf is Running, not Scheduled, so nothing happens + runner.tick(); + expect(world.has(leaf, Running)).toBe(true); + + // External system finishes it + runner.succeed(leaf); + expect(world.has(leaf, Succeeded)).toBe(true); + expect(world.has(leaf, Running)).toBe(false); + }); + + it("sequential waits for multi-frame leaf before advancing", () => { + const seq = makeSequential(world); + const a = makeLeaf(world, seq); + const b = makeLeaf(world, seq); + + runner.schedule(seq); + runner.tick(); // schedules a + runner.tick(); // runs a → Running + + // Tick several times — seq should not advance + runner.tick(); + runner.tick(); + expect(world.has(b, Scheduled)).toBe(false); + expect(world.has(b, Running)).toBe(false); + + // Finish a + runner.succeed(a); + runner.tick(); // seq schedules b + expect(world.has(b, Scheduled)).toBe(true); + }); +}); + +// ── Edge cases ────────────────────────────────────── +describe("Edge cases", () => { + it("schedule() is a no-op on non-task entities", () => { + const world = new World(); + const runner = new TaskRunner(world); + const e = world.spawn(); + + runner.schedule(e); + expect(world.has(e, Scheduled)).toBe(false); + }); + + it("succeed/fail/cancel are no-ops on non-task entities", () => { + const world = new World(); + const runner = new TaskRunner(world); + const e = world.spawn(); + + expect(() => runner.succeed(e)).not.toThrow(); + expect(() => runner.fail(e)).not.toThrow(); + expect(() => runner.cancel(e)).not.toThrow(); + }); + + it("tick is a no-op when nothing is scheduled", () => { + const world = new World(); + const runner = new TaskRunner(world); + const leaf = makeLeaf(world); + + // Leaf exists but is not Scheduled + expect(() => runner.tick()).not.toThrow(); + expect(world.has(leaf, Running)).toBe(false); + }); + + it("deeply nested tree works correctly", () => { + const world = new World(); + const runner = new TaskRunner(world); + + const root = makeSequential(world); + const mid = makeSequential(world, root); + const leaf = makeLeaf(world, mid); + + runner.schedule(root); + + // Tick 1: root schedules mid + runner.tick(); + expect(world.has(mid, Scheduled)).toBe(true); + + // Tick 2: mid schedules leaf + runner.tick(); + expect(world.has(leaf, Scheduled)).toBe(true); + + // Tick 3: leaf runs + runner.tick(); + expect(world.has(leaf, Running)).toBe(true); + + runner.succeed(leaf); + // leaf done → mid scheduled + + runner.tick(); // mid sees leaf done → mid succeeds + expect(world.has(mid, Succeeded)).toBe(true); + + runner.tick(); // root sees mid done → root succeeds + expect(world.has(root, Succeeded)).toBe(true); + }); +}); diff --git a/tsup.config.ts b/tsup.config.ts index 7eb0020..2b616dd 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts", "src/commands/index.ts"], + entry: ["src/index.ts", "src/commands/index.ts", "src/bt/index.ts"], format: ["esm", "cjs"], dts: true, clean: true,