@rigkit/engine 0.0.0-canary-20260518T014918-c5bc0c2

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.
@@ -0,0 +1,121 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ __resetConsoleInterceptForTests,
4
+ runWithStepConsole,
5
+ type ConsoleLevel,
6
+ type StepConsoleSink,
7
+ } from "./console-intercept.ts";
8
+
9
+ type Captured = { level: ConsoleLevel; message: string };
10
+
11
+ function collector(): { sink: StepConsoleSink; entries: Captured[] } {
12
+ const entries: Captured[] = [];
13
+ return { sink: (input) => entries.push(input), entries };
14
+ }
15
+
16
+ afterEach(() => {
17
+ __resetConsoleInterceptForTests();
18
+ delete process.env.RIGKIT_NO_CONSOLE_INTERCEPT;
19
+ });
20
+
21
+ describe("runWithStepConsole", () => {
22
+ test("captures console.{log,info,debug,warn,error} with the right level", async () => {
23
+ const { sink, entries } = collector();
24
+ await runWithStepConsole(sink, async () => {
25
+ console.log("a log line");
26
+ console.info("an info line");
27
+ console.debug("a debug line");
28
+ console.warn("a warn line");
29
+ console.error("an error line");
30
+ });
31
+
32
+ expect(entries).toEqual([
33
+ { level: "log", message: "a log line" },
34
+ { level: "info", message: "an info line" },
35
+ { level: "debug", message: "a debug line" },
36
+ { level: "warn", message: "a warn line" },
37
+ { level: "error", message: "an error line" },
38
+ ]);
39
+ });
40
+
41
+ test("formats objects and printf args like the real console", async () => {
42
+ const { sink, entries } = collector();
43
+ await runWithStepConsole(sink, async () => {
44
+ console.log("counts:", { a: 1, b: 2 });
45
+ console.log("user %s scored %d", "alice", 42);
46
+ });
47
+
48
+ expect(entries[0]!.message).toBe("counts: { a: 1, b: 2 }");
49
+ expect(entries[1]!.message).toBe("user alice scored 42");
50
+ });
51
+
52
+ test("preserves the scope across async boundaries", async () => {
53
+ const { sink, entries } = collector();
54
+ const inner = async () => {
55
+ await Promise.resolve();
56
+ console.log("inside async helper");
57
+ };
58
+ await runWithStepConsole(sink, async () => {
59
+ await inner();
60
+ });
61
+
62
+ expect(entries).toEqual([{ level: "log", message: "inside async helper" }]);
63
+ });
64
+
65
+ test("isolates concurrent step contexts", async () => {
66
+ const a = collector();
67
+ const b = collector();
68
+ await Promise.all([
69
+ runWithStepConsole(a.sink, async () => {
70
+ await Promise.resolve();
71
+ console.log("from A");
72
+ }),
73
+ runWithStepConsole(b.sink, async () => {
74
+ await Promise.resolve();
75
+ console.warn("from B");
76
+ }),
77
+ ]);
78
+
79
+ expect(a.entries).toEqual([{ level: "log", message: "from A" }]);
80
+ expect(b.entries).toEqual([{ level: "warn", message: "from B" }]);
81
+ });
82
+
83
+ test("RIGKIT_NO_CONSOLE_INTERCEPT=1 disables capture", async () => {
84
+ process.env.RIGKIT_NO_CONSOLE_INTERCEPT = "1";
85
+ const { sink, entries } = collector();
86
+ const consoleLogSpy: string[] = [];
87
+ const originalLog = console.log;
88
+ console.log = (...args: unknown[]) => consoleLogSpy.push(String(args[0] ?? ""));
89
+ try {
90
+ await runWithStepConsole(sink, async () => {
91
+ console.log("should fall through to original");
92
+ });
93
+ } finally {
94
+ console.log = originalLog;
95
+ }
96
+
97
+ expect(entries).toEqual([]);
98
+ expect(consoleLogSpy).toEqual(["should fall through to original"]);
99
+ });
100
+
101
+ test("outside the scope, console.* falls through to its original implementation", async () => {
102
+ const { sink, entries } = collector();
103
+ // Install the patch by running a scoped call once.
104
+ await runWithStepConsole(sink, async () => {
105
+ console.log("inside");
106
+ });
107
+
108
+ // Now call outside — should not show up in entries.
109
+ const originalLog = console.log;
110
+ const outsideCalls: unknown[][] = [];
111
+ console.log = (...args: unknown[]) => outsideCalls.push(args);
112
+ try {
113
+ console.log("outside");
114
+ } finally {
115
+ console.log = originalLog;
116
+ }
117
+
118
+ expect(entries).toEqual([{ level: "log", message: "inside" }]);
119
+ expect(outsideCalls).toEqual([["outside"]]);
120
+ });
121
+ });
@@ -0,0 +1,75 @@
1
+ // Captures `console.log` / `info` / `debug` / `warn` / `error` invoked inside a
2
+ // step handler and routes the output through that step's logger. This lets
3
+ // users write `console.log("foo")` instead of threading `step.log` through
4
+ // every helper, and surfaces third-party SDK output for free.
5
+ //
6
+ // Scoped via AsyncLocalStorage so:
7
+ // - Engine/runtime code that itself uses `console.*` is never touched.
8
+ // - Concurrent step executions each get their own logger.
9
+ //
10
+ // Disabled by `RIGKIT_NO_CONSOLE_INTERCEPT=1`.
11
+
12
+ import { AsyncLocalStorage } from "node:async_hooks";
13
+ import { formatWithOptions } from "node:util";
14
+
15
+ export type ConsoleLevel = "debug" | "info" | "log" | "warn" | "error";
16
+
17
+ export type StepConsoleSink = (input: { level: ConsoleLevel; message: string }) => void;
18
+
19
+ type ConsoleMethod = "debug" | "info" | "log" | "warn" | "error";
20
+
21
+ const METHODS: readonly ConsoleMethod[] = ["debug", "info", "log", "warn", "error"] as const;
22
+ const STORAGE = new AsyncLocalStorage<StepConsoleSink>();
23
+
24
+ let installed = false;
25
+ const originalMethods: Partial<Record<ConsoleMethod, (...args: unknown[]) => void>> = {};
26
+
27
+ export function runWithStepConsole<T>(sink: StepConsoleSink, fn: () => Promise<T> | T): Promise<T> | T {
28
+ if (process.env.RIGKIT_NO_CONSOLE_INTERCEPT === "1") return fn();
29
+ ensureInstalled();
30
+ return STORAGE.run(sink, fn);
31
+ }
32
+
33
+ function ensureInstalled(): void {
34
+ if (installed) return;
35
+ installed = true;
36
+
37
+ // util.formatWithOptions colors based on the second arg. We disable colors so
38
+ // the captured output is plain (the CLI presenter colors it on the render
39
+ // side based on level, which is the right place for terminal styling).
40
+ const formatOptions = { colors: false, depth: 4, breakLength: 80 } as const;
41
+
42
+ for (const method of METHODS) {
43
+ const original = console[method].bind(console);
44
+ originalMethods[method] = original;
45
+ console[method] = (...args: unknown[]) => {
46
+ const sink = STORAGE.getStore();
47
+ if (!sink) {
48
+ original(...args);
49
+ return;
50
+ }
51
+ try {
52
+ const message = formatWithOptions(formatOptions, ...args);
53
+ sink({ level: methodToLevel(method), message });
54
+ } catch {
55
+ // If formatting fails, fall back to the original so users at least see
56
+ // something instead of swallowing their log.
57
+ original(...args);
58
+ }
59
+ };
60
+ }
61
+ }
62
+
63
+ function methodToLevel(method: ConsoleMethod): ConsoleLevel {
64
+ return method;
65
+ }
66
+
67
+ // Test-only: restore the original console methods. Not exported from the
68
+ // package's public surface but used by engine.test.ts.
69
+ export function __resetConsoleInterceptForTests(): void {
70
+ for (const method of METHODS) {
71
+ const original = originalMethods[method];
72
+ if (original) console[method] = original;
73
+ }
74
+ installed = false;
75
+ }
@@ -0,0 +1,157 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+ import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
5
+ import { coreSchema, type CoreSchema } from "./schema/index.ts";
6
+
7
+ export const RIGKIT_STATE_SCHEMA_VERSION = "drizzle-push";
8
+
9
+ export type RigkitDatabase<TSchema extends Record<string, unknown> = CoreSchema> =
10
+ BunSQLiteDatabase<TSchema> & { $client: Database };
11
+
12
+ export type RigkitDatabaseSchema = Record<string, unknown>;
13
+
14
+ export type CreateRigkitDatabaseOptions<TSchema extends RigkitDatabaseSchema = CoreSchema> = {
15
+ schema?: TSchema;
16
+ };
17
+
18
+ export type SchemaSyncResult = {
19
+ applied: string[];
20
+ schemaVersion: string;
21
+ statements: string[];
22
+ warnings: string[];
23
+ hasDataLoss: boolean;
24
+ };
25
+
26
+ type DrizzleKitBunSQLiteDatabase = Pick<RigkitDatabase<RigkitDatabaseSchema>, "all" | "run">;
27
+
28
+ type PushSQLiteSchemaResult = {
29
+ hasDataLoss: boolean;
30
+ warnings: string[];
31
+ statementsToExecute: string[];
32
+ apply(): Promise<void>;
33
+ };
34
+
35
+ type PushSQLiteSchemaForBun = (
36
+ imports: RigkitDatabaseSchema,
37
+ drizzleInstance: DrizzleKitBunSQLiteDatabase,
38
+ ) => Promise<PushSQLiteSchemaResult>;
39
+
40
+ export function createRigkitDatabase<TSchema extends RigkitDatabaseSchema = CoreSchema>(
41
+ path: string,
42
+ options: CreateRigkitDatabaseOptions<TSchema> = {},
43
+ ): RigkitDatabase<TSchema> {
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ const db = drizzle(new Database(path, { create: true }), {
46
+ schema: options.schema ?? (coreSchema as unknown as TSchema),
47
+ });
48
+ db.run("PRAGMA journal_mode = WAL");
49
+ db.run("PRAGMA foreign_keys = ON");
50
+ return db;
51
+ }
52
+
53
+ export async function syncRigkitDatabaseSchema<TSchema extends RigkitDatabaseSchema>(
54
+ db: RigkitDatabase<TSchema>,
55
+ schema: TSchema,
56
+ ): Promise<SchemaSyncResult> {
57
+ try {
58
+ const result = await pushRigkitDatabaseSchema(db, schema);
59
+ return toSchemaSyncResult(result);
60
+ } catch (error) {
61
+ const resetStatements = resetRigkitDatabase(db);
62
+ const result = await pushRigkitDatabaseSchema(db, schema);
63
+ return toSchemaSyncResult(result, {
64
+ resetStatements,
65
+ resetReason: errorMessage(error),
66
+ });
67
+ }
68
+ }
69
+
70
+ async function pushRigkitDatabaseSchema<TSchema extends RigkitDatabaseSchema>(
71
+ db: RigkitDatabase<TSchema>,
72
+ schema: TSchema,
73
+ ): Promise<PushSQLiteSchemaResult> {
74
+ const drizzleKitApi = ["drizzle-kit", "api"].join("/");
75
+ const { pushSQLiteSchema } = await import(drizzleKitApi);
76
+ const pushSchema = pushSQLiteSchema as unknown as PushSQLiteSchemaForBun;
77
+ const result = await silenceStdout(() => pushSchema(schema, db));
78
+ await silenceStdout(() => result.apply());
79
+ return result;
80
+ }
81
+
82
+ function toSchemaSyncResult(
83
+ result: PushSQLiteSchemaResult,
84
+ reset?: { resetStatements: string[]; resetReason: string },
85
+ ): SchemaSyncResult {
86
+ const statements = [...(reset?.resetStatements ?? []), ...result.statementsToExecute];
87
+ const warnings = [...result.warnings];
88
+ if (reset) {
89
+ warnings.unshift(`Reset Rigkit state database after Drizzle push failed: ${reset.resetReason}`);
90
+ }
91
+ return {
92
+ applied: statements.length > 0 ? [RIGKIT_STATE_SCHEMA_VERSION] : [],
93
+ schemaVersion: RIGKIT_STATE_SCHEMA_VERSION,
94
+ statements,
95
+ warnings,
96
+ hasDataLoss: reset !== undefined || result.hasDataLoss,
97
+ };
98
+ }
99
+
100
+ function resetRigkitDatabase<TSchema extends RigkitDatabaseSchema>(db: RigkitDatabase<TSchema>): string[] {
101
+ const rows = db.$client
102
+ .query(`
103
+ SELECT type, name
104
+ FROM sqlite_schema
105
+ WHERE type IN ('table', 'view', 'trigger')
106
+ AND name NOT LIKE 'sqlite_%'
107
+ ORDER BY CASE type
108
+ WHEN 'view' THEN 0
109
+ WHEN 'trigger' THEN 1
110
+ ELSE 2
111
+ END
112
+ `)
113
+ .all() as Array<{ type: "table" | "view" | "trigger"; name: string }>;
114
+
115
+ const statements = [
116
+ "PRAGMA foreign_keys=OFF",
117
+ ...rows.map((row) => `DROP ${row.type.toUpperCase()} IF EXISTS ${quoteSqlIdentifier(row.name)}`),
118
+ "PRAGMA foreign_keys=ON",
119
+ ];
120
+ const dropStatements = statements.slice(1, -1);
121
+ const reset = db.$client.transaction(() => {
122
+ for (const sql of dropStatements) {
123
+ db.$client.run(sql);
124
+ }
125
+ });
126
+ db.$client.run("PRAGMA foreign_keys=OFF");
127
+ try {
128
+ reset();
129
+ } finally {
130
+ db.$client.run("PRAGMA foreign_keys=ON");
131
+ }
132
+ return statements;
133
+ }
134
+
135
+ function quoteSqlIdentifier(name: string): string {
136
+ return `"${name.replaceAll('"', '""')}"`;
137
+ }
138
+
139
+ async function silenceStdout<T>(run: () => Promise<T>): Promise<T> {
140
+ const write = process.stdout.write;
141
+ process.stdout.write = (() => true) as typeof process.stdout.write;
142
+ try {
143
+ return await run();
144
+ } finally {
145
+ process.stdout.write = write;
146
+ }
147
+ }
148
+
149
+ function errorMessage(error: unknown): string {
150
+ if (error instanceof Error) return error.message;
151
+ if (typeof error === "string") return error;
152
+ try {
153
+ return JSON.stringify(error);
154
+ } catch {
155
+ return String(error);
156
+ }
157
+ }
@@ -0,0 +1,71 @@
1
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
2
+ import type { JsonValue } from "../../types.ts";
3
+
4
+ export const workspaces = sqliteTable(
5
+ "workspaces",
6
+ {
7
+ id: text("id").primaryKey(),
8
+ name: text("name").notNull(),
9
+ workflow: text("workflow").notNull(),
10
+ workflowCtx: text("workflow_ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
11
+ createdAt: text("created_at").notNull(),
12
+ updatedAt: text("updated_at").notNull(),
13
+ ctx: text("ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
14
+ },
15
+ (table) => [
16
+ uniqueIndex("workspaces_name_idx").on(table.name),
17
+ index("workspaces_workflow_idx").on(table.workflow),
18
+ ],
19
+ );
20
+
21
+ export const workflowNodeRuns = sqliteTable(
22
+ "workflow_node_runs",
23
+ {
24
+ id: text("id").primaryKey(),
25
+ workflow: text("workflow").notNull(),
26
+ nodePath: text("node_path").notNull(),
27
+ nodeName: text("node_name").notNull(),
28
+ nodeKind: text("node_kind").notNull(),
29
+ nodeKey: text("node_key").notNull(),
30
+ providerFingerprint: text("provider_fingerprint").notNull(),
31
+ upstreamRunIds: text("upstream_run_ids_json", { mode: "json" }).$type<string[]>().notNull(),
32
+ output: text("output_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
33
+ artifacts: text("artifacts_json", { mode: "json" }).$type<JsonValue[]>().notNull(),
34
+ invalidated: integer("invalidated", { mode: "boolean" }).notNull().default(false),
35
+ createdAt: text("created_at").notNull(),
36
+ metadata: text("metadata_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
37
+ },
38
+ (table) => [
39
+ index("workflow_node_runs_lookup_idx").on(
40
+ table.workflow,
41
+ table.nodePath,
42
+ table.nodeKey,
43
+ table.providerFingerprint,
44
+ ),
45
+ index("workflow_node_runs_created_idx").on(table.createdAt),
46
+ ],
47
+ );
48
+
49
+ export const providerState = sqliteTable(
50
+ "provider_state",
51
+ {
52
+ providerId: text("provider_id").notNull(),
53
+ key: text("key").notNull(),
54
+ value: text("value_json", { mode: "json" }).$type<JsonValue>().notNull(),
55
+ createdAt: text("created_at").notNull(),
56
+ updatedAt: text("updated_at").notNull(),
57
+ },
58
+ (table) => [
59
+ uniqueIndex("provider_state_provider_key_idx").on(table.providerId, table.key),
60
+ index("provider_state_provider_idx").on(table.providerId),
61
+ ],
62
+ );
63
+
64
+ export const runtimeMetadata = sqliteTable(
65
+ "runtime_metadata",
66
+ {
67
+ key: text("key").primaryKey(),
68
+ value: text("value_json", { mode: "json" }).$type<JsonValue>().notNull(),
69
+ updatedAt: text("updated_at").notNull(),
70
+ },
71
+ );
@@ -0,0 +1,7 @@
1
+ import * as core from "./core.ts";
2
+
3
+ export const coreSchema = core;
4
+
5
+ export type CoreSchema = typeof coreSchema;
6
+
7
+ export * from "./core.ts";