ignotum 0.0.0 → 0.0.2

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.
Files changed (57) hide show
  1. package/README.md +161 -0
  2. package/dist/cli/bin.d.mts +1 -0
  3. package/dist/cli/bin.mjs +1696 -0
  4. package/dist/cli/bin.mjs.map +1 -0
  5. package/dist/runtime/api-BXXIZc_Q.js +119 -0
  6. package/dist/runtime/api-BXXIZc_Q.js.map +1 -0
  7. package/dist/runtime/api-Cpx57mk3.d.ts +47 -0
  8. package/dist/runtime/client/jsx-dev-runtime.d.ts +2 -0
  9. package/dist/runtime/client/jsx-dev-runtime.js +2 -0
  10. package/dist/runtime/client/jsx-runtime.d.ts +2 -0
  11. package/dist/runtime/client/jsx-runtime.js +2 -0
  12. package/dist/runtime/client.d.ts +29 -0
  13. package/dist/runtime/client.js +364 -0
  14. package/dist/runtime/client.js.map +1 -0
  15. package/dist/runtime/index-BgWROoyk.d.ts +249 -0
  16. package/dist/runtime/internal/api.d.ts +2 -0
  17. package/dist/runtime/internal/api.js +2 -0
  18. package/dist/runtime/internal/server.d.ts +6 -0
  19. package/dist/runtime/internal/server.js +7 -0
  20. package/dist/runtime/internal/server.js.map +1 -0
  21. package/dist/runtime/internal/types.d.ts +2 -0
  22. package/dist/runtime/internal/types.js +2 -0
  23. package/dist/runtime/result-B2W-z2wG.js +136 -0
  24. package/dist/runtime/result-B2W-z2wG.js.map +1 -0
  25. package/dist/runtime/result-C1ZdsM6Y.d.ts +106 -0
  26. package/dist/runtime/schema-CNEVLF7D.js +116 -0
  27. package/dist/runtime/schema-CNEVLF7D.js.map +1 -0
  28. package/dist/runtime/server.d.ts +9 -0
  29. package/dist/runtime/server.js +9 -0
  30. package/dist/runtime/server.js.map +1 -0
  31. package/package.json +81 -2
  32. package/src/cli/agent-files.ts +35 -0
  33. package/src/cli/bin.ts +5 -0
  34. package/src/cli/client-plugin.ts +66 -0
  35. package/src/cli/codegen.ts +203 -0
  36. package/src/cli/command.ts +155 -0
  37. package/src/cli/dev.ts +206 -0
  38. package/src/cli/new-project.ts +377 -0
  39. package/src/cli/package-manager.ts +55 -0
  40. package/src/client/errors.ts +83 -0
  41. package/src/client/hooks.ts +141 -0
  42. package/src/client/index.ts +90 -0
  43. package/src/client/jsx-dev-runtime.ts +2 -0
  44. package/src/client/jsx-runtime.ts +2 -0
  45. package/src/client/query.ts +17 -0
  46. package/src/client/sync.ts +487 -0
  47. package/src/dev-runtime/database.ts +374 -0
  48. package/src/dev-runtime/dev-database.ts +199 -0
  49. package/src/dev-runtime/functions.ts +293 -0
  50. package/src/dev-runtime/id.ts +11 -0
  51. package/src/dev-runtime/sync.ts +473 -0
  52. package/src/internal/api.ts +141 -0
  53. package/src/internal/http-paths.ts +5 -0
  54. package/src/internal/server.ts +3 -0
  55. package/src/internal/types.ts +2 -0
  56. package/src/raw.d.ts +4 -0
  57. package/src/server/index.ts +8 -0
@@ -0,0 +1,1696 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { NodeHttpServer, NodeRuntime, NodeServices, NodeSocket } from "@effect/platform-node";
4
+ import { Array, Brand, Cause, Context, Crypto, DateTime, Effect, Effectable, FiberSet, FileSystem, Function, HashMap, Layer, ManagedRuntime, Option, PartitionedSemaphore, Path, Predicate, PubSub, Ref, Schema, Semaphore, String, Terminal } from "effect";
5
+ import { Argument, CliError, Command, Flag } from "effect/unstable/cli";
6
+ import { fileURLToPath } from "node:url";
7
+ import prefresh from "@prefresh/vite";
8
+ import tailwindcss from "@tailwindcss/vite";
9
+ import { createServer, defaultClientConditions, normalizePath } from "vite";
10
+ import { SqliteClient } from "@effect/sql-sqlite-node";
11
+ import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
12
+ import { SqlClient, SqlSchema } from "effect/unstable/sql";
13
+ import { customAlphabet, nanoid } from "nanoid";
14
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
15
+ //#region package.json
16
+ var version = "0.0.2";
17
+ //#endregion
18
+ //#region ../contracts/dist/value-Briqf-Ua.js
19
+ const TransportValueSchema = Schema.suspend(() => Schema.Union([
20
+ Schema.Null,
21
+ Schema.Boolean,
22
+ Schema.Finite,
23
+ Schema.String,
24
+ Schema.Date,
25
+ Schema.Array(TransportValueSchema),
26
+ Schema.Record(Schema.String, Schema.UndefinedOr(TransportValueSchema))
27
+ ]));
28
+ const collectDatePaths = (value, path, paths) => {
29
+ if (Predicate.isDate(value)) {
30
+ Schema.decodeSync(Schema.Date)(value);
31
+ paths.push(path);
32
+ return;
33
+ }
34
+ if (globalThis.Array.isArray(value)) {
35
+ for (const [index, child] of value.entries()) collectDatePaths(child, [...path, index], paths);
36
+ return;
37
+ }
38
+ if (value === void 0 || Predicate.isNull(value) || Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) return;
39
+ for (const [key, child] of Object.entries(value)) collectDatePaths(child, [...path, key], paths);
40
+ };
41
+ const datePathsOf = (value) => {
42
+ const paths = [];
43
+ collectDatePaths(value, [], paths);
44
+ return paths;
45
+ };
46
+ const datePathsOfObject = (value) => datePathsOf(Schema.decodeUnknownSync(TransportValueSchema)(value));
47
+ //#endregion
48
+ //#region ../contracts/dist/runtime/sync.js
49
+ const FunctionNamePart = Schema.String.check(Schema.isPattern(/^[A-Za-z_$][A-Za-z0-9_$]*$/));
50
+ const ApiFunctionAddressParts = Schema.TemplateLiteralParser([
51
+ "api.",
52
+ FunctionNamePart,
53
+ ".",
54
+ FunctionNamePart
55
+ ]);
56
+ const FunctionAddress = Schema.TemplateLiteral([
57
+ "api.",
58
+ FunctionNamePart,
59
+ ".",
60
+ FunctionNamePart
61
+ ]);
62
+ const apiFunctionParts = (address) => {
63
+ const [, moduleName, , functionName] = Schema.decodeSync(ApiFunctionAddressParts)(address);
64
+ return {
65
+ functionName,
66
+ moduleName
67
+ };
68
+ };
69
+ const SubscriptionId = Schema.Natural.pipe(Schema.brand("ignotum/sync/SubscriptionId"));
70
+ const RequestId = Schema.Natural.pipe(Schema.brand("ignotum/sync/RequestId"));
71
+ const Subscribe = Schema.Struct({
72
+ type: Schema.Literal("Subscribe"),
73
+ id: SubscriptionId,
74
+ function: FunctionAddress,
75
+ args: Schema.Json
76
+ });
77
+ const Unsubscribe = Schema.Struct({
78
+ type: Schema.Literal("Unsubscribe"),
79
+ id: SubscriptionId
80
+ });
81
+ const Mutate = Schema.Struct({
82
+ type: Schema.Literal("Mutate"),
83
+ id: RequestId,
84
+ function: FunctionAddress,
85
+ args: Schema.Json
86
+ });
87
+ const ClientMessage = Schema.Union([
88
+ Subscribe,
89
+ Unsubscribe,
90
+ Mutate
91
+ ]);
92
+ const QueryOperation = Schema.Struct({
93
+ type: Schema.Literal("Query"),
94
+ id: SubscriptionId
95
+ });
96
+ const MutateOperation = Schema.Struct({
97
+ type: Schema.Literal("Mutate"),
98
+ id: RequestId
99
+ });
100
+ const Operation = Schema.Union([QueryOperation, MutateOperation]);
101
+ const DatePath = Schema.Array(Schema.Union([Schema.String, Schema.Natural]));
102
+ const DatePaths = Schema.Array(DatePath);
103
+ const WireResult = Schema.Union([Schema.Struct({
104
+ type: Schema.Literal("Success"),
105
+ value: Schema.optional(Schema.Json),
106
+ dates: Schema.optional(DatePaths)
107
+ }), Schema.Struct({
108
+ type: Schema.Literal("Failure"),
109
+ error: Schema.Json,
110
+ dates: Schema.optional(DatePaths)
111
+ })]);
112
+ const QuerySnapshot = Schema.Struct({
113
+ type: Schema.Literal("QuerySnapshot"),
114
+ id: SubscriptionId,
115
+ result: WireResult
116
+ });
117
+ const MutationResult = Schema.Struct({
118
+ type: Schema.Literal("MutationResult"),
119
+ id: RequestId,
120
+ result: WireResult
121
+ });
122
+ const ProtocolErrorCode = Schema.Literals([
123
+ "DuplicateOperationId",
124
+ "FunctionUnavailable",
125
+ "InvalidArguments",
126
+ "InvalidMessage",
127
+ "UnknownFunction",
128
+ "WrongFunctionKind"
129
+ ]);
130
+ const ProtocolError = Schema.Struct({
131
+ type: Schema.Literal("ProtocolError"),
132
+ operation: Schema.optional(Operation),
133
+ code: ProtocolErrorCode,
134
+ message: Schema.String
135
+ });
136
+ const ServerMessage = Schema.Union([
137
+ QuerySnapshot,
138
+ MutationResult,
139
+ ProtocolError
140
+ ]);
141
+ const ClientMessageJson = Schema.fromJsonString(ClientMessage);
142
+ const ServerMessageJson = Schema.fromJsonString(ServerMessage);
143
+ //#endregion
144
+ //#region src/cli/codegen.ts
145
+ const generatedHeader = "// Generated by `ignotum codegen`. Do not edit.";
146
+ var SchemaNotFound = class extends Schema.TaggedError()("SchemaNotFound", {
147
+ message: Schema.String,
148
+ path: Schema.String
149
+ }) {};
150
+ var InvalidFunctionModuleName = class extends Schema.TaggedError()("InvalidFunctionModuleName", {
151
+ message: Schema.String,
152
+ path: Schema.String
153
+ }) {};
154
+ var GeneratedFileConflict = class extends Schema.TaggedError()("GeneratedFileConflict", {
155
+ message: Schema.String,
156
+ path: Schema.String
157
+ }) {};
158
+ const isFunctionModuleFile = (fileName) => fileName.endsWith(".ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".spec.ts") && fileName !== "index.ts" && fileName !== "schema.ts" && !fileName.startsWith("_");
159
+ const moduleNameFromFile = (filePath, fileName) => {
160
+ const moduleName = fileName.slice(0, -3);
161
+ return Schema.decodeEffect(FunctionNamePart)(moduleName).pipe(Effect.mapError(() => InvalidFunctionModuleName.make({
162
+ message: `Server function file names must be valid TypeScript identifiers. Rename ${fileName}.`,
163
+ path: filePath
164
+ })));
165
+ };
166
+ const renderServerBindings = () => `${generatedHeader}
167
+
168
+ import { bindSchema } from "ignotum/internal/server";
169
+
170
+ import schema from "../server/schema.js";
171
+
172
+ export const { mutation, query, values } = bindSchema(schema);
173
+ `;
174
+ const renderTypes = () => `${generatedHeader}
175
+
176
+ import type {
177
+ Document as DocumentFor,
178
+ Id as IdFor,
179
+ SchemaDefinitionOf,
180
+ } from "ignotum/internal/types";
181
+
182
+ import type schema from "../server/schema.js";
183
+
184
+ export type DataModel = SchemaDefinitionOf<typeof schema>;
185
+ export type Doc<TableName extends Extract<keyof DataModel, string>> = DocumentFor<
186
+ DataModel,
187
+ TableName
188
+ >;
189
+ export type Id<TableName extends Extract<keyof DataModel, string>> = IdFor<TableName>;
190
+ `;
191
+ const renderApi = (moduleNames) => {
192
+ const modules = Array.match(moduleNames, {
193
+ onEmpty: () => " // No server function modules found.",
194
+ onNonEmpty: (names) => Array.join(Array.map(names, (moduleName) => ` readonly ${moduleName}: typeof import("../server/${moduleName}.js");`), "\n")
195
+ });
196
+ return `${generatedHeader}
197
+
198
+ import { createApi } from "ignotum/internal/api";
199
+
200
+ type Modules = {
201
+ ${modules}
202
+ };
203
+
204
+ export const api = createApi<Modules>();
205
+ `;
206
+ };
207
+ const inspectOutput = Effect.fn("Codegen.inspectOutput")(function* (output) {
208
+ const fileSystem = yield* FileSystem.FileSystem;
209
+ if (!(yield* fileSystem.exists(output.path))) return "write";
210
+ const current = yield* fileSystem.readFileString(output.path);
211
+ if (current === output.content) return "unchanged";
212
+ if (!current.startsWith(generatedHeader)) return yield* GeneratedFileConflict.make({
213
+ message: `Refusing to overwrite ${output.path} because it was not created by Ignotum.`,
214
+ path: output.path
215
+ });
216
+ return "write";
217
+ });
218
+ const writeOutput = Effect.fn("Codegen.writeOutput")(function* (output) {
219
+ const fileSystem = yield* FileSystem.FileSystem;
220
+ const directory = (yield* Path.Path).dirname(output.path);
221
+ yield* fileSystem.makeDirectory(directory, { recursive: true });
222
+ yield* Effect.scoped(Effect.gen(function* () {
223
+ const temporaryPath = yield* fileSystem.makeTempFileScoped({
224
+ directory,
225
+ prefix: ".ignotum-",
226
+ suffix: ".ts"
227
+ });
228
+ yield* fileSystem.writeFileString(temporaryPath, output.content);
229
+ yield* fileSystem.rename(temporaryPath, output.path);
230
+ }));
231
+ });
232
+ const generate = Effect.fn("Codegen.generate")(function* (projectDirectory) {
233
+ const fileSystem = yield* FileSystem.FileSystem;
234
+ const path = yield* Path.Path;
235
+ const serverDirectory = path.join(projectDirectory, "server");
236
+ const schemaPath = path.join(serverDirectory, "schema.ts");
237
+ if (!(yield* fileSystem.exists(schemaPath))) return yield* SchemaNotFound.make({
238
+ message: `No Ignotum schema found at ${schemaPath}.`,
239
+ path: schemaPath
240
+ });
241
+ const entries = yield* fileSystem.readDirectory(serverDirectory);
242
+ const functionFiles = Array.sort(String.Order)(Array.filter(entries, isFunctionModuleFile));
243
+ const functionModules = yield* Effect.forEach(functionFiles, (fileName) => moduleNameFromFile(path.join(serverDirectory, fileName), fileName));
244
+ const outputs = [
245
+ {
246
+ content: renderServerBindings(),
247
+ path: path.join(projectDirectory, "_generated", "server.ts")
248
+ },
249
+ {
250
+ content: renderApi(functionModules),
251
+ path: path.join(projectDirectory, "_generated", "api.ts")
252
+ },
253
+ {
254
+ content: renderTypes(),
255
+ path: path.join(projectDirectory, "_generated", "types.ts")
256
+ }
257
+ ];
258
+ const inspections = yield* Effect.forEach(outputs, inspectOutput);
259
+ const inspectedOutputs = Array.zip(outputs, inspections);
260
+ const outputsToWrite = Array.map(Array.filter(inspectedOutputs, ([, inspection]) => inspection === "write"), ([output]) => output);
261
+ const unchanged = Array.map(Array.filter(inspectedOutputs, ([, inspection]) => inspection === "unchanged"), ([output]) => output.path);
262
+ const written = Array.map(outputsToWrite, (output) => output.path);
263
+ yield* Effect.forEach(outputsToWrite, writeOutput, { discard: true });
264
+ return {
265
+ functionModules,
266
+ unchanged,
267
+ written
268
+ };
269
+ });
270
+ //#endregion
271
+ //#region src/dev-runtime/dev-database.ts
272
+ const databaseFileNames = [
273
+ "state.db",
274
+ "state.db-shm",
275
+ "state.db-wal",
276
+ "state.db-journal"
277
+ ];
278
+ const lockFileName = "lock.json";
279
+ const DevDatabaseLockRecord = Schema.Struct({
280
+ pid: Schema.Int,
281
+ token: Schema.String
282
+ });
283
+ const DevDatabaseLockRecordJson = Schema.fromJsonString(DevDatabaseLockRecord);
284
+ var DevDatabaseResetFailed = class extends Schema.TaggedError()("DevDatabaseResetFailed", {
285
+ cause: Schema.Defect(),
286
+ message: Schema.String,
287
+ path: Schema.String
288
+ }) {};
289
+ var DevDatabaseInUse = class extends Schema.TaggedError()("DevDatabaseInUse", {
290
+ message: Schema.String,
291
+ path: Schema.String,
292
+ pid: Schema.Int
293
+ }) {};
294
+ var DevDatabaseLockFailed = class extends Schema.TaggedError()("DevDatabaseLockFailed", {
295
+ cause: Schema.Defect(),
296
+ message: Schema.String,
297
+ path: Schema.String
298
+ }) {};
299
+ const devDatabasePath = Effect.fn("DevDatabase.path")(function* (projectDirectory) {
300
+ return (yield* Path.Path).join(projectDirectory, ".ignotum", "dev", "state.db");
301
+ });
302
+ const processIsRunning = (pid) => {
303
+ try {
304
+ process.kill(pid, 0);
305
+ return true;
306
+ } catch (cause) {
307
+ return !(Predicate.hasProperty(cause, "code") && cause.code === "ESRCH");
308
+ }
309
+ };
310
+ const acquireDevDatabaseLock = Effect.fn("DevDatabase.acquireLock")(function* (projectDirectory) {
311
+ const crypto = yield* Crypto.Crypto;
312
+ const fileSystem = yield* FileSystem.FileSystem;
313
+ const path = yield* Path.Path;
314
+ const databasePath = yield* devDatabasePath(projectDirectory);
315
+ const directory = path.dirname(databasePath);
316
+ const lockPath = path.join(directory, lockFileName);
317
+ const record = {
318
+ pid: process.pid,
319
+ token: yield* crypto.randomUUIDv4
320
+ };
321
+ const encoded = yield* Schema.encodeEffect(DevDatabaseLockRecordJson)(record).pipe(Effect.mapError((cause) => DevDatabaseLockFailed.make({
322
+ cause,
323
+ message: `Could not encode the development database lock at ${lockPath}.`,
324
+ path: lockPath
325
+ })));
326
+ yield* fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError((cause) => DevDatabaseLockFailed.make({
327
+ cause,
328
+ message: `Could not create the development state directory at ${directory}.`,
329
+ path: lockPath
330
+ })));
331
+ const createLock = () => fileSystem.writeFileString(lockPath, encoded, { flag: "wx" }).pipe(Effect.matchEffect({
332
+ onFailure: (cause) => cause.reason._tag === "AlreadyExists" ? Effect.succeed(false) : DevDatabaseLockFailed.make({
333
+ cause,
334
+ message: `Could not create the development database lock at ${lockPath}.`,
335
+ path: lockPath
336
+ }),
337
+ onSuccess: () => Effect.succeed(true)
338
+ }));
339
+ const readLock = Effect.fn("DevDatabase.readLock")(function* () {
340
+ const content = yield* fileSystem.readFileString(lockPath).pipe(Effect.mapError((cause) => DevDatabaseLockFailed.make({
341
+ cause,
342
+ message: `Could not read the development database lock at ${lockPath}.`,
343
+ path: lockPath
344
+ })));
345
+ return yield* Schema.decodeEffect(DevDatabaseLockRecordJson)(content).pipe(Effect.mapError((cause) => DevDatabaseLockFailed.make({
346
+ cause,
347
+ message: `The development database lock at ${lockPath} is invalid. Remove it and try again.`,
348
+ path: lockPath
349
+ })));
350
+ });
351
+ const failInUse = (current) => DevDatabaseInUse.make({
352
+ message: `Another Ignotum development server is using ${databasePath} with process ${current.pid}.`,
353
+ path: lockPath,
354
+ pid: current.pid
355
+ });
356
+ const acquire = Effect.gen(function* () {
357
+ if (yield* createLock()) return record;
358
+ const current = yield* readLock();
359
+ if (processIsRunning(current.pid)) return yield* failInUse(current);
360
+ yield* fileSystem.remove(lockPath).pipe(Effect.mapError((cause) => DevDatabaseLockFailed.make({
361
+ cause,
362
+ message: `Could not remove the stale development database lock at ${lockPath}.`,
363
+ path: lockPath
364
+ })));
365
+ if (yield* createLock()) return record;
366
+ return yield* failInUse(yield* readLock());
367
+ });
368
+ const release = (owned) => Effect.gen(function* () {
369
+ if (!(yield* fileSystem.exists(lockPath))) return;
370
+ const content = yield* fileSystem.readFileString(lockPath);
371
+ const current = Schema.decodeOption(DevDatabaseLockRecordJson)(content);
372
+ if (Option.isNone(current) || current.value.token !== owned.token) return;
373
+ yield* fileSystem.remove(lockPath);
374
+ }).pipe(Effect.catchCause((cause) => Effect.logWarning(`Could not release the development database lock at ${lockPath}.`).pipe(Effect.annotateLogs({ cause }))));
375
+ yield* Effect.acquireRelease(acquire, release);
376
+ });
377
+ const resetDevDatabase = Effect.fn("DevDatabase.reset")(function* (projectDirectory) {
378
+ return yield* Effect.scoped(Effect.gen(function* () {
379
+ yield* acquireDevDatabaseLock(projectDirectory);
380
+ const fileSystem = yield* FileSystem.FileSystem;
381
+ const path = yield* Path.Path;
382
+ const directory = path.join(projectDirectory, ".ignotum", "dev");
383
+ const databasePath = yield* devDatabasePath(projectDirectory);
384
+ let removed = 0;
385
+ yield* Effect.forEach(databaseFileNames, Effect.fn("DevDatabase.removeFile")(function* (fileName) {
386
+ const filePath = path.join(directory, fileName);
387
+ if (!(yield* fileSystem.exists(filePath))) return;
388
+ yield* fileSystem.remove(filePath);
389
+ removed += 1;
390
+ }), { discard: true }).pipe(Effect.mapError((cause) => DevDatabaseResetFailed.make({
391
+ cause,
392
+ message: `Could not reset ${databasePath}. Stop the Ignotum dev server and try again.`,
393
+ path: databasePath
394
+ })));
395
+ return {
396
+ databasePath,
397
+ removed
398
+ };
399
+ }));
400
+ });
401
+ //#endregion
402
+ //#region src/cli/client-plugin.ts
403
+ const clientEntryId = "virtual:ignotum/client-entry";
404
+ const resolvedClientEntryId = `\0${clientEntryId}`;
405
+ const document = `<!doctype html>
406
+ <html lang="en">
407
+ <head>
408
+ <meta charset="UTF-8" />
409
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
410
+ <title>Ignotum</title>
411
+ </head>
412
+ <body>
413
+ <div id="app"></div>
414
+ <script type="module">
415
+ import "${clientEntryId}"
416
+ <\/script>
417
+ </body>
418
+ </html>`;
419
+ const clientEntry = `import "preact/debug"
420
+ import { render } from "preact"
421
+ import { jsx } from "preact/jsx-runtime"
422
+
423
+ import "/styles.css"
424
+ import App from "/App.tsx"
425
+
426
+ const root = document.getElementById("app")
427
+
428
+ if (root === null) {
429
+ throw new Error("Ignotum mount element is missing.")
430
+ }
431
+
432
+ render(jsx(App, {}), root)
433
+ `;
434
+ const ignotumClientPlugin = () => ({
435
+ name: "ignotum:client",
436
+ configureServer: (server) => {
437
+ server.middlewares.use((request, response, next) => {
438
+ const acceptsHtml = request.headers.accept?.includes("text/html") === true;
439
+ const isNavigation = request.method === "GET" || request.method === "HEAD";
440
+ if (!acceptsHtml || !isNavigation) {
441
+ next();
442
+ return;
443
+ }
444
+ const requestUrl = request.originalUrl ?? request.url ?? "/";
445
+ server.transformIndexHtml(requestUrl, document).then((html) => {
446
+ response.statusCode = 200;
447
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
448
+ response.end(request.method === "HEAD" ? void 0 : html);
449
+ }).catch(next);
450
+ });
451
+ },
452
+ load: (id) => id === resolvedClientEntryId ? clientEntry : void 0,
453
+ resolveId: (id) => id === clientEntryId ? resolvedClientEntryId : void 0
454
+ });
455
+ //#endregion
456
+ //#region ../runtime/dist/functions.js
457
+ var FunctionRuntime = class extends Context.Service()("@ignotum/runtime/functions/FunctionRuntime") {};
458
+ //#endregion
459
+ //#region ../runtime/dist/sync.js
460
+ var QueryInvalidation = class extends Context.Service()("@ignotum/runtime/sync/QueryInvalidation") {};
461
+ const syncPath = `/_ignotum/sync`;
462
+ const isIgnotumPath = (pathname) => pathname === "/_ignotum" || pathname.startsWith(`/_ignotum/`);
463
+ //#endregion
464
+ //#region ../contracts/dist/runtime/id.js
465
+ const IdAlphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
466
+ const GeneratedId = Schema.String.check(Schema.isPattern(/^[0-9a-z]{24}$/)).pipe(Schema.brand("ignotum/id"));
467
+ //#endregion
468
+ //#region ../contracts/dist/runtime/result.js
469
+ const ResultTypeId = Symbol.for("ignotum/runtime/result/Result");
470
+ const CompletedResultTypeId = Symbol.for("ignotum/runtime/result/Completed");
471
+ const PendingResultTypeId = Symbol.for("ignotum/runtime/result/Pending");
472
+ const ErrorBrand = "ignotum/error";
473
+ Brand.nominal();
474
+ const ErrorValueSchema = Schema.StructWithRest(Schema.Struct({ _tag: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]).pipe(Schema.brand(ErrorBrand));
475
+ var InternalServerErrorCause = class extends Schema.TaggedError()("InternalServerError", { requestId: Schema.String }) {};
476
+ const InternalServerErrorSchema = InternalServerErrorCause.pipe(Schema.brand(ErrorBrand));
477
+ const effectOf = (value) => Predicate.hasProperty(value, ResultTypeId) ? value[ResultTypeId] : Effect.succeed(value);
478
+ function makeResultEffectBase() {
479
+ const Base = function() {};
480
+ Base.prototype = Effectable.Prototype({
481
+ label: "IgnotumResult",
482
+ evaluate() {
483
+ return this[ResultTypeId];
484
+ }
485
+ });
486
+ return Base;
487
+ }
488
+ const ResultEffectBase = makeResultEffectBase();
489
+ var ResultOperation = class ResultOperation extends ResultEffectBase {
490
+ [ResultTypeId];
491
+ constructor(effect, completed) {
492
+ super();
493
+ this[ResultTypeId] = effect;
494
+ if (completed !== void 0) Object.defineProperty(this, CompletedResultTypeId, { value: completed });
495
+ }
496
+ catch(cases) {
497
+ const effectCases = {};
498
+ for (const [tag, handler] of Object.entries(cases)) if (Predicate.isFunction(handler)) effectCases[tag] = (failure) => effectOf(handler(failure));
499
+ const runtimeEffect = this[ResultTypeId];
500
+ const caught = Effect.catchTags(runtimeEffect, effectCases);
501
+ return new ResultOperation(caught);
502
+ }
503
+ };
504
+ var PendingQueryResult = class {
505
+ [PendingResultTypeId] = PendingResultTypeId;
506
+ };
507
+ const inspectResult = (value) => {
508
+ if (!Predicate.hasProperty(value, CompletedResultTypeId)) return void 0;
509
+ return value[CompletedResultTypeId];
510
+ };
511
+ const resultFromEffect = (effect) => new ResultOperation(effect);
512
+ const isFailureResult = (result) => Predicate.hasProperty(result, CompletedResultTypeId) && Predicate.isObject(result[CompletedResultTypeId]) && Predicate.hasProperty(result[CompletedResultTypeId], "type") && result[CompletedResultTypeId].type === "Failure";
513
+ Function.dual(2, (result, matchers) => {
514
+ if (result instanceof PendingQueryResult) {
515
+ if (matchers.pending === void 0) throw new Error("A pending Result requires a pending matcher.");
516
+ return matchers.pending();
517
+ }
518
+ if (!Predicate.hasProperty(result, ResultTypeId)) throw new Error("Unknown Result implementation.");
519
+ const inspected = inspectResult(result);
520
+ if (inspected?.type === "Success") {
521
+ const value = inspected.value;
522
+ return matchers.value(value);
523
+ }
524
+ if (!isFailureResult(result)) throw new Error("Unknown or incomplete Result operation.");
525
+ const error = result[CompletedResultTypeId].error;
526
+ if (error._tag === "InternalServerError") {
527
+ const internalError = Schema.decodeUnknownSync(InternalServerErrorSchema)(error);
528
+ const handler = matchers.internalError;
529
+ if (handler === void 0) throw internalError;
530
+ return handler(internalError);
531
+ }
532
+ const errorMatcher = matchers.error;
533
+ if (errorMatcher === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
534
+ if (Predicate.isFunction(errorMatcher)) return errorMatcher(error);
535
+ const handlers = errorMatcher;
536
+ const handler = Object.hasOwn(handlers, error._tag) ? handlers[error._tag] : void 0;
537
+ if (handler === void 0) throw new Error(`No matcher was provided for ${error._tag}.`);
538
+ return handler(error);
539
+ });
540
+ const documentNotFound = (table, id) => Brand.nominal()({
541
+ _tag: "DocumentNotFound",
542
+ table,
543
+ id
544
+ });
545
+ //#endregion
546
+ //#region ../contracts/dist/runtime/schema.js
547
+ const FunctionSchemaTypeId = Symbol.for("ignotum/runtime/schema/FunctionSchema");
548
+ const getFunctionSchema = (value) => value[FunctionSchemaTypeId];
549
+ //#endregion
550
+ //#region ../contracts/dist/schema/index.js
551
+ const SchemaDefinitionTypeId = Symbol.for("ignotum/schema/SchemaDefinition");
552
+ //#endregion
553
+ //#region ../runtime/dist/id.js
554
+ var IdGenerator = class extends Context.Service()("@ignotum/runtime/id/IdGenerator") {};
555
+ //#endregion
556
+ //#region src/dev-runtime/database.ts
557
+ const StoredDocument = Schema.Struct({
558
+ id: GeneratedId,
559
+ tableName: Schema.String,
560
+ createdAt: Schema.Int,
561
+ updatedAt: Schema.Int,
562
+ fields: Schema.String
563
+ });
564
+ const DocumentLookup = Schema.Struct({
565
+ id: Schema.String,
566
+ tableName: Schema.String
567
+ });
568
+ const TableLookup = Schema.Struct({ tableName: Schema.String });
569
+ var UnknownDatabaseTable = class extends Schema.TaggedError()("UnknownDatabaseTable", {
570
+ message: Schema.String,
571
+ tableName: Schema.String
572
+ }) {};
573
+ var DocumentSchemaMismatch = class extends Schema.TaggedError()("DocumentSchemaMismatch", {
574
+ cause: Schema.Defect(),
575
+ id: Schema.NullOr(Schema.String),
576
+ message: Schema.String,
577
+ operation: Schema.Literals([
578
+ "collect",
579
+ "get",
580
+ "insert",
581
+ "patch",
582
+ "replace"
583
+ ]),
584
+ tableName: Schema.String
585
+ }) {};
586
+ const schemaMismatch = (tableName, id, operation, cause) => DocumentSchemaMismatch.make({
587
+ cause,
588
+ id,
589
+ message: `Document fields for ${tableName} did not match its schema during ${operation}.`,
590
+ operation,
591
+ tableName
592
+ });
593
+ const tableFor = Effect.fn("LocalDatabase.tableFor")(function* (schema, tableName) {
594
+ const table = schema[SchemaDefinitionTypeId][tableName];
595
+ if (table === void 0) return yield* UnknownDatabaseTable.make({
596
+ message: `Unknown database table ${tableName}.`,
597
+ tableName
598
+ });
599
+ return table;
600
+ });
601
+ const fieldsCodec = (table) => Schema.fromJsonString(Schema.make(Schema.toCodecJson(table.schema).ast));
602
+ const encodeFields = Effect.fn("LocalDatabase.encodeFields")(function* (schema, tableName, value, operation, id) {
603
+ const table = yield* tableFor(schema, tableName);
604
+ return yield* Schema.encodeEffect(fieldsCodec(table), { onExcessProperty: "error" })(value).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
605
+ });
606
+ const decodeFields = Effect.fn("LocalDatabase.decodeFields")(function* (schema, row, operation) {
607
+ const table = yield* tableFor(schema, row.tableName);
608
+ return yield* Schema.decodeEffect(fieldsCodec(table), { onExcessProperty: "error" })(row.fields).pipe(Effect.mapError((cause) => schemaMismatch(row.tableName, row.id, operation, cause)));
609
+ });
610
+ const toRuntimeDocument = Effect.fn("LocalDatabase.toRuntimeDocument")(function* (schema, row, operation) {
611
+ return {
612
+ ...yield* decodeFields(schema, row, operation),
613
+ id: row.id,
614
+ createdAt: DateTime.toDate(DateTime.makeUnsafe(row.createdAt)),
615
+ updatedAt: DateTime.toDate(DateTime.makeUnsafe(row.updatedAt))
616
+ };
617
+ });
618
+ const nextUpdatedAt = (now, previous) => Math.max(DateTime.toEpochMillis(now), previous + 1);
619
+ var LocalDatabase = class LocalDatabase extends Context.Service()("ignotum/dev-runtime/database/LocalDatabase") {
620
+ static layer = Layer.effect(LocalDatabase, Effect.gen(function* () {
621
+ const ids = yield* IdGenerator;
622
+ const sql = yield* SqlClient.SqlClient;
623
+ yield* sql`
624
+ CREATE TABLE IF NOT EXISTS documents (
625
+ id TEXT PRIMARY KEY NOT NULL
626
+ CHECK (length(id) = 24 AND id NOT GLOB '*[^0-9a-z]*'),
627
+ tableName TEXT NOT NULL,
628
+ createdAt INTEGER NOT NULL,
629
+ updatedAt INTEGER NOT NULL,
630
+ fields TEXT NOT NULL CHECK (json_valid(fields))
631
+ ) STRICT
632
+ `;
633
+ yield* sql`
634
+ CREATE INDEX IF NOT EXISTS documents_table_name_idx
635
+ ON documents (tableName)
636
+ `;
637
+ const findStored = SqlSchema.findOneOption({
638
+ Request: DocumentLookup,
639
+ Result: StoredDocument,
640
+ execute: ({ id, tableName }) => sql`
641
+ SELECT id, tableName, createdAt, updatedAt, fields
642
+ FROM documents
643
+ WHERE id = ${id} AND tableName = ${tableName}
644
+ LIMIT 1
645
+ `
646
+ });
647
+ const collectStored = SqlSchema.findAll({
648
+ Request: TableLookup,
649
+ Result: StoredDocument,
650
+ execute: ({ tableName }) => sql`
651
+ SELECT id, tableName, createdAt, updatedAt, fields
652
+ FROM documents
653
+ WHERE tableName = ${tableName}
654
+ ORDER BY createdAt ASC, id ASC
655
+ `
656
+ });
657
+ const find = Effect.fn("LocalDatabase.find")(function* (schema, tableName, id) {
658
+ yield* tableFor(schema, tableName);
659
+ const row = yield* findStored({
660
+ id,
661
+ tableName
662
+ });
663
+ if (Option.isNone(row)) return void 0;
664
+ return yield* toRuntimeDocument(schema, row.value, "get");
665
+ });
666
+ const collect = Effect.fn("LocalDatabase.collect")(function* (schema, tableName) {
667
+ yield* tableFor(schema, tableName);
668
+ const rows = yield* collectStored({ tableName });
669
+ return yield* Effect.forEach(rows, (row) => toRuntimeDocument(schema, row, "collect"));
670
+ });
671
+ const insert = Effect.fn("LocalDatabase.insert")(function* (schema, tableName, value) {
672
+ const fields = yield* encodeFields(schema, tableName, value, "insert", null);
673
+ const id = yield* ids.generate;
674
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
675
+ yield* sql`
676
+ INSERT INTO documents (id, tableName, createdAt, updatedAt, fields)
677
+ VALUES (${id}, ${tableName}, ${now}, ${now}, ${fields})
678
+ `;
679
+ return id;
680
+ });
681
+ const deleteDocument = Effect.fn("LocalDatabase.delete")(function* (schema, tableName, id) {
682
+ yield* tableFor(schema, tableName);
683
+ yield* sql`DELETE FROM documents WHERE id = ${id} AND tableName = ${tableName}`;
684
+ });
685
+ const patch = Effect.fn("LocalDatabase.patch")(function* (schema, tableName, id, value) {
686
+ yield* tableFor(schema, tableName);
687
+ const stored = yield* findStored({
688
+ id,
689
+ tableName
690
+ });
691
+ if (Option.isNone(stored)) return;
692
+ const current = yield* decodeFields(schema, stored.value, "patch");
693
+ const fields = yield* encodeFields(schema, tableName, {
694
+ ...current,
695
+ ...value
696
+ }, "patch", id);
697
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
698
+ yield* sql`
699
+ UPDATE documents
700
+ SET fields = ${fields}, updatedAt = ${updatedAt}
701
+ WHERE id = ${id} AND tableName = ${tableName}
702
+ `;
703
+ });
704
+ const replace = Effect.fn("LocalDatabase.replace")(function* (schema, tableName, id, value) {
705
+ yield* tableFor(schema, tableName);
706
+ const stored = yield* findStored({
707
+ id,
708
+ tableName
709
+ });
710
+ if (Option.isNone(stored)) return;
711
+ const fields = yield* encodeFields(schema, tableName, value, "replace", id);
712
+ const updatedAt = nextUpdatedAt(yield* DateTime.now, stored.value.updatedAt);
713
+ yield* sql`
714
+ UPDATE documents
715
+ SET fields = ${fields}, updatedAt = ${updatedAt}
716
+ WHERE id = ${id} AND tableName = ${tableName}
717
+ `;
718
+ });
719
+ const makeReader = (schema) => Object.freeze({
720
+ find: (tableName, id) => resultFromEffect(find(schema, tableName, id).pipe(Effect.orDie)),
721
+ get: (tableName, id) => resultFromEffect(find(schema, tableName, id).pipe(Effect.orDie, Effect.flatMap((document) => document === void 0 ? Effect.fail(documentNotFound(tableName, id)) : Effect.succeed(document)))),
722
+ query: (tableName) => Object.freeze({ collect: () => resultFromEffect(collect(schema, tableName).pipe(Effect.orDie)) })
723
+ });
724
+ const makeWriter = (schema) => {
725
+ const reader = makeReader(schema);
726
+ return Object.freeze({
727
+ ...reader,
728
+ delete: (tableName, id) => resultFromEffect(deleteDocument(schema, tableName, id).pipe(Effect.orDie)),
729
+ insert: (tableName, value) => resultFromEffect(insert(schema, tableName, value).pipe(Effect.orDie)),
730
+ patch: (tableName, id, value) => resultFromEffect(patch(schema, tableName, id, value).pipe(Effect.orDie)),
731
+ replace: (tableName, id, value) => resultFromEffect(replace(schema, tableName, id, value).pipe(Effect.orDie))
732
+ });
733
+ };
734
+ return LocalDatabase.of({
735
+ queryTransaction: (schema, use) => sql.withTransaction(Effect.gen(function* () {
736
+ return yield* use(Object.freeze({ db: makeReader(schema) }));
737
+ })),
738
+ mutationTransaction: (schema, use) => sql.withTransaction(Effect.gen(function* () {
739
+ return yield* use(Object.freeze({ db: makeWriter(schema) }));
740
+ }))
741
+ });
742
+ }));
743
+ };
744
+ //#endregion
745
+ //#region src/dev-runtime/id.ts
746
+ const idGeneratorLayer = Layer.sync(IdGenerator, () => {
747
+ const nanoid = customAlphabet(IdAlphabet, 24);
748
+ return IdGenerator.of({ generate: Effect.sync(() => GeneratedId.make(nanoid())) });
749
+ });
750
+ //#endregion
751
+ //#region ../contracts/dist/runtime/functions.js
752
+ const FunctionKind = Schema.Literals(["Mutation", "Query"]);
753
+ var UnknownFunction = class extends Schema.TaggedError()("UnknownFunction", {
754
+ function: FunctionAddress,
755
+ message: Schema.String
756
+ }) {};
757
+ var WrongFunctionKind = class extends Schema.TaggedError()("WrongFunctionKind", {
758
+ actual: FunctionKind,
759
+ expected: FunctionKind,
760
+ function: FunctionAddress,
761
+ message: Schema.String
762
+ }) {};
763
+ var InvalidArguments = class extends Schema.TaggedError()("InvalidArguments", {
764
+ function: FunctionAddress,
765
+ message: Schema.String
766
+ }) {};
767
+ var FunctionUnavailable = class extends Schema.TaggedError()("FunctionUnavailable", {
768
+ cause: Schema.Defect(),
769
+ function: FunctionAddress,
770
+ message: Schema.String
771
+ }) {};
772
+ //#endregion
773
+ //#region src/dev-runtime/functions.ts
774
+ const inspectRuntimeFunctionDefinition = (value) => {
775
+ const schema = getFunctionSchema(value);
776
+ if (schema === void 0 || !Predicate.hasProperty(value, "_tag") || value._tag !== "Mutation" && value._tag !== "Query" || Predicate.hasProperty(value, "args") && value.args !== void 0 && !Predicate.isObject(value.args) || Predicate.hasProperty(value, "returns") && value.returns !== void 0 && !Schema.isSchema(value.returns) || Predicate.hasProperty(value, "errors") && value.errors !== void 0 && !Schema.isSchema(value.errors) || !Predicate.hasProperty(value, "handler") || !Predicate.isFunction(value.handler)) return;
777
+ if (Predicate.hasProperty(value, "args") && Predicate.isObject(value.args) && !Object.values(value.args).every(Schema.isSchema)) return;
778
+ return {
779
+ definition: value,
780
+ schema
781
+ };
782
+ };
783
+ var FunctionRegistry = class FunctionRegistry extends Context.Service()("ignotum/dev-runtime/functions/FunctionRegistry") {
784
+ static layer(server, projectDirectory) {
785
+ return Layer.effect(FunctionRegistry, Effect.gen(function* () {
786
+ const path = yield* Path.Path;
787
+ return FunctionRegistry.of({ resolve: Effect.fn("FunctionRegistry.resolve")(function* (functionAddress, kind, args) {
788
+ const { functionName, moduleName } = apiFunctionParts(functionAddress);
789
+ const modulePath = normalizePath(path.join(projectDirectory, "server", `${moduleName}.ts`));
790
+ const candidate = (yield* Effect.tryPromise({
791
+ try: () => server.ssrLoadModule(modulePath),
792
+ catch: (cause) => FunctionUnavailable.make({
793
+ cause,
794
+ function: functionAddress,
795
+ message: `Could not load ${functionAddress}.`
796
+ })
797
+ }))[functionName];
798
+ const inspected = Predicate.isObject(candidate) ? inspectRuntimeFunctionDefinition(candidate) : void 0;
799
+ if (inspected === void 0) return yield* UnknownFunction.make({
800
+ function: functionAddress,
801
+ message: `Unknown server function ${functionAddress}.`
802
+ });
803
+ const definition = inspected.definition;
804
+ if (definition._tag !== kind) return yield* WrongFunctionKind.make({
805
+ actual: definition._tag,
806
+ expected: kind,
807
+ function: functionAddress,
808
+ message: `${functionAddress} is a ${definition._tag.toLowerCase()}, not a ${kind.toLowerCase()}.`
809
+ });
810
+ return {
811
+ args: yield* Schema.decodeEffect(Schema.toCodecJson(Schema.Struct(definition.args ?? {})))(args).pipe(Effect.mapError(() => InvalidArguments.make({
812
+ function: functionAddress,
813
+ message: `Invalid arguments for ${functionAddress}.`
814
+ }))),
815
+ definition,
816
+ schema: inspected.schema
817
+ };
818
+ }) });
819
+ }));
820
+ }
821
+ };
822
+ var MutationApplicationFailure = class extends Schema.TaggedError()("MutationApplicationFailure", { error: Schema.Json }) {};
823
+ var FunctionExecutor = class FunctionExecutor extends Context.Service()("ignotum/dev-runtime/functions/FunctionExecutor") {
824
+ static layer = Layer.effect(FunctionExecutor, Effect.gen(function* () {
825
+ const database = yield* LocalDatabase;
826
+ const mutationSemaphore = yield* Semaphore.make(1);
827
+ const encodeSuccess = (definition, value) => definition.returns === void 0 ? Schema.encodeUnknownEffect(Schema.Undefined)(value).pipe(Effect.orDie, Effect.as({ type: "Success" })) : Schema.encodeEffect(Schema.toCodecJson(definition.returns))(value).pipe(Effect.orDie, Effect.map((encoded) => {
828
+ const result = {
829
+ type: "Success",
830
+ value: encoded
831
+ };
832
+ const dates = datePathsOf(value);
833
+ return dates.length === 0 ? result : {
834
+ ...result,
835
+ dates
836
+ };
837
+ }));
838
+ const encodeFailure = (definition, error) => Schema.encodeEffect(Schema.toCodecJson(definition.errors ?? ErrorValueSchema))(error).pipe(Effect.orDie, Effect.map((encoded) => {
839
+ const result = {
840
+ type: "Failure",
841
+ error: encoded
842
+ };
843
+ const dates = datePathsOfObject(error);
844
+ return dates.length === 0 ? result : {
845
+ ...result,
846
+ dates
847
+ };
848
+ }));
849
+ return FunctionExecutor.of({ execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved) {
850
+ const invoke = (context) => Effect.gen(() => resolved.definition.handler(context, resolved.args)).pipe(Effect.matchEffect({
851
+ onFailure: (error) => encodeFailure(resolved.definition, error),
852
+ onSuccess: (value) => encodeSuccess(resolved.definition, value)
853
+ }));
854
+ return yield* (kind === "Query" ? database.queryTransaction(resolved.schema, invoke) : mutationSemaphore.withPermits(1)(database.mutationTransaction(resolved.schema, (context) => invoke(context).pipe(Effect.flatMap((result) => result.type === "Failure" ? Effect.fail(MutationApplicationFailure.make({ error: result.error })) : Effect.succeed(result)))).pipe(Effect.catchTags({ MutationApplicationFailure: ({ error }) => Effect.succeed({
855
+ type: "Failure",
856
+ error
857
+ }) })))).pipe(Effect.orDie, Effect.catchCauseIf((cause) => !Cause.hasInterruptsOnly(cause), (cause) => {
858
+ const requestId = nanoid();
859
+ return Effect.logError(`${functionAddress} failed during execution.`).pipe(Effect.annotateLogs({
860
+ cause,
861
+ function: functionAddress,
862
+ requestId
863
+ }), Effect.as({
864
+ type: "Failure",
865
+ error: {
866
+ _tag: "InternalServerError",
867
+ requestId
868
+ }
869
+ }));
870
+ }));
871
+ }) });
872
+ }));
873
+ };
874
+ /** Adapts the Vite/SQLite development implementation to the shared runtime contract. */
875
+ const functionRuntimeLayer = Layer.effect(FunctionRuntime, Effect.gen(function* () {
876
+ const registry = yield* FunctionRegistry;
877
+ const executor = yield* FunctionExecutor;
878
+ return FunctionRuntime.of({ prepare: Effect.fn("DevFunctionRuntime.prepare")(function* (functionAddress, kind, args) {
879
+ const resolved = yield* registry.resolve(functionAddress, kind, args);
880
+ return { execute: executor.execute(functionAddress, kind, resolved) };
881
+ }) });
882
+ }));
883
+ //#endregion
884
+ //#region src/dev-runtime/sync.ts
885
+ const queryInvalidationLayer = Layer.effect(QueryInvalidation, Effect.gen(function* () {
886
+ const pubsub = yield* PubSub.unbounded();
887
+ return QueryInvalidation.of({
888
+ publish: PubSub.publish(pubsub, void 0),
889
+ subscribe: PubSub.subscribe(pubsub)
890
+ });
891
+ }));
892
+ const operationForQuery = (subscriptionId) => ({
893
+ type: "Query",
894
+ id: subscriptionId
895
+ });
896
+ const protocolError = (code, message, operation) => {
897
+ if (operation === void 0) return {
898
+ type: "ProtocolError",
899
+ code,
900
+ message
901
+ };
902
+ return {
903
+ type: "ProtocolError",
904
+ code,
905
+ message,
906
+ operation
907
+ };
908
+ };
909
+ const runSession = Effect.fn("SyncServer.runSession")(function* (socket) {
910
+ const runtime = yield* FunctionRuntime;
911
+ const invalidation = yield* QueryInvalidation;
912
+ const subscriptions = yield* Ref.make(HashMap.empty());
913
+ const invocationFibers = yield* FiberSet.make();
914
+ const querySemaphore = yield* PartitionedSemaphore.make({ permits: 1 });
915
+ const messageSemaphore = yield* Semaphore.make(1);
916
+ const writeSemaphore = yield* Semaphore.make(1);
917
+ const write = yield* socket.writer;
918
+ const send = Effect.fn("SyncServer.send")(function* (message) {
919
+ yield* writeSemaphore.withPermits(1)(Schema.encodeEffect(ServerMessageJson)(message).pipe(Effect.flatMap(write)));
920
+ });
921
+ const sendResolutionError = (operation, error, deliver = send) => {
922
+ const code = error._tag;
923
+ return deliver(protocolError(code, error.message, operation));
924
+ };
925
+ const isActive = (subscriptionId, subscription) => Ref.get(subscriptions).pipe(Effect.map((current) => Option.getOrUndefined(HashMap.get(current, subscriptionId)) === subscription));
926
+ const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (subscriptionId, subscription, message) {
927
+ if (yield* isActive(subscriptionId, subscription)) yield* send(message);
928
+ });
929
+ const executeQuery = Effect.fn("SyncServer.executeQuery")(function* (subscriptionId, subscription, previouslyPrepared) {
930
+ if (!(yield* isActive(subscriptionId, subscription))) return;
931
+ const operation = operationForQuery(subscriptionId);
932
+ const deliver = (message) => sendIfActive(subscriptionId, subscription, message);
933
+ const prepared = previouslyPrepared ?? (yield* runtime.prepare(subscription.function, "Query", subscription.args).pipe(Effect.catchTags({
934
+ FunctionUnavailable: (error) => sendResolutionError(operation, error, deliver),
935
+ InvalidArguments: (error) => sendResolutionError(operation, error, deliver),
936
+ UnknownFunction: (error) => sendResolutionError(operation, error, deliver),
937
+ WrongFunctionKind: (error) => sendResolutionError(operation, error, deliver)
938
+ })));
939
+ if (prepared === void 0) return;
940
+ yield* deliver({
941
+ type: "QuerySnapshot",
942
+ id: subscriptionId,
943
+ result: yield* prepared.execute
944
+ });
945
+ });
946
+ const scheduleQuery = (subscriptionId, subscription, previouslyPrepared) => querySemaphore.withPermits(subscriptionId, 1)(executeQuery(subscriptionId, subscription, previouslyPrepared)).pipe(Effect.catchCause((cause) => Effect.logError("A query refresh fiber failed.").pipe(Effect.annotateLogs({
947
+ cause,
948
+ function: subscription.function,
949
+ subscriptionId
950
+ }))), FiberSet.run(invocationFibers), Effect.asVoid);
951
+ const refresh = Effect.fn("SyncServer.refresh")(function* () {
952
+ const current = yield* Ref.get(subscriptions);
953
+ yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) => scheduleQuery(subscriptionId, subscription));
954
+ });
955
+ const invalidations = yield* invalidation.subscribe;
956
+ yield* PubSub.take(invalidations).pipe(Effect.flatMap(refresh), Effect.forever, Effect.forkScoped);
957
+ const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (message) {
958
+ const operation = operationForQuery(message.id);
959
+ const current = yield* Ref.get(subscriptions);
960
+ if (HashMap.has(current, message.id)) {
961
+ yield* send(protocolError("DuplicateOperationId", `Subscription ${message.id} already exists.`, operation));
962
+ return;
963
+ }
964
+ const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(Effect.catchTags({
965
+ FunctionUnavailable: (error) => sendResolutionError(operation, error),
966
+ InvalidArguments: (error) => sendResolutionError(operation, error),
967
+ UnknownFunction: (error) => sendResolutionError(operation, error),
968
+ WrongFunctionKind: (error) => sendResolutionError(operation, error)
969
+ }));
970
+ if (prepared === void 0) return;
971
+ const subscription = {
972
+ args: message.args,
973
+ function: message.function
974
+ };
975
+ yield* Ref.update(subscriptions, HashMap.set(message.id, subscription));
976
+ yield* scheduleQuery(message.id, subscription, prepared);
977
+ });
978
+ const handleMutate = Effect.fn("SyncServer.handleMutate")(function* (message) {
979
+ const operation = {
980
+ type: "Mutate",
981
+ id: message.id
982
+ };
983
+ const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(Effect.catchTags({
984
+ FunctionUnavailable: (error) => sendResolutionError(operation, error),
985
+ InvalidArguments: (error) => sendResolutionError(operation, error),
986
+ UnknownFunction: (error) => sendResolutionError(operation, error),
987
+ WrongFunctionKind: (error) => sendResolutionError(operation, error)
988
+ }));
989
+ if (prepared === void 0) return;
990
+ yield* prepared.execute.pipe(Effect.flatMap((result) => {
991
+ const response = send({
992
+ type: "MutationResult",
993
+ id: message.id,
994
+ result
995
+ });
996
+ return result.type === "Success" ? response.pipe(Effect.ensuring(invalidation.publish)) : response;
997
+ }), Effect.catchCause((cause) => Effect.logError("A mutation invocation fiber failed.").pipe(Effect.annotateLogs({
998
+ cause,
999
+ function: message.function,
1000
+ requestId: message.id
1001
+ }))), FiberSet.run(invocationFibers), Effect.asVoid);
1002
+ });
1003
+ const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text) {
1004
+ const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(Effect.catch(() => send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON."))));
1005
+ if (message === void 0) return;
1006
+ switch (message.type) {
1007
+ case "Subscribe":
1008
+ yield* handleSubscribe(message);
1009
+ return;
1010
+ case "Unsubscribe":
1011
+ yield* Ref.update(subscriptions, HashMap.remove(message.id));
1012
+ return;
1013
+ case "Mutate":
1014
+ yield* handleMutate(message);
1015
+ return;
1016
+ }
1017
+ });
1018
+ yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)));
1019
+ });
1020
+ var SyncHandlers = class extends Context.Service()("ignotum/dev-runtime/sync/SyncHandlers") {};
1021
+ const makeHandlersLayer = (server, projectDirectory, databasePath) => {
1022
+ const databaseLayer = LocalDatabase.layer.pipe(Layer.provide(Layer.merge(idGeneratorLayer, SqliteClient.layer({ filename: databasePath }))));
1023
+ const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(databaseLayer));
1024
+ const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(Layer.provide(Layer.merge(FunctionRegistry.layer(server, projectDirectory).pipe(Layer.provide(NodeServices.layer)), executorLayer)));
1025
+ const dependencies = Layer.mergeAll(devFunctionRuntimeLayer, queryInvalidationLayer, NodeServices.layer);
1026
+ return Layer.effect(SyncHandlers, Effect.gen(function* () {
1027
+ const invalidation = yield* QueryInvalidation;
1028
+ const fileSystem = yield* FileSystem.FileSystem;
1029
+ const path = yield* Path.Path;
1030
+ const scope = yield* Effect.scope;
1031
+ const reloadSemaphore = yield* Semaphore.make(1);
1032
+ const webSocketServer = yield* Effect.acquireRelease(Effect.sync(() => new NodeSocket.NodeWS.WebSocketServer({
1033
+ maxPayload: 1048576,
1034
+ noServer: true
1035
+ })), (instance) => Effect.callback((resume) => {
1036
+ instance.close(() => resume(Effect.void));
1037
+ }));
1038
+ const httpApp = Effect.gen(function* () {
1039
+ const request = yield* HttpServerRequest.HttpServerRequest;
1040
+ const pathname = new URL(request.url, "http://ignotum.local").pathname;
1041
+ return yield* HttpServerResponse.json(pathname === syncPath ? {
1042
+ code: "UpgradeRequired",
1043
+ message: `Connect to ${syncPath} with WebSocket.`
1044
+ } : {
1045
+ code: "NotFound",
1046
+ message: "Unknown Ignotum API route."
1047
+ }, { status: pathname === syncPath ? 426 : 404 });
1048
+ });
1049
+ const socketApp = Effect.gen(function* () {
1050
+ const socket = yield* (yield* HttpServerRequest.HttpServerRequest).upgrade;
1051
+ yield* runSession(socket);
1052
+ return HttpServerResponse.empty();
1053
+ });
1054
+ const http = yield* NodeHttpServer.makeHandler(httpApp, { scope });
1055
+ const upgrade = yield* NodeHttpServer.makeUpgradeHandler(Effect.succeed(webSocketServer), socketApp, { scope });
1056
+ const serverDirectory = path.join(projectDirectory, "server");
1057
+ const reloadUnsafe = Effect.fn("SyncServer.reload")(function* (file, event) {
1058
+ const relative = path.relative(serverDirectory, file);
1059
+ if (relative.startsWith("..") || path.isAbsolute(relative) || !file.endsWith(".ts")) return;
1060
+ if (event !== "change") yield* generate(projectDirectory).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path));
1061
+ yield* invalidation.publish;
1062
+ });
1063
+ const reload = (file, event) => reloadSemaphore.withPermits(1)(reloadUnsafe(file, event)).pipe(Effect.catchCause((cause) => Effect.logError("Could not reload Ignotum server functions.").pipe(Effect.annotateLogs({
1064
+ cause,
1065
+ event,
1066
+ file
1067
+ }))));
1068
+ return SyncHandlers.of({
1069
+ http,
1070
+ reload,
1071
+ upgrade
1072
+ });
1073
+ })).pipe(Layer.provide(dependencies));
1074
+ };
1075
+ const isApiPath = (request) => {
1076
+ const pathname = new URL(request.url ?? "/", "http://ignotum.local").pathname;
1077
+ return isIgnotumPath(pathname);
1078
+ };
1079
+ const isAppOrigin = (host, origin) => {
1080
+ if (host === void 0 || origin === void 0) return false;
1081
+ return Option.match(Schema.decodeOption(Schema.URLFromString)(origin), {
1082
+ onNone: () => false,
1083
+ onSome: (url) => url.origin === origin && url.host === host
1084
+ });
1085
+ };
1086
+ const rejectUpgrade = (socket) => {
1087
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", () => socket.destroy());
1088
+ };
1089
+ const ignotumSyncPlugin = Function.dual(2, (projectDirectory, databasePath) => ({
1090
+ name: "ignotum:sync",
1091
+ configureServer: (server) => {
1092
+ if (server.httpServer === null) throw new Error("Ignotum sync requires Vite's Node HTTP server.");
1093
+ const runtime = ManagedRuntime.make(makeHandlersLayer(server, projectDirectory, databasePath));
1094
+ const httpServer = server.httpServer;
1095
+ return runtime.runPromise(SyncHandlers).then((handlers) => {
1096
+ const onUpgrade = (request, socket, head) => {
1097
+ if (new URL(request.url ?? "/", "http://ignotum.local").pathname !== syncPath) return;
1098
+ if (!isAppOrigin(request.headers.host, request.headers.origin)) {
1099
+ rejectUpgrade(socket);
1100
+ return;
1101
+ }
1102
+ handlers.upgrade(request, socket, head);
1103
+ };
1104
+ const onAdd = (file) => runtime.runFork(handlers.reload(file, "add"));
1105
+ const onChange = (file) => runtime.runFork(handlers.reload(file, "change"));
1106
+ const onUnlink = (file) => runtime.runFork(handlers.reload(file, "unlink"));
1107
+ const cleanup = () => {
1108
+ httpServer.off("upgrade", onUpgrade);
1109
+ server.watcher.off("add", onAdd);
1110
+ server.watcher.off("change", onChange);
1111
+ server.watcher.off("unlink", onUnlink);
1112
+ runtime.dispose();
1113
+ };
1114
+ httpServer.on("upgrade", onUpgrade);
1115
+ httpServer.once("close", cleanup);
1116
+ server.watcher.on("add", onAdd);
1117
+ server.watcher.on("change", onChange);
1118
+ server.watcher.on("unlink", onUnlink);
1119
+ server.middlewares.use((request, response, next) => {
1120
+ if (!isApiPath(request)) {
1121
+ next();
1122
+ return;
1123
+ }
1124
+ handlers.http(request, response);
1125
+ });
1126
+ }, (error) => {
1127
+ runtime.dispose();
1128
+ throw error;
1129
+ });
1130
+ }
1131
+ }));
1132
+ //#endregion
1133
+ //#region src/cli/dev.ts
1134
+ const requiredClientFiles = ["App.tsx", "styles.css"];
1135
+ const preactSpecifiers = [
1136
+ "preact/jsx-dev-runtime",
1137
+ "preact/jsx-runtime",
1138
+ "preact/devtools",
1139
+ "preact/debug",
1140
+ "preact/hooks",
1141
+ "preact"
1142
+ ];
1143
+ const prefreshSpecifiers = ["@prefresh/core", "@prefresh/utils"];
1144
+ var ClientFileNotFound = class extends Schema.TaggedError()("ClientFileNotFound", {
1145
+ message: Schema.String,
1146
+ path: Schema.String
1147
+ }) {};
1148
+ var ClientRuntimeResolutionFailed = class extends Schema.TaggedError()("ClientRuntimeResolutionFailed", {
1149
+ cause: Schema.Defect(),
1150
+ message: Schema.String
1151
+ }) {};
1152
+ var ViteStartupFailed = class extends Schema.TaggedError()("ViteStartupFailed", {
1153
+ cause: Schema.Defect(),
1154
+ message: Schema.String
1155
+ }) {};
1156
+ const validateClientFiles = Effect.fn("Dev.validateClientFiles")(function* (projectDirectory) {
1157
+ const fileSystem = yield* FileSystem.FileSystem;
1158
+ const path = yield* Path.Path;
1159
+ const clientDirectory = path.join(projectDirectory, "client");
1160
+ yield* Effect.forEach(requiredClientFiles, Effect.fn("Dev.validateClientFile")(function* (fileName) {
1161
+ const filePath = path.join(clientDirectory, fileName);
1162
+ if (!(yield* fileSystem.exists(filePath))) return yield* ClientFileNotFound.make({
1163
+ message: `Required client file not found: ${filePath}`,
1164
+ path: filePath
1165
+ });
1166
+ }), { discard: true });
1167
+ });
1168
+ const resolveClientAliases = Effect.fn("Dev.resolveClientAliases")(function* () {
1169
+ return yield* Effect.try({
1170
+ try: () => {
1171
+ const prefreshRequire = createRequire(import.meta.resolve("@prefresh/vite"));
1172
+ return [
1173
+ {
1174
+ find: /^tailwindcss$/,
1175
+ replacement: normalizePath(fileURLToPath(import.meta.resolve("tailwindcss/index.css")))
1176
+ },
1177
+ ...preactSpecifiers.map((specifier) => ({
1178
+ find: specifier,
1179
+ replacement: normalizePath(fileURLToPath(import.meta.resolve(specifier)))
1180
+ })),
1181
+ ...prefreshSpecifiers.map((specifier) => ({
1182
+ find: specifier,
1183
+ replacement: normalizePath(prefreshRequire.resolve(specifier))
1184
+ }))
1185
+ ];
1186
+ },
1187
+ catch: (cause) => ClientRuntimeResolutionFailed.make({
1188
+ cause,
1189
+ message: "Ignotum could not resolve its client runtime."
1190
+ })
1191
+ });
1192
+ });
1193
+ const acquireViteServer = Effect.fn("Dev.acquireViteServer")(function* (options) {
1194
+ const fileSystem = yield* FileSystem.FileSystem;
1195
+ const path = yield* Path.Path;
1196
+ const aliases = yield* resolveClientAliases();
1197
+ const clientDirectory = path.join(options.projectDirectory, "client");
1198
+ const databasePath = yield* devDatabasePath(options.projectDirectory);
1199
+ const stateDirectory = path.dirname(databasePath);
1200
+ const sourceClientEntry = fileURLToPath(new URL("../../src/client/index.ts", import.meta.url));
1201
+ const sourceContractsEntry = fileURLToPath(new URL("../../../contracts/src/schema/index.ts", import.meta.url));
1202
+ const hasSourceClient = yield* fileSystem.exists(sourceClientEntry);
1203
+ const hasSourceContracts = yield* fileSystem.exists(sourceContractsEntry);
1204
+ const hasWorkspaceSourceExports = hasSourceClient && hasSourceContracts;
1205
+ yield* fileSystem.makeDirectory(stateDirectory, { recursive: true });
1206
+ return yield* Effect.acquireRelease(Effect.tryPromise({
1207
+ try: () => prefresh().then((prefreshPlugin) => createServer({
1208
+ appType: "spa",
1209
+ configFile: false,
1210
+ mode: "development",
1211
+ root: clientDirectory,
1212
+ plugins: [
1213
+ ignotumSyncPlugin(options.projectDirectory, databasePath),
1214
+ ignotumClientPlugin(),
1215
+ prefreshPlugin,
1216
+ tailwindcss()
1217
+ ],
1218
+ oxc: { jsx: {
1219
+ importSource: "ignotum/client",
1220
+ runtime: "automatic"
1221
+ } },
1222
+ resolve: {
1223
+ alias: aliases,
1224
+ conditions: [...hasWorkspaceSourceExports ? ["@ignotum/source"] : [], ...defaultClientConditions],
1225
+ dedupe: ["preact"],
1226
+ tsconfigPaths: true
1227
+ },
1228
+ server: {
1229
+ forwardConsole: {
1230
+ logLevels: ["warn", "error"],
1231
+ unhandledErrors: true
1232
+ },
1233
+ host: options.host,
1234
+ open: options.open,
1235
+ port: options.port,
1236
+ strictPort: true
1237
+ }
1238
+ })),
1239
+ catch: (cause) => ViteStartupFailed.make({
1240
+ cause,
1241
+ message: "Ignotum could not create the Vite development server."
1242
+ })
1243
+ }), (server) => Effect.promise(() => server.close()));
1244
+ });
1245
+ const listen = Effect.fn("Dev.listen")(function* (server) {
1246
+ yield* Effect.tryPromise({
1247
+ try: () => server.listen(),
1248
+ catch: (cause) => ViteStartupFailed.make({
1249
+ cause,
1250
+ message: "Ignotum could not start the Vite development server."
1251
+ })
1252
+ });
1253
+ });
1254
+ const dev$1 = Effect.fn("Dev.run")(function* (options) {
1255
+ yield* validateClientFiles(options.projectDirectory);
1256
+ yield* generate(options.projectDirectory);
1257
+ return yield* Effect.scoped(Effect.gen(function* () {
1258
+ yield* acquireDevDatabaseLock(options.projectDirectory);
1259
+ const server = yield* acquireViteServer(options);
1260
+ yield* listen(server);
1261
+ yield* Effect.sync(() => {
1262
+ server.printUrls();
1263
+ server.bindCLIShortcuts({ print: true });
1264
+ });
1265
+ return yield* Effect.never;
1266
+ }));
1267
+ });
1268
+ const agentProjectFiles = [
1269
+ {
1270
+ content: "# AGENTS.md\n\nThis is an Ignotum app. Before changing it, read `.agents/skills/ignotum/SKILL.md`.\n\nThe skill explains how to inspect the project, which bundled reference to read, and how to check\nyour changes.\n",
1271
+ path: "AGENTS.md"
1272
+ },
1273
+ {
1274
+ content: "---\nname: ignotum\ndescription: Build and modify an Ignotum app. Use when working on its schema, server functions, generated API, client UI, or local development workflow.\n---\n\n# Working on an Ignotum app\n\nAn Ignotum app defines a database schema, server functions, and a JSX client. Ignotum supplies the\ndatabase runtime, generated bindings, realtime query updates, Vite dev server, JSX runtime, and\nTailwind setup.\n\nThe current release supports local development. Do not assume that authentication, files, actions,\nworkflows, scheduled jobs, or production deployment APIs exist.\n\n## Start with the app\n\nInspect `server/schema.ts`, the function modules in `server`, the files in `client`, `package.json`,\nand `tsconfig.json` before changing code.\n\nTreat `_generated` as compiler output. Read it when you need to understand an inferred type, but do\nnot edit it. After code changes, regenerate the bindings and run the typechecker:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nFor normal Ignotum app work, do not add an HTML file, Vite configuration, Tailwind configuration,\nAPI routes, or direct database setup.\n\n## Read the relevant reference\n\nThe bundled references are the Ignotum user guides. Read only the guides needed for the task:\n\n- For project creation and installation, read [getting started](references/getting-started.md) or\n [manual setup](references/manual-setup.md).\n- For tables, fields, IDs, and generated document types, read\n [schema syntax](references/schema.md).\n- For validators and their TypeScript types, read [values](references/values.md).\n- For queries, mutations, database access, Results, and application errors, read\n [server functions](references/server-functions.md).\n- For hooks, query state, mutations, JSX, and Tailwind, read [client](references/client.md).\n- For development commands, code generation, flags, and local data, read\n [dev server](references/dev-server.md).\n- For the complete guide list, read the [documentation index](references/index.md).\n\n## Rules that cross guide boundaries\n\n- Define tables in `server/schema.ts`.\n- Put queries and mutations in TypeScript files directly inside `server`.\n- Import server builders from `@/_generated/server.js` and client references from\n `@/_generated/api.js`. Keep the `.js` suffix.\n- Put code used by both the client and server in `shared` and import it through `@/shared`.\n- Never edit `_generated`.\n- Use JSX and hooks from `ignotum/client`, not React or Preact packages directly.\n- Keep the app inside Ignotum's current model unless the user explicitly asks to move beyond it.\n",
1275
+ path: ".agents/skills/ignotum/SKILL.md"
1276
+ },
1277
+ ...[
1278
+ ["client.md", "# Client\n\nIgnotum apps use JSX, hooks from `ignotum/client`, and Tailwind CSS. Import server function\nreferences from `@/_generated/api.js`:\n\n```tsx\nimport { Query, Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\n```\n\n## Tailwind CSS\n\nEvery app has `client/styles.css` with the Tailwind import:\n\n```css\n@import \"tailwindcss\";\n```\n\nStyle JSX with Tailwind utility classes. Use the JSX `class` attribute:\n\n```tsx\nexport default function App() {\n return (\n <main class=\"mx-auto max-w-xl px-6 py-16\">\n <h1 class=\"text-2xl font-semibold text-zinc-950\">Todos</h1>\n </main>\n );\n}\n```\n\nThe dev server loads `styles.css` and configures Tailwind. The app does not need a Tailwind\nconfiguration file.\n\n## Run a query\n\n`useQuery` takes a generated query reference. Pass the typed arguments when the query declares\nthem:\n\n```tsx\nconst todos = useQuery(api.todos.list);\nconst todo = useQuery(api.todos.get, { id });\nconst selectedTodo = useQuery(api.todos.get, id === undefined ? Query.skip : { id });\n```\n\n`Query.skip` keeps a query pending without opening a subscription. Use it when the arguments are\nnot available yet.\n\nThe first value is pending. Match every state with `Result.match`:\n\n```tsx\nreturn Result.match(todos, {\n pending: () => <p class=\"text-zinc-500\">Loading...</p>,\n value: (items) => (\n <ul>\n {items.map((todo) => (\n <li key={todo.id}>{todo.text}</li>\n ))}\n </ul>\n ),\n});\n```\n\nIgnotum keeps an active query up to date. A successful mutation refreshes subscribed queries in\nevery open client.\n\n## Run a mutation\n\n`useMutation` takes a generated mutation reference and returns a function:\n\n```tsx\nconst createTodo = useMutation(api.todos.create);\n\nvoid createTodo({ text }).then(\n Result.match({\n value: (id) => console.log(id),\n error: {\n InvalidTodoText: ({ text }) => console.log(`Invalid text: ${text}`),\n TodoLimitReached: ({ limit }) => console.log(`The limit is ${limit}`),\n },\n internalError: ({ requestId }) => console.log(`Request ${requestId} failed.`),\n }),\n);\n```\n\nAn argument-free mutation returns a zero-argument function:\n\n```tsx\nconst clearTodos = useMutation(api.todos.clear);\nvoid clearTodos();\n```\n\nThe client `Result` API only inspects completed server responses. Server-only operations such as\n`Result.fail`, `Result.succeed`, `Result.try`, `yield*`, and `.catch()` are not available here.\n\nApplication errors are handled by `error`, either with one function or an exhaustive map keyed by\n`_tag`. An `InternalServerError` contains a request ID and is thrown when `Result.match` has no\n`internalError` handler. During rendering, it reaches the nearest UI error boundary. A mutation can\nhandle it locally with `internalError`, as above. Connection and protocol failures reject mutation\npromises; query connection failures go to the nearest UI error boundary.\n"],
1279
+ ["dev-server.md", "# Dev server\n\nRun the dev server from the project root:\n\n```sh\nnpx ignotum dev\n```\n\nIt expects these files:\n\n```text\nclient/App.tsx\nclient/styles.css\nserver/schema.ts\n```\n\nThe dev server generates the client and server bindings, mounts the default export from\n`client/App.tsx`, loads Tailwind from `client/styles.css`, and serves the app at\n<http://127.0.0.1:3210>. It reloads client and server changes and updates active queries after\nserver changes and successful mutations.\n\nYou do not need an HTML file, Vite configuration, or Tailwind configuration.\n\n## Flags\n\nUse flags to change the address or open the browser:\n\n```sh\nnpx ignotum dev --host 0.0.0.0 --port 3000 --open\n```\n\nThe defaults are host `127.0.0.1`, port `3210`, and no automatic browser opening.\n\n## Code generation\n\nThe dev server runs code generation when it starts. It updates generated files when you add or\nremove a server function file.\n\nRun code generation before typechecking without the dev server:\n\n```sh\nnpx ignotum codegen\nnpx tsc --noEmit\n```\n\nIgnotum creates:\n\n- `_generated/server.ts` with schema-bound `query`, `mutation`, and `values` exports;\n- `_generated/api.ts` with client references such as `api.todos.list`;\n- `_generated/types.ts` with `DataModel`, `Doc`, and `Id`.\n\nDo not edit generated files.\n\n## Local data\n\nData persists between dev-server restarts. Stop the server and reset that data with:\n\n```sh\nnpx ignotum dev db reset\n```\n"],
1280
+ ["getting-started.md", "# Getting started\n\nIgnotum requires Node.js 22.18 or newer.\n\nCreate an app:\n\n```sh\nnpx ignotum new my-app\ncd my-app\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. The generated app is a small counter with a schema, a query, a\nmutation, and a JSX client.\n\n`ignotum new` installs dependencies with pnpm when it is available. It falls back to npm only when\npnpm is not installed. It generates `_generated`, initializes a Git repository, and creates an\n`Init` commit containing the generated files after the rest of the setup finishes.\n\nPass `.` to create the app in the current directory. The directory must be empty:\n\n```sh\nnpx ignotum new .\n```\n\nUse `--no-git` to skip Git or `--no-install` to skip dependency installation. You can install the\ndependencies later with the same pnpm and npm fallback behavior:\n\n```sh\nnpx ignotum install\n```\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to build the app. The [manual setup](manual-setup.md) recreates the counter\napp without `ignotum new`.\n"],
1281
+ ["index.md", "# Ignotum\n\nAn Ignotum app has a schema, server functions, and a client. Ignotum is opinionated about the\nclient tooling. Every app uses JSX, hooks from `ignotum/client`, and Tailwind CSS.\n\nThe current release supports local development.\n\n- [Getting started](getting-started.md) creates and runs a counter app with `ignotum new`.\n- [Manual setup](manual-setup.md) recreates the generated counter app by hand.\n- [Schema syntax](schema.md) covers tables, fields, IDs, and generated document types.\n- [Values](values.md) lists every value validator and its TypeScript type.\n- [Server functions](server-functions.md) covers queries, mutations, database access, and errors.\n- [Client](client.md) covers queries, mutations, results, and Tailwind styling.\n- [Dev server](dev-server.md) covers local development, code generation, flags, and data reset.\n"],
1282
+ ["manual-setup.md", "# Manual setup\n\nIgnotum requires Node.js 22.18 or newer. This guide recreates the counter app from `ignotum new`\nwithout running the project generator. It uses pnpm to install dependencies.\n\nCreate the project directory:\n\n```sh\nmkdir my-ignotum-app\ncd my-ignotum-app\n```\n\nCreate this structure:\n\n```text\nmy-ignotum-app/\n client/\n App.tsx\n styles.css\n server/\n counter.ts\n schema.ts\n shared/\n utils.ts\n package.json\n tsconfig.json\n```\n\n## Configure the package\n\nAdd `package.json`:\n\n```json\n{\n \"name\": \"my-ignotum-app\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"typecheck\": \"ignotum codegen && tsc --noEmit\"\n },\n \"dependencies\": {\n \"ignotum\": \"latest\"\n },\n \"devDependencies\": {\n \"typescript\": \"^7.0.2\"\n },\n \"engines\": {\n \"node\": \">=22.18.0\"\n }\n}\n```\n\nInstall the dependencies:\n\n```sh\npnpm install\n```\n\n## Configure TypeScript\n\nAdd `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2023\",\n \"lib\": [\"ES2023\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"ignotum/client\",\n \"strict\": true,\n \"noEmit\": true,\n \"skipLibCheck\": true,\n \"paths\": {\n \"@/*\": [\"./*\"]\n }\n },\n \"include\": [\"_generated\", \"client\", \"server\", \"shared\"]\n}\n```\n\n## Add shared code\n\nAdd `shared/utils.ts`:\n\n```ts\nexport const counterIncrement = 1;\n```\n\nBoth the client and server can import files in `shared` through `@/shared`.\n\n## Define the schema\n\nAdd `server/schema.ts`:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n counters: table({\n value: values.number(),\n }),\n}));\n```\n\n## Add the server functions\n\nAdd `server/counter.ts`:\n\n```ts\nimport { mutation, query, values } from \"@/_generated/server.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport const get = query({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n return counters[0]?.value ?? 0;\n },\n});\n\nexport const increment = mutation({\n returns: values.number(),\n\n handler: function* (ctx) {\n const counters = yield* ctx.db.query(\"counters\").collect();\n const counter = counters[0];\n const value = (counter?.value ?? 0) + counterIncrement;\n\n if (counter === undefined) {\n yield* ctx.db.insert(\"counters\", { value });\n } else {\n yield* ctx.db.patch(\"counters\", counter.id, { value });\n }\n\n return value;\n },\n});\n```\n\nKeep the `.js` suffix on imports from `@/_generated`, even though the generated files use\nTypeScript.\n\n## Add the client\n\nAdd `client/styles.css`:\n\n```css\n@import \"tailwindcss\";\n```\n\nAdd `client/App.tsx`:\n\n```tsx\nimport { Result, useMutation, useQuery } from \"ignotum/client\";\n\nimport { api } from \"@/_generated/api.js\";\nimport { counterIncrement } from \"@/shared/utils.js\";\n\nexport default function App() {\n const count = useQuery(api.counter.get);\n const increment = useMutation(api.counter.increment);\n\n return (\n <main class=\"mx-auto max-w-sm px-6 py-20 text-center\">\n <h1 class=\"text-2xl font-semibold\">Counter</h1>\n {Result.match(count, {\n pending: () => <p class=\"mt-6\">Loading...</p>,\n value: (value) => (\n <>\n <p class=\"my-6 text-5xl tabular-nums\">{value}</p>\n <button\n class=\"rounded bg-zinc-900 px-4 py-2 text-white\"\n type=\"button\"\n onClick={() => void increment()}\n >\n Increment by {counterIncrement}\n </button>\n </>\n ),\n })}\n </main>\n );\n}\n```\n\nIgnotum loads Tailwind from `client/styles.css`. You do not need an HTML file, Vite configuration,\nor Tailwind configuration.\n\n## Run the app\n\nStart the dev server:\n\n```sh\nnpx ignotum dev\n```\n\nOpen <http://127.0.0.1:3210>. Ignotum creates `_generated` before starting the app.\n\nRead [schema syntax](schema.md), [server functions](server-functions.md), and the\n[client guide](client.md) to continue building the app.\n"],
1283
+ ["schema.md", "# Schema syntax\n\nDefine the data model in `server/schema.ts`. The keys returned from `defineSchema` are table\nnames, and each `table` call defines that table's fields:\n\n```ts\nimport { defineSchema } from \"ignotum/server\";\n\nexport default defineSchema(({ table, values }) => ({\n users: table({\n name: values.string(),\n }),\n todos: table({\n text: values.string(),\n completed: values.boolean(),\n ownerId: values.optional(values.id(\"users\")),\n }),\n}));\n```\n\n## Field values\n\nRead [values](values.md) for the complete validator list and the TypeScript type produced by each\none. `values.id` only accepts a table declared in the same schema. Arrays and objects can be nested,\nand their contents can use any value validator.\n\n## System fields\n\nIgnotum adds three fields to every stored document:\n\n| Field | Type |\n| ----------- | ------------------------- |\n| `id` | The ID type for its table |\n| `createdAt` | `Date` |\n| `updatedAt` | `Date` |\n\nDo not declare these fields in a table. Do not pass them to `insert`, `patch`, or `replace`.\n\n## Generated types\n\n`_generated/types.ts` exports the data model, document, and ID types:\n\n```ts\nimport type { DataModel, Doc, Id } from \"@/_generated/types.js\";\n\ntype Todo = Doc<\"todos\">;\ntype TodoId = Id<\"todos\">;\n```\n\n`Doc<\"todos\">` includes the fields from the `todos` table and its three system fields. An\n`Id<\"todos\">` cannot be passed where an `Id<\"users\">` is required.\n"],
1284
+ ["server-functions.md", "# Server functions\n\nPut queries and mutations in `.ts` files directly inside `server`. The file name becomes the API\nmodule, and each exported function keeps its export name:\n\n```text\nserver/todos.ts -> api.todos.list\nserver/users.ts -> api.users.get\n```\n\nIgnotum ignores `schema.ts`, `index.ts`, test files, and names beginning with `_`.\n\nImport schema-bound builders from the generated server file. Import `Result` only when the module\nintroduces or catches typed errors:\n\n```ts\nimport { Result } from \"ignotum/server\";\n\nimport { mutation, query, values } from \"@/_generated/server.js\";\n```\n\nKeep the `.js` suffix on generated imports.\n\n## Define a function\n\nA function has optional argument, return, and public error schemas, plus a generator handler:\n\n```ts\nexport const getTitle = query({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.string(),\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id);\n return todo.text;\n },\n});\n```\n\nOmit `args` when the function takes no arguments. Omit `returns` when it returns nothing. An\nomitted `returns` only permits a `void` handler; Ignotum never infers an unchecked return schema.\n`yield*` waits for an Ignotum operation and propagates its typed application errors. Return\nsuccessful values with ordinary `return`.\n\nIgnotum validates arguments before running the handler. It also validates returned values and\npublic application errors before sending them to a client.\n\nAn argument-free query and a mutation with no return value can stay small:\n\n```ts\nexport const list = query({\n returns: values.array(Todo),\n\n handler: function* (ctx) {\n return yield* ctx.db.query(\"todos\").collect();\n },\n});\n\nexport const remove = mutation({\n args: { id: values.id(\"todos\") },\n\n handler: function* (ctx, args) {\n yield* ctx.db.delete(\"todos\", args.id);\n },\n});\n```\n\nCall an argument-free query as `useQuery(api.todos.list)`. An argument-free mutation returns a\nzero-argument function:\n\n```ts\nconst clear = useMutation(api.todos.clear);\nvoid clear();\n```\n\n## Database reads\n\nUse `find` when a missing document is a normal result:\n\n```ts\nconst todo = yield * ctx.db.find(\"todos\", args.id);\n// Todo | undefined\n```\n\nUse `get` when the document should exist:\n\n```ts\nconst todo = yield * ctx.db.get(\"todos\", args.id);\n// Todo\n```\n\nA missing `get` fails with a typed `DocumentNotFound` value containing `table` and `id`. Collect a\nwhole table through a query:\n\n```ts\nconst todos = yield * ctx.db.query(\"todos\").collect();\n```\n\nQuery handlers only receive read methods.\n\n## Database writes\n\nMutation handlers receive the read methods and these writes:\n\n```ts\nconst id =\n yield *\n ctx.db.insert(\"todos\", {\n text: \"Learn Ignotum\",\n completed: false,\n });\n\nyield * ctx.db.patch(\"todos\", id, { completed: true });\n\nyield *\n ctx.db.replace(\"todos\", id, {\n text: \"Build an app\",\n completed: false,\n });\n\nyield * ctx.db.delete(\"todos\", id);\n```\n\n`patch` changes only supplied fields. `replace` requires every non-optional table field. Ignotum\nrolls back a mutation when its handler fails with a typed application error or encounters an\ninternal failure.\n\n## Application errors\n\nDefine an application error with `values.error`. Its name becomes `_tag`:\n\n```ts\nconst TodoNotFound = values.error(\"TodoNotFound\", {\n id: values.id(\"todos\"),\n});\n```\n\n`Result.fail` deliberately stops the operation with a typed error:\n\n```ts\nyield * Result.fail(TodoNotFound({ id: args.id }));\n```\n\nThe `errors` field is optional. If omitted, Ignotum infers the handler's remaining application\nerrors. If supplied, it is the public contract and the handler must conform to it:\n\n```ts\nexport const toggle = mutation({\n args: {\n id: values.id(\"todos\"),\n },\n returns: values.boolean(),\n errors: TodoNotFound,\n\n handler: function* (ctx, args) {\n const todo = yield* ctx.db.get(\"todos\", args.id).catch({\n DocumentNotFound: (error) => Result.fail(TodoNotFound({ id: error.id })),\n });\n\n const completed = !todo.completed;\n yield* ctx.db.patch(\"todos\", args.id, { completed });\n return completed;\n },\n});\n```\n\nCombine public errors with `values.union`:\n\n```ts\nerrors: values.union(InvalidTodoText, TodoLimitReached),\n```\n\n## Catch and recover\n\nEvery Result operation has a partial, tag-based `catch`. Handlers receive the narrowed error type.\nUnmatched errors continue through the channel:\n\n```ts\nconst settings =\n yield *\n loadSettings().catch({\n SettingsNotFound: () => defaultSettings,\n });\n```\n\nReturn a plain value to recover. Return `Result.fail(...)` to map one error to another. Unknown tag\nnames fail the TypeScript check.\n\nUse `Result.try` for one catch boundary around several operations:\n\n```ts\nconst author =\n yield *\n Result.try(function* () {\n const membership = yield* ctx.db.get(\"memberships\", topic.membershipId);\n return yield* ctx.db.get(\"users\", membership.userId);\n }).catch({\n DocumentNotFound: () => Result.fail(InvalidAuthor({ topicId: topic.id })),\n });\n```\n\nThere is no async variant. Ignotum operations always use `yield*` in server code.\n\n## Standalone results\n\n`Result.succeed` remains useful for helpers that return a Result:\n\n```ts\nfunction validateName(name: string) {\n if (name.length === 0) {\n return Result.fail(InvalidName({}));\n }\n\n return Result.succeed(name.trim());\n}\n```\n\nA handler can use the helper with `const name = yield* validateName(args.name)`. Normal handlers do\nnot wrap successful returns in `Result.succeed`.\n\n## Internal failures and defects\n\nDatabase outages, internal runtime failures, and thrown JavaScript exceptions are not application\nerrors. Ignotum logs their full cause and sends only:\n\n```ts\n{\n _tag: \"InternalServerError\",\n requestId: \"...\",\n}\n```\n\nEvery generated client function includes `InternalServerError` in its Result error union. The\nrequest ID links the client-visible failure to server logs without exposing private details.\n`InternalServerError` is reserved by Ignotum: never define it with `values.error` or include it in a\nfunction's `errors` schema.\n"],
1285
+ ["values.md", "# Values\n\nUse `values` validators to describe table fields, function arguments, return values, and application\nerrors. Each validator checks values at runtime and supplies the matching TypeScript type.\n\n| Validator | TypeScript type | Notes |\n| ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `values.string()` | `string` | |\n| `values.number()` | `number` | Accepts finite JavaScript numbers, including integers. |\n| `values.integer()` | `number` | Accepts safe integers. |\n| `values.boolean()` | `boolean` | |\n| `values.date()` | `Date` | Accepts valid JavaScript dates. |\n| `values.null()` | `null` | Accepts only `null`. |\n| `values.literal(value)` | The exact type of `value` | Accepts one string, finite number, or boolean value. |\n| `values.literals(first, second, ...rest)` | A union of the supplied literal types | Requires at least two string, finite number, or boolean values. |\n| `values.id(\"todos\")` | `Id<\"todos\">` | Accepts an ID for a table declared in the same schema. IDs for different tables are different types. |\n| `values.optional(value)` | `T \\| undefined` | Makes an object or table field optional. The field may be omitted. |\n| `values.nullable(value)` | `T \\| null` | The value remains required unless it is also wrapped with `optional`. |\n| `values.array(value)` | `ReadonlyArray<T>` | Every item must match `value`. |\n| `values.object(fields)` | An object matching `fields` | Defines an object with known field names. |\n| `values.record(value)` | `Readonly<Record<string, T>>` | Defines an object with dynamic string keys whose values all match `value`. |\n| `values.union(...values)` | A union of the supplied types | Accepts a value matching any supplied validator. |\n| `values.never()` | `never` | No value can pass this validator. |\n| `values.error(\"Name\", fields)` | A tagged error object | Defines an application error whose `_tag` is the supplied name. |\n\n`T` means the TypeScript type produced by the wrapped validator.\n\n## Examples\n\n```ts\nconst TodoStatus = values.literals(\"pending\", \"completed\");\n\nconst Todo = values.object({\n status: TodoStatus,\n scheduledAt: values.nullable(values.date()),\n scores: values.record(values.integer()),\n title: values.string(),\n});\n```\n\nUse `optional` when a field may be absent. Use `nullable` when a present field may contain `null`:\n\n```ts\nvalues.object({\n nickname: values.optional(values.string()),\n deletedAt: values.nullable(values.date()),\n});\n```\n"]
1286
+ ].map(([name, content]) => ({
1287
+ content,
1288
+ path: `.agents/skills/ignotum/references/${name}`
1289
+ }))
1290
+ ];
1291
+ //#endregion
1292
+ //#region src/cli/package-manager.ts
1293
+ var DependencyInstallationFailed = class extends Schema.TaggedError()("DependencyInstallationFailed", {
1294
+ cause: Schema.optional(Schema.Defect()),
1295
+ message: Schema.String,
1296
+ path: Schema.String
1297
+ }) {};
1298
+ const installWith = Effect.fn("PackageManager.installWith")(function* (projectDirectory, packageManager) {
1299
+ const exitCode = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).exitCode(ChildProcess.make(packageManager, ["install"], {
1300
+ cwd: projectDirectory,
1301
+ stderr: "inherit",
1302
+ stdin: "inherit",
1303
+ stdout: "inherit"
1304
+ }));
1305
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) return yield* DependencyInstallationFailed.make({
1306
+ message: `${packageManager} install exited with code ${exitCode} in ${projectDirectory}.`,
1307
+ path: projectDirectory
1308
+ });
1309
+ return packageManager;
1310
+ });
1311
+ const installDependencies = Effect.fn("PackageManager.installDependencies")(function* (projectDirectory) {
1312
+ return yield* installWith(projectDirectory, "pnpm").pipe(Effect.catchReasons("PlatformError", { NotFound: () => installWith(projectDirectory, "npm") }), Effect.catchTags({ PlatformError: (cause) => DependencyInstallationFailed.make({
1313
+ cause,
1314
+ message: `Could not run pnpm or npm in ${projectDirectory}.`,
1315
+ path: projectDirectory
1316
+ }) }));
1317
+ });
1318
+ //#endregion
1319
+ //#region src/cli/new-project.ts
1320
+ var ProjectDirectoryNotEmpty = class extends Schema.TaggedError()("ProjectDirectoryNotEmpty", {
1321
+ message: Schema.String,
1322
+ path: Schema.String
1323
+ }) {};
1324
+ var InvalidProjectName = class extends Schema.TaggedError()("InvalidProjectName", {
1325
+ message: Schema.String,
1326
+ path: Schema.String
1327
+ }) {};
1328
+ var GitInitializationFailed = class extends Schema.TaggedError()("GitInitializationFailed", {
1329
+ cause: Schema.optional(Schema.Defect()),
1330
+ message: Schema.String,
1331
+ path: Schema.String
1332
+ }) {};
1333
+ const packageJson = (projectName) => `${JSON.stringify({
1334
+ name: projectName,
1335
+ private: true,
1336
+ version: "0.0.0",
1337
+ type: "module",
1338
+ scripts: { typecheck: "ignotum codegen && tsc --noEmit" },
1339
+ dependencies: { ignotum: "latest" },
1340
+ devDependencies: { typescript: "^7.0.2" },
1341
+ engines: { node: ">=22.18.0" }
1342
+ }, null, 2)}\n`;
1343
+ const tsconfig = `{
1344
+ "compilerOptions": {
1345
+ "target": "ES2023",
1346
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
1347
+ "module": "NodeNext",
1348
+ "moduleResolution": "NodeNext",
1349
+ "jsx": "react-jsx",
1350
+ "jsxImportSource": "ignotum/client",
1351
+ "strict": true,
1352
+ "noEmit": true,
1353
+ "skipLibCheck": true,
1354
+ "paths": {
1355
+ "@/*": ["./*"]
1356
+ }
1357
+ },
1358
+ "include": ["_generated", "client", "server", "shared"]
1359
+ }
1360
+ `;
1361
+ const gitignore = `node_modules/
1362
+ .ignotum/
1363
+
1364
+ .env
1365
+ .env.*
1366
+ !.env.example
1367
+
1368
+ .DS_Store
1369
+ Thumbs.db
1370
+ `;
1371
+ const schema = `import { defineSchema } from "ignotum/server";
1372
+
1373
+ export default defineSchema(({ table, values }) => ({
1374
+ counters: table({
1375
+ value: values.number(),
1376
+ }),
1377
+ }));
1378
+ `;
1379
+ const counterFunctions = `import { mutation, query, values } from "@/_generated/server.js";
1380
+ import { counterIncrement } from "@/shared/utils.js";
1381
+
1382
+ export const get = query({
1383
+ returns: values.number(),
1384
+
1385
+ handler: function* (ctx) {
1386
+ const counters = yield* ctx.db.query("counters").collect();
1387
+ return counters[0]?.value ?? 0;
1388
+ },
1389
+ });
1390
+
1391
+ export const increment = mutation({
1392
+ returns: values.number(),
1393
+
1394
+ handler: function* (ctx) {
1395
+ const counters = yield* ctx.db.query("counters").collect();
1396
+ const counter = counters[0];
1397
+ const value = (counter?.value ?? 0) + counterIncrement;
1398
+
1399
+ if (counter === undefined) {
1400
+ yield* ctx.db.insert("counters", { value });
1401
+ } else {
1402
+ yield* ctx.db.patch("counters", counter.id, { value });
1403
+ }
1404
+
1405
+ return value;
1406
+ },
1407
+ });
1408
+ `;
1409
+ const app = `import { Result, useMutation, useQuery } from "ignotum/client";
1410
+
1411
+ import { api } from "@/_generated/api.js";
1412
+ import { counterIncrement } from "@/shared/utils.js";
1413
+
1414
+ export default function App() {
1415
+ const count = useQuery(api.counter.get);
1416
+ const increment = useMutation(api.counter.increment);
1417
+
1418
+ return (
1419
+ <main class="mx-auto max-w-sm px-6 py-20 text-center">
1420
+ <h1 class="text-2xl font-semibold">Counter</h1>
1421
+ {Result.match(count, {
1422
+ pending: () => <p class="mt-6">Loading...</p>,
1423
+ value: (value) => (
1424
+ <>
1425
+ <p class="my-6 text-5xl tabular-nums">{value}</p>
1426
+ <button
1427
+ class="rounded bg-zinc-900 px-4 py-2 text-white"
1428
+ type="button"
1429
+ onClick={() => void increment()}
1430
+ >
1431
+ Increment by {counterIncrement}
1432
+ </button>
1433
+ </>
1434
+ ),
1435
+ })}
1436
+ </main>
1437
+ );
1438
+ }
1439
+ `;
1440
+ const utils = `export const counterIncrement = 1;
1441
+ `;
1442
+ const styles = `@import "tailwindcss";
1443
+ `;
1444
+ const readme = (projectName) => `# ${projectName}
1445
+
1446
+ A small Ignotum counter app.
1447
+
1448
+ ## Getting started
1449
+
1450
+ You need Node.js 22.18 or newer. The project generator installs dependencies by default. If you
1451
+ created the project with \`--no-install\`, install them now:
1452
+
1453
+ \`\`\`sh
1454
+ npx ignotum install
1455
+ \`\`\`
1456
+
1457
+ This command uses pnpm when it is installed and otherwise uses npm.
1458
+
1459
+ Start the app:
1460
+
1461
+ \`\`\`sh
1462
+ npx ignotum dev
1463
+ \`\`\`
1464
+
1465
+ Open <http://127.0.0.1:3210>.
1466
+
1467
+ The project generator creates \`_generated\`. The dev server checks those files before it starts
1468
+ and updates them when the schema or server functions change.
1469
+
1470
+ ## Project files
1471
+
1472
+ - \`server/schema.ts\` defines the database tables.
1473
+ - \`server/counter.ts\` defines the query and mutation used by the counter.
1474
+ - \`client/App.tsx\` is the UI.
1475
+ - \`client/styles.css\` loads Tailwind CSS.
1476
+ - \`shared/utils.ts\` contains code shared across the app.
1477
+ - \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
1478
+
1479
+ Run the typechecker after a change:
1480
+
1481
+ \`\`\`sh
1482
+ npx ignotum codegen
1483
+ npx tsc --noEmit
1484
+ \`\`\`
1485
+
1486
+ ## Claude Code
1487
+
1488
+ If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
1489
+ can find the project instructions and Ignotum skill.
1490
+ `;
1491
+ const projectFiles = (projectName) => [
1492
+ {
1493
+ content: app,
1494
+ path: "client/App.tsx"
1495
+ },
1496
+ {
1497
+ content: styles,
1498
+ path: "client/styles.css"
1499
+ },
1500
+ {
1501
+ content: counterFunctions,
1502
+ path: "server/counter.ts"
1503
+ },
1504
+ {
1505
+ content: schema,
1506
+ path: "server/schema.ts"
1507
+ },
1508
+ {
1509
+ content: utils,
1510
+ path: "shared/utils.ts"
1511
+ },
1512
+ {
1513
+ content: packageJson(projectName),
1514
+ path: "package.json"
1515
+ },
1516
+ {
1517
+ content: tsconfig,
1518
+ path: "tsconfig.json"
1519
+ },
1520
+ {
1521
+ content: gitignore,
1522
+ path: ".gitignore"
1523
+ },
1524
+ {
1525
+ content: readme(projectName),
1526
+ path: "README.md"
1527
+ },
1528
+ ...agentProjectFiles
1529
+ ];
1530
+ const writeProjectFile = Effect.fn("NewProject.writeProjectFile")(function* (projectDirectory, file) {
1531
+ const fileSystem = yield* FileSystem.FileSystem;
1532
+ const path = yield* Path.Path;
1533
+ const filePath = path.join(projectDirectory, file.path);
1534
+ yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true });
1535
+ yield* fileSystem.writeFileString(filePath, file.content, { flag: "wx" });
1536
+ });
1537
+ const gitExitCode = Effect.fn("NewProject.gitExitCode")(function* (projectDirectory, args) {
1538
+ return yield* (yield* ChildProcessSpawner.ChildProcessSpawner).exitCode(ChildProcess.make("git", args, {
1539
+ cwd: projectDirectory,
1540
+ stderr: "ignore",
1541
+ stdout: "ignore"
1542
+ })).pipe(Effect.mapError((cause) => GitInitializationFailed.make({
1543
+ cause,
1544
+ message: `Could not run Git in ${projectDirectory}.`,
1545
+ path: projectDirectory
1546
+ })));
1547
+ });
1548
+ const runGit = Effect.fn("NewProject.runGit")(function* (projectDirectory, args, action) {
1549
+ const exitCode = yield* gitExitCode(projectDirectory, args);
1550
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) return yield* GitInitializationFailed.make({
1551
+ message: `Git exited with code ${exitCode} while ${action} in ${projectDirectory}.`,
1552
+ path: projectDirectory
1553
+ });
1554
+ });
1555
+ const initializeGit = Effect.fn("NewProject.initializeGit")(function* (projectDirectory) {
1556
+ yield* runGit(projectDirectory, ["init", "--quiet"], "initializing the repository");
1557
+ yield* runGit(projectDirectory, ["add", "--all"], "staging the initial files");
1558
+ const [hasName, hasEmail] = yield* Effect.all([gitExitCode(projectDirectory, ["config", "user.name"]), gitExitCode(projectDirectory, ["config", "user.email"])], { concurrency: "unbounded" }).pipe(Effect.map((exitCodes) => exitCodes.map((exitCode) => exitCode === ChildProcessSpawner.ExitCode(0))));
1559
+ yield* runGit(projectDirectory, [
1560
+ ...hasName === true && hasEmail === true ? [] : [
1561
+ "-c",
1562
+ "user.name=Ignotum",
1563
+ "-c",
1564
+ "user.email=ignotum@localhost"
1565
+ ],
1566
+ "commit",
1567
+ "--quiet",
1568
+ "--no-gpg-sign",
1569
+ "-m",
1570
+ "Init"
1571
+ ], "creating the initial commit");
1572
+ });
1573
+ const createProject = Effect.fn("NewProject.createProject")(function* (options) {
1574
+ const fileSystem = yield* FileSystem.FileSystem;
1575
+ const path = yield* Path.Path;
1576
+ const projectDirectory = path.resolve(options.currentDirectory, options.directory);
1577
+ const projectName = String.kebabCase(path.basename(projectDirectory));
1578
+ if (projectName.length === 0) return yield* InvalidProjectName.make({
1579
+ message: `Could not derive a package name from ${projectDirectory}.`,
1580
+ path: projectDirectory
1581
+ });
1582
+ if (yield* fileSystem.exists(projectDirectory)) {
1583
+ if ((yield* fileSystem.stat(projectDirectory)).type !== "Directory") return yield* ProjectDirectoryNotEmpty.make({
1584
+ message: `${projectDirectory} already exists and is not an empty directory.`,
1585
+ path: projectDirectory
1586
+ });
1587
+ if ((yield* fileSystem.readDirectory(projectDirectory)).length > 0) return yield* ProjectDirectoryNotEmpty.make({
1588
+ message: `${projectDirectory} is not empty. Choose an empty directory.`,
1589
+ path: projectDirectory
1590
+ });
1591
+ } else yield* fileSystem.makeDirectory(projectDirectory, { recursive: true });
1592
+ yield* Effect.forEach(projectFiles(projectName), (file) => writeProjectFile(projectDirectory, file), { discard: true });
1593
+ yield* generate(projectDirectory);
1594
+ const packageManager = options.install ? yield* installDependencies(projectDirectory) : null;
1595
+ if (options.git) yield* initializeGit(projectDirectory);
1596
+ return {
1597
+ directory: projectDirectory,
1598
+ gitInitialized: options.git,
1599
+ packageManager,
1600
+ projectName
1601
+ };
1602
+ });
1603
+ //#endregion
1604
+ //#region src/cli/command.ts
1605
+ const DevPort = Schema.Int.check(Schema.isBetween({
1606
+ minimum: 1,
1607
+ maximum: 65535
1608
+ }));
1609
+ const codegen = Command.make("codegen", {}, Effect.fn("codegen")(function* () {
1610
+ const path = yield* Path.Path;
1611
+ const terminal = yield* Terminal.Terminal;
1612
+ const result = yield* generate(path.resolve(".")).pipe(Effect.mapError((error) => CliError.UserError.make({
1613
+ cause: error,
1614
+ userMessage: error.message
1615
+ })));
1616
+ yield* terminal.display(`Generated ${result.written.length} file${result.written.length === 1 ? "" : "s"}; ${result.unchanged.length} unchanged.\n`);
1617
+ }));
1618
+ const resetDevDatabaseCommand = Command.make("reset", {}, Effect.fn("dev db reset")(function* () {
1619
+ const path = yield* Path.Path;
1620
+ const terminal = yield* Terminal.Terminal;
1621
+ const result = yield* resetDevDatabase(path.resolve(".")).pipe(Effect.mapError((error) => CliError.UserError.make({
1622
+ cause: error,
1623
+ userMessage: error.message
1624
+ })));
1625
+ const message = result.removed === 0 ? `Development database is already empty: ${result.databasePath}\n` : `Reset development database: ${result.databasePath}\n`;
1626
+ yield* terminal.display(message);
1627
+ })).pipe(Command.withDescription("Delete the local SQLite development database."));
1628
+ const devDatabase = Command.make("db").pipe(Command.withDescription("Manage the local development database."), Command.withSubcommands([resetDevDatabaseCommand]));
1629
+ const dev = Command.make("dev", {
1630
+ host: Flag.string("host").pipe(Flag.withDefault("127.0.0.1")),
1631
+ open: Flag.boolean("open").pipe(Flag.withDefault(false)),
1632
+ port: Flag.integer("port").pipe(Flag.withSchema(DevPort), Flag.withDefault(3210))
1633
+ }, Effect.fn("dev")(function* ({ host, open, port }) {
1634
+ const path = yield* Path.Path;
1635
+ return yield* dev$1({
1636
+ host,
1637
+ open,
1638
+ port,
1639
+ projectDirectory: path.resolve(".")
1640
+ }).pipe(Effect.mapError((error) => CliError.UserError.make({
1641
+ cause: error,
1642
+ userMessage: error.message
1643
+ })));
1644
+ })).pipe(Command.withDescription("Run the local Ignotum development server."), Command.withSubcommands([devDatabase]));
1645
+ const install = Command.make("install", {}, Effect.fn("install")(function* () {
1646
+ const path = yield* Path.Path;
1647
+ const terminal = yield* Terminal.Terminal;
1648
+ const packageManager = yield* installDependencies(path.resolve(".")).pipe(Effect.mapError((error) => CliError.UserError.make({
1649
+ cause: error,
1650
+ userMessage: error.message
1651
+ })));
1652
+ yield* terminal.display(`Installed dependencies with ${packageManager}.\n`);
1653
+ })).pipe(Command.withDescription("Install dependencies with pnpm, or npm when pnpm is unavailable."));
1654
+ const newProject = Command.make("new", {
1655
+ directory: Argument.string("directory").pipe(Argument.withDescription("Directory to create. Use \".\" for the current directory.")),
1656
+ git: Flag.boolean("git").pipe(Flag.withDescription("Initialize a Git repository. Use --no-git to skip it."), Flag.withDefault(true)),
1657
+ install: Flag.boolean("install").pipe(Flag.withDescription("Install dependencies. Use --no-install to skip it."), Flag.withDefault(true))
1658
+ }, Effect.fn("new")(function* ({ directory, git, install }) {
1659
+ const path = yield* Path.Path;
1660
+ const terminal = yield* Terminal.Terminal;
1661
+ const currentDirectory = path.resolve(".");
1662
+ const result = yield* createProject({
1663
+ currentDirectory,
1664
+ directory,
1665
+ git,
1666
+ install
1667
+ }).pipe(Effect.mapError((error) => CliError.UserError.make({
1668
+ cause: error,
1669
+ userMessage: error.message
1670
+ })));
1671
+ const changeDirectory = result.directory === currentDirectory ? "" : ` cd ${directory}\n`;
1672
+ const installCommand = result.packageManager === null ? " npx ignotum install\n" : "";
1673
+ yield* terminal.display(`Created ${result.projectName} in ${result.directory}.\n\nNext steps:\n${changeDirectory}${installCommand} npx ignotum dev\n`);
1674
+ })).pipe(Command.withDescription("Create a new Ignotum app in an empty directory."), Command.withExamples([{
1675
+ command: "ignotum new my-app",
1676
+ description: "Create an app in a new directory."
1677
+ }, {
1678
+ command: "ignotum new . --no-git --no-install",
1679
+ description: "Create an app in the current directory without Git or dependency installation."
1680
+ }]));
1681
+ const ignotum = Command.make("ignotum").pipe(Command.withSubcommands([
1682
+ newProject,
1683
+ install,
1684
+ codegen,
1685
+ dev
1686
+ ]));
1687
+ const main = () => {
1688
+ ignotum.pipe(Command.run({ version }), Effect.provide(NodeServices.layer), NodeRuntime.runMain);
1689
+ };
1690
+ //#endregion
1691
+ //#region src/cli/bin.ts
1692
+ main();
1693
+ //#endregion
1694
+ export {};
1695
+
1696
+ //# sourceMappingURL=bin.mjs.map