@rigkit/engine 0.1.8

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,52 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ export function loadDotEnv(projectDir: string): void {
5
+ for (const path of findDotEnvFiles(projectDir)) {
6
+ loadDotEnvFile(path);
7
+ }
8
+ }
9
+
10
+ function loadDotEnvFile(path: string): void {
11
+ const content = readFileSync(path, "utf8");
12
+ for (const rawLine of content.split(/\r?\n/)) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith("#")) continue;
15
+
16
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
17
+ if (!match) continue;
18
+
19
+ const [, key, rawValue] = match;
20
+ if (process.env[key] !== undefined) continue;
21
+
22
+ process.env[key] = parseEnvValue(rawValue);
23
+ }
24
+ }
25
+
26
+ function findDotEnvFiles(projectDir: string): string[] {
27
+ const files: string[] = [];
28
+ let current = projectDir;
29
+
30
+ while (true) {
31
+ const candidate = join(current, ".env");
32
+ if (existsSync(candidate)) files.unshift(candidate);
33
+
34
+ const parent = dirname(current);
35
+ if (parent === current) break;
36
+ current = parent;
37
+ }
38
+
39
+ return files;
40
+ }
41
+
42
+ function parseEnvValue(rawValue: string): string {
43
+ const trimmed = rawValue.trim();
44
+ if (
45
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
46
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
47
+ ) {
48
+ return trimmed.slice(1, -1);
49
+ }
50
+
51
+ return trimmed;
52
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function stableJson(value: unknown): string {
4
+ return JSON.stringify(sortValue(value));
5
+ }
6
+
7
+ export function hash(value: unknown): string {
8
+ return createHash("sha256").update(stableJson(value)).digest("hex");
9
+ }
10
+
11
+ function sortValue(value: unknown): unknown {
12
+ if (Array.isArray(value)) return value.map(sortValue);
13
+ if (!value || typeof value !== "object") return value;
14
+
15
+ return Object.fromEntries(
16
+ Object.entries(value as Record<string, unknown>)
17
+ .sort(([a], [b]) => a.localeCompare(b))
18
+ .map(([key, inner]) => [key, sortValue(inner)]),
19
+ );
20
+ }
21
+
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ export { createDevMachineEngine, DevMachineEngine } from "./engine.ts";
2
+ export {
3
+ EngineOperationNotFoundError,
4
+ EngineOperationValidationError,
5
+ } from "./engine.ts";
6
+ export type {
7
+ EngineOperationCli,
8
+ EngineOperationCliOption,
9
+ EngineOperationCliPosition,
10
+ EngineOperationKind,
11
+ EngineOperationSource,
12
+ EngineOperationSummary,
13
+ InteractionPresenter,
14
+ InteractionPresentationRequest,
15
+ } from "./engine.ts";
16
+ export { createRigkitDatabase, RIGKIT_STATE_SCHEMA_VERSION, syncRigkitDatabaseSchema } from "./db/index.ts";
17
+ export { coreSchema } from "./db/schema/index.ts";
18
+ export { createStateStore } from "./state.ts";
19
+ export { RIGKIT_ENGINE_VERSION } from "./version.ts";
20
+ export {
21
+ defineConfig,
22
+ defineProvider,
23
+ env,
24
+ isRigkitConfig,
25
+ isProviderDefinition,
26
+ isWorkflow,
27
+ isWorkflowNode,
28
+ sequence,
29
+ workflow,
30
+ } from "./authoring.ts";
31
+ export type * from "./types.ts";
32
+ export type { RigkitDatabase, RigkitDatabaseSchema, SchemaSyncResult } from "./db/index.ts";
33
+ export type * from "./provider/types.ts";
34
+ export type * from "./state.ts";
@@ -0,0 +1,139 @@
1
+ import type {
2
+ EventHandler,
3
+ ExecOptions,
4
+ ExecResult,
5
+ JsonObject,
6
+ JsonValue,
7
+ LoadedProviderDefinition,
8
+ LocalWorkspaceRuntime,
9
+ MaybePromise,
10
+ ProviderWorkspaceContext,
11
+ WorkspaceRecord,
12
+ } from "../types.ts";
13
+
14
+ export type VmHandle = {
15
+ vmId: string;
16
+ };
17
+
18
+ export type SnapshotHandle = {
19
+ snapshotId: string;
20
+ sourceVmId: string;
21
+ };
22
+
23
+ export type SshOptions = {
24
+ user?: string;
25
+ };
26
+
27
+ export type SshConnection = {
28
+ kind: "ssh";
29
+ host: string;
30
+ port?: number;
31
+ username: string;
32
+ auth: { type: "token"; token: string } | { type: "privateKey"; privateKey: string };
33
+ command: string;
34
+ };
35
+
36
+ export interface BaseDevMachineProvider<
37
+ WorkspaceContext extends ProviderWorkspaceContext = ProviderWorkspaceContext,
38
+ > {
39
+ readonly providerId: string;
40
+ createVm(): Promise<VmHandle>;
41
+ createVmFromSnapshot(input: { snapshotId: string }): Promise<VmHandle>;
42
+ exec(vm: VmHandle, command: string, options?: ExecOptions): Promise<ExecResult>;
43
+ readFile(vm: VmHandle, path: string): Promise<string>;
44
+ writeFile(vm: VmHandle, path: string, content: string): Promise<void>;
45
+ snapshot(vm: VmHandle): Promise<SnapshotHandle>;
46
+ ssh(vm: VmHandle, options?: SshOptions): Promise<SshConnection>;
47
+ workspaceContext?(vm: VmHandle, input: { workspace: WorkspaceRecord }): MaybePromise<WorkspaceContext>;
48
+ deleteVm(vm: VmHandle): Promise<void>;
49
+ }
50
+
51
+ export type InteractionPresentationRequest = {
52
+ id: string;
53
+ nodePath: string;
54
+ title: string;
55
+ url: string;
56
+ instructions?: string;
57
+ };
58
+
59
+ export type InteractionPresenter = (request: InteractionPresentationRequest) => Promise<void>;
60
+
61
+ export type ProviderInteractionSession<Result = void> = {
62
+ id?: string;
63
+ title: string;
64
+ url: string;
65
+ instructions?: string;
66
+ completed: Promise<Result>;
67
+ stop(): MaybePromise<void>;
68
+ };
69
+
70
+ export type ProviderRuntimeContext = {
71
+ workflow: string;
72
+ nodePath: string;
73
+ emit: EventHandler;
74
+ interaction: {
75
+ present<Result>(session: ProviderInteractionSession<Result>): Promise<Result>;
76
+ };
77
+ local: LocalWorkspaceRuntime;
78
+ metadata(metadata: JsonObject): void;
79
+ };
80
+
81
+ export type WorkflowWorkspaceCreateResult = {
82
+ providerId?: string;
83
+ resourceId: string;
84
+ snapshotId?: string;
85
+ sourceRef?: JsonValue;
86
+ metadata?: JsonObject;
87
+ };
88
+
89
+ export interface WorkflowWorkspaceProvider<
90
+ WorkspaceContext extends ProviderWorkspaceContext = ProviderWorkspaceContext,
91
+ > {
92
+ canUse(sourceRef: JsonValue): boolean;
93
+ createWorkspace(sourceRef: JsonValue, input: { name: string }): Promise<WorkflowWorkspaceCreateResult>;
94
+ deleteWorkspace(workspace: WorkspaceRecord): Promise<void>;
95
+ snapshotWorkspace(workspace: WorkspaceRecord): Promise<WorkflowWorkspaceCreateResult>;
96
+ ssh(workspaceOrResourceId: string, options?: SshOptions): Promise<SshConnection>;
97
+ workspaceContext?(workspace: WorkspaceRecord): MaybePromise<WorkspaceContext>;
98
+ }
99
+
100
+ export interface WorkflowProviderController<
101
+ Runtime = unknown,
102
+ WorkspaceContext extends ProviderWorkspaceContext = ProviderWorkspaceContext,
103
+ > {
104
+ readonly providerId: string;
105
+ runtime(context: ProviderRuntimeContext): MaybePromise<Runtime>;
106
+ validateArtifact?(ref: JsonValue): MaybePromise<boolean>;
107
+ workspace?: WorkflowWorkspaceProvider<WorkspaceContext>;
108
+ }
109
+
110
+ export type ProviderFactoryInput = {
111
+ provider: LoadedProviderDefinition;
112
+ storage: ProviderStorage;
113
+ };
114
+
115
+ export type ProviderFactory = (
116
+ input: ProviderFactoryInput,
117
+ ) => WorkflowProviderController | Promise<WorkflowProviderController>;
118
+
119
+ export type ProviderStorageRecord<Value extends JsonValue = JsonValue> = {
120
+ providerId: string;
121
+ key: string;
122
+ value: Value;
123
+ createdAt: string;
124
+ updatedAt: string;
125
+ };
126
+
127
+ export interface ProviderStorage {
128
+ get<Value extends JsonValue = JsonValue>(key: string): ProviderStorageRecord<Value> | undefined;
129
+ set<Value extends JsonValue = JsonValue>(key: string, value: Value): ProviderStorageRecord<Value>;
130
+ delete(key: string): void;
131
+ entries(prefix?: string): ProviderStorageRecord[];
132
+ }
133
+
134
+ export type BaseProviderPlugin = {
135
+ providerId: string;
136
+ createProvider(input: ProviderFactoryInput): WorkflowProviderController | Promise<WorkflowProviderController>;
137
+ };
138
+
139
+ export type DevMachineProvider = BaseDevMachineProvider;
package/src/state.ts ADDED
@@ -0,0 +1,318 @@
1
+ import { join } from "node:path";
2
+ import { and, asc, desc, eq, or } from "drizzle-orm";
3
+ import type { ProviderStorage, ProviderStorageRecord } from "./provider/types.ts";
4
+ import type { JsonValue, WorkspaceRecord } from "./types.ts";
5
+ import {
6
+ createRigkitDatabase,
7
+ syncRigkitDatabaseSchema,
8
+ type RigkitDatabase,
9
+ type SchemaSyncResult,
10
+ } from "./db/index.ts";
11
+ import { coreSchema, type CoreSchema } from "./db/schema/index.ts";
12
+ import { providerState, runtimeMetadata, workflowNodeRuns, workspaces } from "./db/schema/index.ts";
13
+ import { stableJson } from "./hash.ts";
14
+ import { RIGKIT_ENGINE_VERSION } from "./version.ts";
15
+
16
+ export type WorkflowNodeRunRecord = {
17
+ id: string;
18
+ workflow: string;
19
+ nodePath: string;
20
+ nodeName: string;
21
+ nodeKind: string;
22
+ nodeKey: string;
23
+ providerFingerprint: string;
24
+ upstreamRunIds: string[];
25
+ output: Record<string, JsonValue>;
26
+ artifacts: JsonValue[];
27
+ invalidated: boolean;
28
+ createdAt: string;
29
+ metadata: Record<string, JsonValue>;
30
+ };
31
+
32
+ export type SnapshotRecord = WorkflowNodeRunRecord;
33
+
34
+ export type StateServiceOptions = {
35
+ projectDir: string;
36
+ statePath?: string;
37
+ projectId?: string;
38
+ configPath?: string;
39
+ runtimeVersion?: string;
40
+ source?: JsonValue;
41
+ };
42
+
43
+ export type StateServiceFactory = (options: StateServiceOptions) => StateService;
44
+
45
+ export interface StateService {
46
+ readonly path: string;
47
+ syncSchema(): Promise<SchemaSyncResult>;
48
+ listWorkspaces(): WorkspaceRecord[];
49
+ findWorkspace(nameOrResourceId: string): WorkspaceRecord | undefined;
50
+ getWorkspace(name: string): WorkspaceRecord | undefined;
51
+ saveWorkspace(workspace: WorkspaceRecord): void;
52
+ deleteWorkspace(name: string): void;
53
+ listNodeRuns(): WorkflowNodeRunRecord[];
54
+ listSnapshots(): SnapshotRecord[];
55
+ findReusableNodeRun(input: {
56
+ workflow: string;
57
+ nodePath: string;
58
+ nodeKey: string;
59
+ providerFingerprint: string;
60
+ upstreamRunIds: readonly string[];
61
+ }): WorkflowNodeRunRecord | undefined;
62
+ saveNodeRun(run: WorkflowNodeRunRecord): void;
63
+ providerStorage(providerId: string): ProviderStorage;
64
+ }
65
+
66
+ export class StateStore implements StateService {
67
+ readonly path: string;
68
+ readonly db: RigkitDatabase<CoreSchema>;
69
+ private readonly schema = coreSchema;
70
+ private readonly projectDir: string;
71
+ private readonly metadata: Omit<StateServiceOptions, "projectDir" | "statePath">;
72
+ private schemaSync?: Promise<SchemaSyncResult>;
73
+
74
+ constructor(projectDir: string, options: Omit<StateServiceOptions, "projectDir"> = {}) {
75
+ this.projectDir = projectDir;
76
+ this.path = options.statePath ?? join(projectDir, ".rigkit", "state.sqlite");
77
+ this.metadata = {
78
+ projectId: options.projectId,
79
+ configPath: options.configPath,
80
+ runtimeVersion: options.runtimeVersion,
81
+ source: options.source,
82
+ };
83
+ this.db = createRigkitDatabase(this.path, { schema: this.schema });
84
+ }
85
+
86
+ async syncSchema(): Promise<SchemaSyncResult> {
87
+ this.schemaSync ??= this.syncSchemaOnce();
88
+ return await this.schemaSync;
89
+ }
90
+
91
+ listWorkspaces(): WorkspaceRecord[] {
92
+ return this.db.select().from(workspaces).orderBy(asc(workspaces.name)).all().map(toWorkspaceRecord);
93
+ }
94
+
95
+ findWorkspace(nameOrResourceId: string): WorkspaceRecord | undefined {
96
+ const row = this.db
97
+ .select()
98
+ .from(workspaces)
99
+ .where(or(eq(workspaces.name, nameOrResourceId), eq(workspaces.resourceId, nameOrResourceId)))
100
+ .get();
101
+ return row ? toWorkspaceRecord(row) : undefined;
102
+ }
103
+
104
+ getWorkspace(name: string): WorkspaceRecord | undefined {
105
+ const row = this.db.select().from(workspaces).where(eq(workspaces.name, name)).get();
106
+ return row ? toWorkspaceRecord(row) : undefined;
107
+ }
108
+
109
+ saveWorkspace(workspace: WorkspaceRecord): void {
110
+ this.db
111
+ .insert(workspaces)
112
+ .values(workspace)
113
+ .onConflictDoUpdate({
114
+ target: workspaces.name,
115
+ set: {
116
+ id: workspace.id,
117
+ providerId: workspace.providerId,
118
+ workflow: workspace.workflow,
119
+ resourceId: workspace.resourceId,
120
+ snapshotId: workspace.snapshotId,
121
+ sourceRef: workspace.sourceRef,
122
+ context: workspace.context,
123
+ updatedAt: workspace.updatedAt,
124
+ metadata: workspace.metadata,
125
+ },
126
+ })
127
+ .run();
128
+ }
129
+
130
+ deleteWorkspace(name: string): void {
131
+ this.db.delete(workspaces).where(eq(workspaces.name, name)).run();
132
+ }
133
+
134
+ listNodeRuns(): WorkflowNodeRunRecord[] {
135
+ return this.db
136
+ .select()
137
+ .from(workflowNodeRuns)
138
+ .orderBy(desc(workflowNodeRuns.createdAt))
139
+ .all()
140
+ .map(toNodeRunRecord);
141
+ }
142
+
143
+ listSnapshots(): SnapshotRecord[] {
144
+ return this.listNodeRuns();
145
+ }
146
+
147
+ findReusableNodeRun(input: {
148
+ workflow: string;
149
+ nodePath: string;
150
+ nodeKey: string;
151
+ providerFingerprint: string;
152
+ upstreamRunIds: readonly string[];
153
+ }): WorkflowNodeRunRecord | undefined {
154
+ const upstream = stableJson([...input.upstreamRunIds]);
155
+ const candidates = this.db
156
+ .select()
157
+ .from(workflowNodeRuns)
158
+ .where(eq(workflowNodeRuns.workflow, input.workflow))
159
+ .orderBy(desc(workflowNodeRuns.createdAt))
160
+ .all()
161
+ .filter((run) =>
162
+ !run.invalidated &&
163
+ run.nodePath === input.nodePath &&
164
+ run.nodeKey === input.nodeKey &&
165
+ run.providerFingerprint === input.providerFingerprint &&
166
+ stableJson(run.upstreamRunIds) === upstream
167
+ );
168
+
169
+ const row = candidates[0];
170
+ return row ? toNodeRunRecord(row) : undefined;
171
+ }
172
+
173
+ saveNodeRun(run: WorkflowNodeRunRecord): void {
174
+ this.db.insert(workflowNodeRuns).values(run).run();
175
+ }
176
+
177
+ providerStorage(providerId: string): ProviderStorage {
178
+ return new StateProviderStorage(this.db, providerId);
179
+ }
180
+
181
+ private async syncSchemaOnce(): Promise<SchemaSyncResult> {
182
+ const result = await syncRigkitDatabaseSchema(this.db, this.schema);
183
+ this.writeRuntimeMetadata(result.schemaVersion);
184
+ return result;
185
+ }
186
+
187
+ private writeRuntimeMetadata(schemaVersion: string): void {
188
+ const now = new Date().toISOString();
189
+ const entries: Array<[string, JsonValue]> = [
190
+ ["engine.version", RIGKIT_ENGINE_VERSION],
191
+ ["state.schemaVersion", schemaVersion],
192
+ ["project.dir", this.projectDir],
193
+ ["state.path", this.path],
194
+ ];
195
+
196
+ if (this.metadata.projectId) entries.push(["project.id", this.metadata.projectId]);
197
+ if (this.metadata.configPath) entries.push(["config.path", this.metadata.configPath]);
198
+ if (this.metadata.runtimeVersion) entries.push(["runtime.version", this.metadata.runtimeVersion]);
199
+ if (this.metadata.source !== undefined) entries.push(["source", this.metadata.source]);
200
+
201
+ for (const [key, value] of entries) {
202
+ this.db
203
+ .insert(runtimeMetadata)
204
+ .values({ key, value, updatedAt: now })
205
+ .onConflictDoUpdate({
206
+ target: runtimeMetadata.key,
207
+ set: { value, updatedAt: now },
208
+ })
209
+ .run();
210
+ }
211
+ }
212
+ }
213
+
214
+ export const createStateStore: StateServiceFactory = (options) =>
215
+ new StateStore(options.projectDir, options);
216
+
217
+ function toWorkspaceRecord(row: typeof workspaces.$inferSelect): WorkspaceRecord {
218
+ return {
219
+ id: row.id,
220
+ name: row.name,
221
+ providerId: row.providerId,
222
+ workflow: row.workflow,
223
+ resourceId: row.resourceId,
224
+ snapshotId: row.snapshotId ?? undefined,
225
+ sourceRef: row.sourceRef,
226
+ context: row.context,
227
+ createdAt: row.createdAt,
228
+ updatedAt: row.updatedAt,
229
+ metadata: row.metadata,
230
+ };
231
+ }
232
+
233
+ function toNodeRunRecord(row: typeof workflowNodeRuns.$inferSelect): WorkflowNodeRunRecord {
234
+ return {
235
+ id: row.id,
236
+ workflow: row.workflow,
237
+ nodePath: row.nodePath,
238
+ nodeName: row.nodeName,
239
+ nodeKind: row.nodeKind,
240
+ nodeKey: row.nodeKey,
241
+ providerFingerprint: row.providerFingerprint,
242
+ upstreamRunIds: row.upstreamRunIds,
243
+ output: row.output,
244
+ artifacts: row.artifacts,
245
+ invalidated: row.invalidated,
246
+ createdAt: row.createdAt,
247
+ metadata: row.metadata,
248
+ };
249
+ }
250
+
251
+ class StateProviderStorage implements ProviderStorage {
252
+ constructor(
253
+ private readonly db: RigkitDatabase<CoreSchema>,
254
+ private readonly providerId: string,
255
+ ) {}
256
+
257
+ get<Value extends JsonValue = JsonValue>(key: string): ProviderStorageRecord<Value> | undefined {
258
+ const row = this.db
259
+ .select()
260
+ .from(providerState)
261
+ .where(and(eq(providerState.providerId, this.providerId), eq(providerState.key, key)))
262
+ .get();
263
+ return row ? toProviderStorageRecord(row) as ProviderStorageRecord<Value> : undefined;
264
+ }
265
+
266
+ set<Value extends JsonValue = JsonValue>(key: string, value: Value): ProviderStorageRecord<Value> {
267
+ const now = new Date().toISOString();
268
+ const existing = this.get(key);
269
+ const record: ProviderStorageRecord<Value> = {
270
+ providerId: this.providerId,
271
+ key,
272
+ value,
273
+ createdAt: existing?.createdAt ?? now,
274
+ updatedAt: now,
275
+ };
276
+ this.db
277
+ .insert(providerState)
278
+ .values(record)
279
+ .onConflictDoUpdate({
280
+ target: [providerState.providerId, providerState.key],
281
+ set: {
282
+ value,
283
+ updatedAt: record.updatedAt,
284
+ },
285
+ })
286
+ .run();
287
+ return record;
288
+ }
289
+
290
+ delete(key: string): void {
291
+ this.db
292
+ .delete(providerState)
293
+ .where(and(eq(providerState.providerId, this.providerId), eq(providerState.key, key)))
294
+ .run();
295
+ }
296
+
297
+ entries(prefix = ""): ProviderStorageRecord[] {
298
+ const rows = this.db
299
+ .select()
300
+ .from(providerState)
301
+ .where(eq(providerState.providerId, this.providerId))
302
+ .orderBy(asc(providerState.key))
303
+ .all();
304
+ return rows
305
+ .filter((row) => row.key.startsWith(prefix))
306
+ .map(toProviderStorageRecord);
307
+ }
308
+ }
309
+
310
+ function toProviderStorageRecord(row: typeof providerState.$inferSelect): ProviderStorageRecord {
311
+ return {
312
+ providerId: row.providerId,
313
+ key: row.key,
314
+ value: row.value,
315
+ createdAt: row.createdAt,
316
+ updatedAt: row.updatedAt,
317
+ };
318
+ }