@ekairos/domain 1.22.36-beta.development.0 → 1.22.36

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 (46) hide show
  1. package/README.md +177 -18
  2. package/SKILL.md +56 -0
  3. package/dist/cli/bin.d.ts +1 -1
  4. package/dist/cli/bin.d.ts.map +1 -1
  5. package/dist/cli/bin.js +141 -20
  6. package/dist/cli/bin.js.map +1 -1
  7. package/dist/cli/create-app.d.ts +34 -1
  8. package/dist/cli/create-app.d.ts.map +1 -1
  9. package/dist/cli/create-app.js +2138 -507
  10. package/dist/cli/create-app.js.map +1 -1
  11. package/dist/cli/http.d.ts.map +1 -1
  12. package/dist/cli/http.js +1 -5
  13. package/dist/cli/http.js.map +1 -1
  14. package/dist/cli/server.d.ts.map +1 -1
  15. package/dist/cli/server.js +5 -2
  16. package/dist/cli/server.js.map +1 -1
  17. package/dist/cli/types.d.ts +1 -0
  18. package/dist/cli/types.d.ts.map +1 -1
  19. package/dist/cli/ui.d.ts.map +1 -1
  20. package/dist/cli/ui.js +2 -0
  21. package/dist/cli/ui.js.map +1 -1
  22. package/dist/context.test-runner.js +3 -1
  23. package/dist/context.test-runner.js.map +1 -1
  24. package/dist/domain-doc.d.ts +2 -0
  25. package/dist/domain-doc.d.ts.map +1 -1
  26. package/dist/domain-doc.js +14 -0
  27. package/dist/domain-doc.js.map +1 -1
  28. package/dist/index.d.ts +194 -53
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +313 -137
  31. package/dist/index.js.map +1 -1
  32. package/dist/next.d.ts.map +1 -1
  33. package/dist/next.js +3 -2
  34. package/dist/next.js.map +1 -1
  35. package/dist/runtime-handle.d.ts +14 -3
  36. package/dist/runtime-handle.d.ts.map +1 -1
  37. package/dist/runtime-handle.js +2 -0
  38. package/dist/runtime-handle.js.map +1 -1
  39. package/dist/runtime-step.d.ts.map +1 -1
  40. package/dist/runtime-step.js +2 -0
  41. package/dist/runtime-step.js.map +1 -1
  42. package/dist/runtime.d.ts +7 -7
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +11 -8
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +15 -11
@@ -1,14 +1,19 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createServer } from "node:net";
2
3
  import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { dirname, join, relative, resolve } from "node:path";
4
5
  import { PlatformApi } from "@instantdb/platform";
5
6
  import { i } from "@instantdb/core";
6
7
  import { domain } from "../index.js";
7
- const TEMPLATE_NEXT_VERSION = "15.5.7";
8
- const TEMPLATE_REACT_VERSION = "19.2.1";
9
- const TEMPLATE_TYPESCRIPT_VERSION = "^5.9.2";
10
- const TEMPLATE_INSTANT_VERSION = "0.22.126";
11
- const TEMPLATE_WORKFLOW_VERSION = "^5.0.0-beta.1";
8
+ export const CREATE_DOMAIN_APP_TEMPLATES = ["empty", "supply-chain", "agent"];
9
+ const TEMPLATE_NEXT_VERSION = "16.2.6";
10
+ const TEMPLATE_REACT_VERSION = "19.2.6";
11
+ const TEMPLATE_TYPESCRIPT_VERSION = "^6.0.3";
12
+ const TEMPLATE_INSTANT_VERSION = "1.0.39";
13
+ const TEMPLATE_INSTANT_REACT_VERSION = "1.0.39";
14
+ const TEMPLATE_WORKFLOW_VERSION = "5.0.0-beta.6";
15
+ const TEMPLATE_VERCEL_OIDC_VERSION = "3.4.1";
16
+ const TEMPLATE_EVENTS_VERSION = "1.22.82-beta.development.0";
12
17
  function trimOrEmpty(value) {
13
18
  return typeof value === "string" ? value.trim() : "";
14
19
  }
@@ -67,52 +72,371 @@ async function readDomainPackageVersion() {
67
72
  const parsed = JSON.parse(raw);
68
73
  return trimOrEmpty(parsed.version) || "latest";
69
74
  }
70
- function createScaffoldSchema() {
71
- const scaffoldDomain = domain("ekairos.app").schema({
75
+ export function normalizeCreateAppTemplate(value) {
76
+ const normalized = trimOrEmpty(value);
77
+ if (!normalized)
78
+ return null;
79
+ if (CREATE_DOMAIN_APP_TEMPLATES.includes(normalized)) {
80
+ return normalized;
81
+ }
82
+ throw new Error(`Unsupported template: ${normalized}. Supported templates: ${CREATE_DOMAIN_APP_TEMPLATES.join(", ")}.`);
83
+ }
84
+ function resolveCreateAppTemplate(params) {
85
+ if (params.template) {
86
+ if (params.demo && params.template !== "supply-chain") {
87
+ throw new Error("--demo only supports --template=supply-chain.");
88
+ }
89
+ return params.template;
90
+ }
91
+ return params.demo ? "supply-chain" : "empty";
92
+ }
93
+ function createScaffoldSchema(template) {
94
+ if (template === "supply-chain")
95
+ return createSupplyChainScaffoldSchema();
96
+ if (template === "agent")
97
+ return createAgentScaffoldSchema();
98
+ return createEmptyScaffoldSchema();
99
+ }
100
+ function createEmptyScaffoldSchema() {
101
+ const scaffoldDomain = domain("app").withSchema({
102
+ entities: {},
103
+ links: {},
104
+ rooms: {},
105
+ });
106
+ return scaffoldDomain.toInstantSchema();
107
+ }
108
+ function createSupplyChainScaffoldSchema() {
109
+ const supplierNetworkDomain = domain("supplierNetwork").withSchema({
110
+ entities: {
111
+ supplierNetwork_supplier: i.entity({
112
+ name: i.string().indexed(),
113
+ region: i.string().indexed(),
114
+ risk: i.string().indexed(),
115
+ score: i.number().indexed(),
116
+ createdAt: i.number().indexed(),
117
+ }),
118
+ },
119
+ links: {},
120
+ rooms: {},
121
+ });
122
+ const procurementDomain = domain("procurement")
123
+ .includes(supplierNetworkDomain)
124
+ .withSchema({
125
+ entities: {
126
+ procurement_order: i.entity({
127
+ reference: i.string().indexed(),
128
+ status: i.string().indexed(),
129
+ spend: i.number().indexed(),
130
+ createdAt: i.number().indexed(),
131
+ }),
132
+ },
133
+ links: {
134
+ procurement_orderSupplier: {
135
+ forward: { on: "procurement_order", has: "one", label: "supplier" },
136
+ reverse: { on: "supplierNetwork_supplier", has: "many", label: "orders" },
137
+ },
138
+ },
139
+ rooms: {},
140
+ });
141
+ const inventoryDomain = domain("inventory")
142
+ .includes(procurementDomain)
143
+ .withSchema({
72
144
  entities: {
73
- app_tasks: i.entity({
74
- title: i.string().indexed(),
145
+ inventory_stockItem: i.entity({
146
+ sku: i.string().indexed(),
147
+ warehouse: i.string().indexed(),
148
+ available: i.number().indexed(),
149
+ safetyStock: i.number().indexed(),
150
+ createdAt: i.number().indexed(),
151
+ }),
152
+ },
153
+ links: {
154
+ inventory_stockItemOrder: {
155
+ forward: { on: "inventory_stockItem", has: "one", label: "order" },
156
+ reverse: { on: "procurement_order", has: "many", label: "stockItems" },
157
+ },
158
+ },
159
+ rooms: {},
160
+ });
161
+ const transportationDomain = domain("transportation")
162
+ .includes(procurementDomain)
163
+ .withSchema({
164
+ entities: {
165
+ transportation_shipment: i.entity({
166
+ carrier: i.string().indexed(),
167
+ lane: i.string().indexed(),
75
168
  status: i.string().indexed(),
169
+ etaHours: i.number().indexed(),
76
170
  createdAt: i.number().indexed(),
77
171
  }),
78
- app_task_comments: i.entity({
79
- body: i.string(),
172
+ },
173
+ links: {
174
+ transportation_shipmentOrder: {
175
+ forward: { on: "transportation_shipment", has: "one", label: "order" },
176
+ reverse: { on: "procurement_order", has: "many", label: "shipments" },
177
+ },
178
+ },
179
+ rooms: {},
180
+ });
181
+ const qualityControlDomain = domain("qualityControl")
182
+ .includes(transportationDomain)
183
+ .withSchema({
184
+ entities: {
185
+ qualityControl_inspection: i.entity({
186
+ result: i.string().indexed(),
187
+ severity: i.string().indexed(),
188
+ note: i.string(),
80
189
  createdAt: i.number().indexed(),
81
190
  }),
82
191
  },
83
192
  links: {
84
- taskComments: {
85
- forward: { on: "app_tasks", has: "many", label: "comments" },
86
- reverse: { on: "app_task_comments", has: "one", label: "task" },
193
+ qualityControl_inspectionShipment: {
194
+ forward: { on: "qualityControl_inspection", has: "one", label: "shipment" },
195
+ reverse: { on: "transportation_shipment", has: "many", label: "inspections" },
87
196
  },
88
197
  },
89
198
  rooms: {},
90
199
  });
200
+ const scaffoldDomain = domain("supplyChain")
201
+ .includes(inventoryDomain)
202
+ .includes(qualityControlDomain)
203
+ .withSchema({ entities: {}, links: {}, rooms: {} });
91
204
  return scaffoldDomain.toInstantSchema();
92
205
  }
93
- function createScaffoldPerms() {
206
+ function createAgentScaffoldSchema() {
207
+ return domain("events")
208
+ .withSchema({
209
+ entities: {
210
+ event_contexts: i.entity({
211
+ createdAt: i.date(),
212
+ updatedAt: i.date().optional(),
213
+ key: i.string().optional().indexed().unique(),
214
+ name: i.string().optional(),
215
+ status: i.string().optional().indexed(),
216
+ content: i.any().optional(),
217
+ reactor: i.json().optional(),
218
+ }),
219
+ event_items: i.entity({
220
+ channel: i.string().indexed(),
221
+ createdAt: i.date().indexed(),
222
+ type: i.string().optional().indexed(),
223
+ content: i.any().optional(),
224
+ status: i.string().optional().indexed(),
225
+ }),
226
+ event_executions: i.entity({
227
+ createdAt: i.date(),
228
+ updatedAt: i.date().optional(),
229
+ status: i.string().optional().indexed(),
230
+ workflowRunId: i.string().optional().indexed(),
231
+ activeStreamId: i.string().optional().indexed(),
232
+ activeStreamClientId: i.string().optional().indexed(),
233
+ lastStreamId: i.string().optional().indexed(),
234
+ lastStreamClientId: i.string().optional().indexed(),
235
+ }),
236
+ event_steps: i.entity({
237
+ createdAt: i.date().indexed(),
238
+ updatedAt: i.date().optional(),
239
+ status: i.string().optional().indexed(),
240
+ iteration: i.number().indexed(),
241
+ errorText: i.string().optional(),
242
+ streamId: i.string().optional().indexed(),
243
+ streamClientId: i.string().optional().indexed(),
244
+ streamStartedAt: i.date().optional().indexed(),
245
+ streamFinishedAt: i.date().optional().indexed(),
246
+ streamAbortReason: i.string().optional(),
247
+ }),
248
+ event_parts: i.entity({
249
+ key: i.string().unique().indexed(),
250
+ stepId: i.string().indexed(),
251
+ idx: i.number().indexed(),
252
+ type: i.string().optional().indexed(),
253
+ part: i.json().optional(),
254
+ metadata: i.json().optional(),
255
+ updatedAt: i.date().optional(),
256
+ }),
257
+ event_trace_events: i.entity({
258
+ key: i.string().unique().indexed(),
259
+ workflowRunId: i.string().indexed(),
260
+ seq: i.number().indexed(),
261
+ eventId: i.string().indexed(),
262
+ eventKind: i.string().indexed(),
263
+ eventAt: i.date().optional(),
264
+ ingestedAt: i.date().optional(),
265
+ orgId: i.string().optional().indexed(),
266
+ projectId: i.string().optional().indexed(),
267
+ contextKey: i.string().optional().indexed(),
268
+ contextId: i.string().optional().indexed(),
269
+ executionId: i.string().optional().indexed(),
270
+ stepId: i.string().optional().indexed(),
271
+ contextEventId: i.string().optional().indexed(),
272
+ toolCallId: i.string().optional().indexed(),
273
+ partKey: i.string().optional().indexed(),
274
+ partIdx: i.number().optional().indexed(),
275
+ spanId: i.string().optional().indexed(),
276
+ parentSpanId: i.string().optional().indexed(),
277
+ isDeleted: i.boolean().optional(),
278
+ aiProvider: i.string().optional().indexed(),
279
+ aiModel: i.string().optional().indexed(),
280
+ promptTokens: i.number().optional(),
281
+ promptTokensCached: i.number().optional(),
282
+ promptTokensUncached: i.number().optional(),
283
+ completionTokens: i.number().optional(),
284
+ totalTokens: i.number().optional(),
285
+ latencyMs: i.number().optional(),
286
+ cacheCostUsd: i.number().optional(),
287
+ computeCostUsd: i.number().optional(),
288
+ costUsd: i.number().optional(),
289
+ payload: i.any().optional(),
290
+ }),
291
+ event_trace_runs: i.entity({
292
+ workflowRunId: i.string().unique().indexed(),
293
+ orgId: i.string().optional().indexed(),
294
+ projectId: i.string().optional().indexed(),
295
+ firstEventAt: i.date().optional().indexed(),
296
+ lastEventAt: i.date().optional().indexed(),
297
+ lastIngestedAt: i.date().optional().indexed(),
298
+ eventsCount: i.number().optional(),
299
+ status: i.string().optional().indexed(),
300
+ payload: i.any().optional(),
301
+ }),
302
+ event_trace_spans: i.entity({
303
+ spanId: i.string().unique().indexed(),
304
+ parentSpanId: i.string().optional().indexed(),
305
+ workflowRunId: i.string().indexed(),
306
+ executionId: i.string().optional().indexed(),
307
+ stepId: i.string().optional().indexed(),
308
+ kind: i.string().optional().indexed(),
309
+ name: i.string().optional().indexed(),
310
+ status: i.string().optional().indexed(),
311
+ startedAt: i.date().optional().indexed(),
312
+ endedAt: i.date().optional().indexed(),
313
+ durationMs: i.number().optional(),
314
+ payload: i.any().optional(),
315
+ }),
316
+ document_documents: i.entity({
317
+ name: i.string().optional().indexed(),
318
+ mimeType: i.string().optional(),
319
+ size: i.number().optional(),
320
+ ownerId: i.string().optional().indexed(),
321
+ orgId: i.string().optional().indexed(),
322
+ createdAt: i.date().optional().indexed(),
323
+ processedAt: i.date().optional().indexed(),
324
+ updatedAt: i.date().optional(),
325
+ lastJobId: i.string().optional(),
326
+ content: i.json().optional(),
327
+ }),
328
+ },
329
+ links: {
330
+ contextItemsContext: {
331
+ forward: { on: "event_items", has: "one", label: "context" },
332
+ reverse: { on: "event_contexts", has: "many", label: "items" },
333
+ },
334
+ contextExecutionsContext: {
335
+ forward: { on: "event_executions", has: "one", label: "context" },
336
+ reverse: { on: "event_contexts", has: "many", label: "executions" },
337
+ },
338
+ contextCurrentExecution: {
339
+ forward: { on: "event_contexts", has: "one", label: "currentExecution" },
340
+ reverse: { on: "event_executions", has: "one", label: "currentOf" },
341
+ },
342
+ contextExecutionsTrigger: {
343
+ forward: { on: "event_executions", has: "one", label: "trigger" },
344
+ reverse: { on: "event_items", has: "many", label: "executionsAsTrigger" },
345
+ },
346
+ contextExecutionsReaction: {
347
+ forward: { on: "event_executions", has: "one", label: "reaction" },
348
+ reverse: { on: "event_items", has: "many", label: "executionsAsReaction" },
349
+ },
350
+ contextStepsExecution: {
351
+ forward: { on: "event_steps", has: "one", label: "execution" },
352
+ reverse: { on: "event_executions", has: "many", label: "steps" },
353
+ },
354
+ contextExecutionItems: {
355
+ forward: { on: "event_items", has: "one", label: "execution" },
356
+ reverse: { on: "event_executions", has: "many", label: "items" },
357
+ },
358
+ contextPartsStep: {
359
+ forward: { on: "event_parts", has: "one", label: "step" },
360
+ reverse: { on: "event_steps", has: "many", label: "parts" },
361
+ },
362
+ contextStepStream: {
363
+ forward: { on: "event_steps", has: "one", label: "stream" },
364
+ reverse: { on: "$streams", has: "many", label: "step" },
365
+ },
366
+ contextExecutionActiveStream: {
367
+ forward: { on: "event_executions", has: "one", label: "activeStream" },
368
+ reverse: { on: "$streams", has: "many", label: "activeOf" },
369
+ },
370
+ contextExecutionLastStream: {
371
+ forward: { on: "event_executions", has: "one", label: "lastStream" },
372
+ reverse: { on: "$streams", has: "many", label: "lastOf" },
373
+ },
374
+ documentFile: {
375
+ forward: { on: "document_documents", has: "one", label: "file" },
376
+ reverse: { on: "$files", has: "one", label: "document" },
377
+ },
378
+ },
379
+ rooms: {},
380
+ })
381
+ .toInstantSchema();
382
+ }
383
+ function createScaffoldPerms(template) {
384
+ if (template === "supply-chain")
385
+ return createSupplyChainScaffoldPerms();
386
+ if (template === "agent")
387
+ return createAgentScaffoldPerms();
388
+ return createEmptyScaffoldPerms();
389
+ }
390
+ function createEmptyScaffoldPerms() {
94
391
  return {
95
392
  attrs: {
96
393
  allow: { create: "true" },
97
394
  },
98
- app_tasks: {
99
- bind: ["isLoggedIn", "auth.id != null"],
100
- allow: {
101
- view: "true",
102
- create: "isLoggedIn",
103
- update: "isLoggedIn",
104
- delete: "false",
105
- },
395
+ };
396
+ }
397
+ function createSupplyChainScaffoldPerms() {
398
+ const entityRules = {
399
+ bind: ["isLoggedIn", "auth.id != null"],
400
+ allow: {
401
+ view: "true",
402
+ create: "isLoggedIn",
403
+ update: "isLoggedIn",
404
+ delete: "false",
106
405
  },
107
- app_task_comments: {
108
- bind: ["isLoggedIn", "auth.id != null"],
109
- allow: {
110
- view: "true",
111
- create: "isLoggedIn",
112
- update: "isLoggedIn",
113
- delete: "false",
114
- },
406
+ };
407
+ return {
408
+ attrs: {
409
+ allow: { create: "true" },
115
410
  },
411
+ supplierNetwork_supplier: entityRules,
412
+ procurement_order: entityRules,
413
+ inventory_stockItem: entityRules,
414
+ transportation_shipment: entityRules,
415
+ qualityControl_inspection: entityRules,
416
+ };
417
+ }
418
+ function createAgentScaffoldPerms() {
419
+ const entityRules = {
420
+ allow: {
421
+ view: "true",
422
+ create: "true",
423
+ update: "true",
424
+ delete: "false",
425
+ },
426
+ };
427
+ return {
428
+ attrs: {
429
+ allow: { create: "true" },
430
+ },
431
+ event_contexts: entityRules,
432
+ event_items: entityRules,
433
+ event_executions: entityRules,
434
+ event_steps: entityRules,
435
+ event_parts: entityRules,
436
+ event_trace_events: entityRules,
437
+ event_trace_runs: entityRules,
438
+ event_trace_spans: entityRules,
439
+ document_documents: entityRules,
116
440
  };
117
441
  }
118
442
  function installCommandFor(packageManager) {
@@ -133,6 +457,328 @@ function runScriptCommandFor(packageManager, script) {
133
457
  return `bun run ${script}`;
134
458
  return `npm run ${script}`;
135
459
  }
460
+ function packageBinCommandFor(packageManager, command) {
461
+ void command;
462
+ return "ekairos domain";
463
+ }
464
+ function typecheckCommandFor(packageManager) {
465
+ if (packageManager === "pnpm")
466
+ return { command: "pnpm", args: ["typecheck"] };
467
+ if (packageManager === "yarn")
468
+ return { command: "yarn", args: ["typecheck"] };
469
+ if (packageManager === "bun")
470
+ return { command: "bun", args: ["run", "typecheck"] };
471
+ return { command: "npm", args: ["run", "typecheck"] };
472
+ }
473
+ function nextDevCommandFor(targetDir, port) {
474
+ return {
475
+ command: process.execPath,
476
+ args: [
477
+ join(targetDir, "node_modules", "next", "dist", "bin", "next"),
478
+ "dev",
479
+ "--hostname",
480
+ "127.0.0.1",
481
+ "--port",
482
+ String(port),
483
+ ],
484
+ };
485
+ }
486
+ async function runCommand(params) {
487
+ const timeoutMs = params.timeoutMs ?? 2 * 60 * 1000;
488
+ await new Promise((resolveRun, rejectRun) => {
489
+ const child = spawn(params.command, params.args, {
490
+ cwd: params.targetDir,
491
+ env: process.env,
492
+ shell: process.platform === "win32",
493
+ stdio: "pipe",
494
+ });
495
+ let output = "";
496
+ const timer = setTimeout(() => {
497
+ stopProcess(child.pid);
498
+ rejectRun(new Error(`${params.command} timed out after ${timeoutMs}ms`));
499
+ }, timeoutMs);
500
+ child.stdout.on("data", (chunk) => {
501
+ output += chunk.toString();
502
+ });
503
+ child.stderr.on("data", (chunk) => {
504
+ output += chunk.toString();
505
+ });
506
+ child.on("error", (error) => {
507
+ clearTimeout(timer);
508
+ rejectRun(error);
509
+ });
510
+ child.on("close", (code) => {
511
+ clearTimeout(timer);
512
+ if (code === 0) {
513
+ resolveRun();
514
+ return;
515
+ }
516
+ rejectRun(new Error(output.trim() || `${params.command} failed with exit code ${code ?? "unknown"}`));
517
+ });
518
+ });
519
+ }
520
+ async function reservePort() {
521
+ return await new Promise((resolvePort, rejectPort) => {
522
+ const server = createServer();
523
+ server.once("error", rejectPort);
524
+ server.listen(0, "127.0.0.1", () => {
525
+ const address = server.address();
526
+ if (!address || typeof address === "string") {
527
+ rejectPort(new Error("Failed to reserve smoke port"));
528
+ return;
529
+ }
530
+ const port = address.port;
531
+ server.close((error) => {
532
+ if (error)
533
+ rejectPort(error);
534
+ else
535
+ resolvePort(port);
536
+ });
537
+ });
538
+ });
539
+ }
540
+ function stopProcess(pid) {
541
+ if (!pid)
542
+ return;
543
+ if (process.platform === "win32") {
544
+ spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
545
+ stdio: "ignore",
546
+ });
547
+ return;
548
+ }
549
+ try {
550
+ process.kill(-pid, "SIGTERM");
551
+ }
552
+ catch {
553
+ try {
554
+ process.kill(pid, "SIGTERM");
555
+ }
556
+ catch {
557
+ // Process already exited.
558
+ }
559
+ }
560
+ }
561
+ function quotePowerShellString(value) {
562
+ return `'${value.replace(/'/g, "''")}'`;
563
+ }
564
+ function startDetachedWindowsProcess(params) {
565
+ const argumentList = params.args.map(quotePowerShellString).join(", ");
566
+ const script = [
567
+ `$p = Start-Process -FilePath ${quotePowerShellString(params.command)} ` +
568
+ `-ArgumentList @(${argumentList}) ` +
569
+ `-WorkingDirectory ${quotePowerShellString(params.targetDir)} ` +
570
+ "-WindowStyle Hidden -PassThru",
571
+ "Write-Output $p.Id",
572
+ ].join("; ");
573
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
574
+ encoding: "utf8",
575
+ stdio: "pipe",
576
+ });
577
+ if ((result.status ?? 1) !== 0) {
578
+ throw new Error(result.stderr?.trim() ||
579
+ result.stdout?.trim() ||
580
+ "Failed to start detached Next dev server");
581
+ }
582
+ const pid = Number(String(result.stdout ?? "").trim().split(/\s+/).pop());
583
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
584
+ }
585
+ async function fetchJsonWithTimeout(url, init, timeoutMs = 10000) {
586
+ const controller = new AbortController();
587
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
588
+ try {
589
+ const response = await fetch(url, {
590
+ ...init,
591
+ signal: controller.signal,
592
+ });
593
+ const data = await response.json().catch(() => null);
594
+ return { response, data: data };
595
+ }
596
+ finally {
597
+ clearTimeout(timer);
598
+ }
599
+ }
600
+ async function waitForDomainEndpoint(params) {
601
+ const endpoint = `${params.baseUrl}/api/ekairos/domain`;
602
+ const deadline = Date.now() + 3 * 60 * 1000;
603
+ let lastError = "";
604
+ while (Date.now() < deadline) {
605
+ if (params.processExited()) {
606
+ throw new Error(`Next dev server exited before smoke endpoint was ready.\n${params.readLogs()}`);
607
+ }
608
+ try {
609
+ const { response, data } = await fetchJsonWithTimeout(endpoint);
610
+ if (response.ok && data?.ok === true)
611
+ return data;
612
+ lastError = `status:${response.status}`;
613
+ }
614
+ catch (error) {
615
+ lastError = error instanceof Error ? error.message : String(error);
616
+ }
617
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 1000));
618
+ }
619
+ throw new Error(`Timed out waiting for ${endpoint}: ${lastError}\n${params.readLogs()}`);
620
+ }
621
+ function countCollection(value) {
622
+ if (Array.isArray(value))
623
+ return value.length;
624
+ if (value && typeof value === "object")
625
+ return Object.keys(value).length;
626
+ return 0;
627
+ }
628
+ function asArray(value) {
629
+ return Array.isArray(value) ? value : [];
630
+ }
631
+ async function postSmokeJson(baseUrl, body) {
632
+ const { response, data } = await fetchJsonWithTimeout(`${baseUrl}/api/ekairos/domain`, {
633
+ method: "POST",
634
+ headers: { "content-type": "application/json" },
635
+ body: JSON.stringify(body),
636
+ }, 30000);
637
+ if (!response.ok || data?.ok !== true) {
638
+ throw new Error(data?.error || `Smoke request failed with status ${response.status}`);
639
+ }
640
+ return data;
641
+ }
642
+ async function runSmoke(params) {
643
+ await emitProgress(params.onProgress, {
644
+ stage: "smoke",
645
+ status: "running",
646
+ message: "Running typecheck",
647
+ progress: 97,
648
+ });
649
+ const typecheck = typecheckCommandFor(params.packageManager);
650
+ await runCommand({
651
+ targetDir: params.targetDir,
652
+ command: typecheck.command,
653
+ args: typecheck.args,
654
+ });
655
+ const port = await reservePort();
656
+ const baseUrl = `http://127.0.0.1:${port}`;
657
+ const dev = nextDevCommandFor(params.targetDir, port);
658
+ let logs = "";
659
+ let smokePassed = false;
660
+ let detachedPid = null;
661
+ const child = params.keepServer && process.platform === "win32"
662
+ ? null
663
+ : spawn(dev.command, dev.args, {
664
+ cwd: params.targetDir,
665
+ env: process.env,
666
+ detached: params.keepServer,
667
+ shell: false,
668
+ stdio: params.keepServer ? "ignore" : "pipe",
669
+ });
670
+ if (params.keepServer && process.platform === "win32") {
671
+ detachedPid = startDetachedWindowsProcess({
672
+ targetDir: params.targetDir,
673
+ command: dev.command,
674
+ args: dev.args,
675
+ });
676
+ }
677
+ if (child && !params.keepServer) {
678
+ child.stdout?.on("data", (chunk) => {
679
+ logs += chunk.toString();
680
+ });
681
+ child.stderr?.on("data", (chunk) => {
682
+ logs += chunk.toString();
683
+ });
684
+ }
685
+ try {
686
+ await emitProgress(params.onProgress, {
687
+ stage: "smoke",
688
+ status: "running",
689
+ message: `Waiting for ${baseUrl}`,
690
+ progress: 98,
691
+ });
692
+ const manifest = await waitForDomainEndpoint({
693
+ baseUrl,
694
+ processExited: () => (child ? child.exitCode !== null : false),
695
+ readLogs: () => logs,
696
+ });
697
+ let launchedOrder = false;
698
+ let orders = [];
699
+ let shipments = 0;
700
+ let inspections = 0;
701
+ if (params.demo) {
702
+ const launch = await postSmokeJson(baseUrl, {
703
+ op: "action",
704
+ action: "supplyChain.order.launch",
705
+ input: {
706
+ reference: "PO-SMOKE-7842",
707
+ sku: "DRV-2048",
708
+ supplierName: "Marula Components",
709
+ },
710
+ admin: true,
711
+ });
712
+ if (String(launch.action ?? "") !== "supplyChain.order.launch") {
713
+ throw new Error("Smoke launch order action returned an unexpected action");
714
+ }
715
+ launchedOrder = true;
716
+ const query = await postSmokeJson(baseUrl, {
717
+ op: "query",
718
+ query: {
719
+ procurement_order: {
720
+ supplier: {},
721
+ stockItems: {},
722
+ shipments: {
723
+ inspections: {},
724
+ },
725
+ },
726
+ },
727
+ admin: true,
728
+ });
729
+ orders = asArray(query?.data?.procurement_order);
730
+ shipments = orders.reduce((total, order) => {
731
+ return total + asArray(order?.shipments).length;
732
+ }, 0);
733
+ inspections = orders.reduce((total, order) => {
734
+ const orderShipments = asArray(order?.shipments);
735
+ return total + orderShipments.reduce((shipmentTotal, shipment) => {
736
+ return shipmentTotal + asArray(shipment?.inspections).length;
737
+ }, 0);
738
+ }, 0);
739
+ }
740
+ smokePassed = true;
741
+ const result = {
742
+ ok: true,
743
+ baseUrl,
744
+ keepServer: params.keepServer,
745
+ pid: detachedPid ?? child?.pid ?? null,
746
+ typecheck: true,
747
+ domainEndpoint: true,
748
+ inspect: {
749
+ entities: countCollection(manifest?.domain?.entities),
750
+ actions: countCollection(manifest?.actions),
751
+ },
752
+ launchedOrder,
753
+ query: {
754
+ inspections,
755
+ orders: orders.length,
756
+ shipments,
757
+ },
758
+ };
759
+ await emitProgress(params.onProgress, {
760
+ stage: "smoke",
761
+ status: "completed",
762
+ message: params.keepServer
763
+ ? `Smoke passed and server is running at ${baseUrl}`
764
+ : "Smoke passed",
765
+ progress: 99,
766
+ });
767
+ if (params.keepServer) {
768
+ params.onKeepServer?.({
769
+ unref() {
770
+ child?.unref();
771
+ },
772
+ });
773
+ }
774
+ return result;
775
+ }
776
+ finally {
777
+ if (!params.keepServer || !smokePassed) {
778
+ stopProcess(detachedPid ?? child?.pid);
779
+ }
780
+ }
781
+ }
136
782
  async function provisionInstantApp(params) {
137
783
  const api = new PlatformApi({
138
784
  auth: { token: params.instantToken },
@@ -140,8 +786,8 @@ async function provisionInstantApp(params) {
140
786
  const created = await api.createApp({
141
787
  title: `ekairos-${trimOrEmpty(params.directory.split(/[\\/]/).pop()) || "app"}`,
142
788
  orgId: trimOrEmpty(params.orgId) || undefined,
143
- schema: createScaffoldSchema(),
144
- perms: createScaffoldPerms(),
789
+ schema: createScaffoldSchema(params.template),
790
+ perms: createScaffoldPerms(params.template),
145
791
  });
146
792
  const appId = trimOrEmpty(created?.app?.id);
147
793
  const adminToken = trimOrEmpty(created?.app?.adminToken);
@@ -219,20 +865,27 @@ async function writeScaffoldFiles(targetDir, files) {
219
865
  await writeFile(absolutePath, content, "utf8");
220
866
  }
221
867
  }
222
- function resolveDomainDependencyVersion(version, targetDir, workspacePath) {
868
+ function resolveWorkspacePackageDependencyVersion(packageName, version, targetDir, workspacePath) {
223
869
  const workspaceRoot = trimOrEmpty(workspacePath);
224
870
  if (!workspaceRoot) {
225
871
  return version;
226
872
  }
227
- const packageRoot = resolve(workspaceRoot, "packages/domain");
873
+ const packageRoot = resolve(workspaceRoot, "packages", packageName.split("/").pop() || packageName);
228
874
  const relativePath = toPosix(relative(targetDir, packageRoot));
229
875
  if (!relativePath)
230
876
  return "file:.";
231
877
  const prefixed = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
232
878
  return `file:${prefixed}`;
233
879
  }
880
+ function resolveDomainDependencyVersion(version, targetDir, workspacePath) {
881
+ return resolveWorkspacePackageDependencyVersion("@ekairos/domain", version, targetDir, workspacePath);
882
+ }
234
883
  function buildNextTemplateFiles(params) {
235
- const domainDependency = resolveDomainDependencyVersion(params.domainVersion, params.targetDir, params.workspacePath);
884
+ const workspaceRoot = trimOrEmpty(params.workspacePath);
885
+ const domainDependency = params.template === "agent" && !workspaceRoot
886
+ ? TEMPLATE_EVENTS_VERSION
887
+ : resolveDomainDependencyVersion(params.domainVersion, params.targetDir, params.workspacePath);
888
+ const eventsDependency = resolveWorkspacePackageDependencyVersion("@ekairos/events", TEMPLATE_EVENTS_VERSION, params.targetDir, params.workspacePath);
236
889
  const packageJson = {
237
890
  name: trimOrEmpty(params.targetDir.split(/[\\/]/).pop()) || "ekairos-app",
238
891
  private: true,
@@ -246,12 +899,15 @@ function buildNextTemplateFiles(params) {
246
899
  },
247
900
  dependencies: {
248
901
  "@ekairos/domain": domainDependency,
902
+ "@vercel/oidc": TEMPLATE_VERCEL_OIDC_VERSION,
249
903
  "@instantdb/admin": TEMPLATE_INSTANT_VERSION,
250
904
  "@instantdb/core": TEMPLATE_INSTANT_VERSION,
905
+ "@instantdb/react": TEMPLATE_INSTANT_REACT_VERSION,
251
906
  next: TEMPLATE_NEXT_VERSION,
252
907
  react: TEMPLATE_REACT_VERSION,
253
908
  "react-dom": TEMPLATE_REACT_VERSION,
254
909
  workflow: TEMPLATE_WORKFLOW_VERSION,
910
+ zod: "^4.3.6",
255
911
  },
256
912
  devDependencies: {
257
913
  "@types/node": "^24.5.0",
@@ -265,6 +921,651 @@ function buildNextTemplateFiles(params) {
265
921
  ? "yarn@1"
266
922
  : undefined,
267
923
  };
924
+ if (params.template === "agent") {
925
+ ;
926
+ packageJson.dependencies["@ekairos/events"] = eventsDependency;
927
+ if (params.packageManager === "pnpm") {
928
+ ;
929
+ packageJson.pnpm = {
930
+ overrides: {
931
+ "@ekairos/domain": domainDependency,
932
+ "@instantdb/admin": TEMPLATE_INSTANT_VERSION,
933
+ "@instantdb/core": TEMPLATE_INSTANT_VERSION,
934
+ },
935
+ };
936
+ }
937
+ }
938
+ if (params.template === "empty") {
939
+ return {
940
+ ".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
941
+ ".env.example": [
942
+ "NEXT_PUBLIC_INSTANT_APP_ID=",
943
+ "INSTANT_ADMIN_TOKEN=",
944
+ "",
945
+ "# Optional: use this only while provisioning new apps with the CLI.",
946
+ "INSTANT_PERSONAL_ACCESS_TOKEN=",
947
+ ].join("\n"),
948
+ "DOMAIN.md": [
949
+ "# Ekairos App Domain",
950
+ "",
951
+ "This app starts empty on purpose.",
952
+ "",
953
+ "Add your first domain in `src/domain.ts`, then expose it through `src/runtime.ts` and `/api/ekairos/domain`.",
954
+ "",
955
+ "Suggested first step:",
956
+ "- create one domain with camelCase name",
957
+ "- name entities as `<domainName>_<entityName>`",
958
+ "- add one `defineAction` for the first business write",
959
+ ].join("\n"),
960
+ "instant.schema.ts": [
961
+ 'import appDomain from "./src/domain";',
962
+ "",
963
+ "const schema = appDomain.toInstantSchema();",
964
+ "",
965
+ "export default schema;",
966
+ ].join("\n"),
967
+ "next-env.d.ts": [
968
+ '/// <reference types="next" />',
969
+ '/// <reference types="next/image-types/global" />',
970
+ "",
971
+ "// This file is managed by Next.js.",
972
+ ].join("\n"),
973
+ "next.config.ts": [
974
+ 'import type { NextConfig } from "next";',
975
+ 'import { withWorkflow } from "workflow/next";',
976
+ "",
977
+ "const nextConfig: NextConfig = {",
978
+ " transpilePackages: [\"@ekairos/domain\"],",
979
+ "};",
980
+ "",
981
+ "export default withWorkflow(nextConfig) as NextConfig;",
982
+ ].join("\n"),
983
+ "src/app/api/ekairos/domain/route.ts": [
984
+ 'import { createRuntimeRouteHandler } from "@ekairos/domain/next";',
985
+ 'import { createRuntime } from "@/runtime";',
986
+ "",
987
+ "export const { GET, POST } = createRuntimeRouteHandler({",
988
+ " createRuntime,",
989
+ "});",
990
+ ].join("\n"),
991
+ "package.json": `${JSON.stringify(packageJson, null, 2)}\n`,
992
+ "tsconfig.json": [
993
+ "{",
994
+ ' "compilerOptions": {',
995
+ ' "target": "ES2022",',
996
+ ' "lib": ["dom", "dom.iterable", "es2022"],',
997
+ ' "allowJs": false,',
998
+ ' "skipLibCheck": true,',
999
+ ' "strict": true,',
1000
+ ' "noEmit": true,',
1001
+ ' "esModuleInterop": true,',
1002
+ ' "module": "esnext",',
1003
+ ' "moduleResolution": "bundler",',
1004
+ ' "resolveJsonModule": true,',
1005
+ ' "isolatedModules": true,',
1006
+ ' "jsx": "react-jsx",',
1007
+ ' "incremental": true,',
1008
+ ' "paths": {',
1009
+ ' "@/*": ["./src/*"]',
1010
+ " },",
1011
+ ' "plugins": [',
1012
+ ' { "name": "next" }',
1013
+ " ]",
1014
+ " },",
1015
+ ' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],',
1016
+ ' "exclude": ["node_modules"]',
1017
+ "}",
1018
+ ].join("\n"),
1019
+ "src/app/globals.css": [
1020
+ ":root {",
1021
+ " color-scheme: light;",
1022
+ " --background: #f7f8f6;",
1023
+ " --foreground: #181b18;",
1024
+ " --muted: #626a63;",
1025
+ " --border: #d9ded7;",
1026
+ " --surface: #ffffff;",
1027
+ " --accent: #2f6f5d;",
1028
+ "}",
1029
+ "",
1030
+ "* { box-sizing: border-box; }",
1031
+ "html, body { margin: 0; min-height: 100%; }",
1032
+ "body { min-height: 100dvh; background: var(--background); color: var(--foreground); font-family: \"Segoe UI\", sans-serif; }",
1033
+ "button, input { font: inherit; }",
1034
+ "main { width: min(980px, calc(100% - 40px)); margin: 0 auto; padding: 48px 0; }",
1035
+ ".shell { display: grid; gap: 24px; }",
1036
+ ".eyebrow { color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }",
1037
+ "h1 { max-width: 720px; margin: 0; font-size: clamp(2.4rem, 6vw, 4.8rem); letter-spacing: -0.05em; line-height: 0.96; }",
1038
+ "p { max-width: 64ch; margin: 0; color: var(--muted); line-height: 1.65; }",
1039
+ ".workspace { display: grid; gap: 14px; border: 1px solid var(--border); background: var(--surface); padding: 22px; }",
1040
+ ".status-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--border); }",
1041
+ ".status-grid div { display: grid; gap: 6px; padding: 14px; border-right: 1px solid var(--border); }",
1042
+ ".status-grid div:last-child { border-right: 0; }",
1043
+ ".status-grid span { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }",
1044
+ ".status-grid strong { font-size: 1.4rem; }",
1045
+ ".next-steps { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }",
1046
+ ".next-steps li { border-top: 1px solid var(--border); padding-top: 10px; color: var(--muted); }",
1047
+ "code { font-family: \"Cascadia Code\", monospace; }",
1048
+ "@media (max-width: 720px) { main { width: min(100% - 28px, 980px); } .status-grid { grid-template-columns: 1fr; } .status-grid div { border-right: 0; border-bottom: 1px solid var(--border); } }",
1049
+ ].join("\n"),
1050
+ "src/app/layout.tsx": [
1051
+ 'import "./globals.css";',
1052
+ 'import type { ReactNode } from "react";',
1053
+ "",
1054
+ "export const metadata = {",
1055
+ ' title: "Ekairos App",',
1056
+ ' description: "Scaffolded Ekairos app",',
1057
+ "};",
1058
+ "",
1059
+ "export default function RootLayout({ children }: { children: ReactNode }) {",
1060
+ " return (",
1061
+ ' <html lang="en">',
1062
+ " <body>{children}</body>",
1063
+ " </html>",
1064
+ " );",
1065
+ "}",
1066
+ ].join("\n"),
1067
+ "src/app/page.tsx": [
1068
+ 'import DomainWorkbench from "./domain-workbench";',
1069
+ "",
1070
+ 'export const dynamic = "force-dynamic";',
1071
+ "",
1072
+ "export default function HomePage() {",
1073
+ " return (",
1074
+ " <main>",
1075
+ ' <section className="shell">',
1076
+ ' <div className="eyebrow">Ekairos App</div>',
1077
+ " <h1>Empty app. Runtime ready.</h1>",
1078
+ " <p>",
1079
+ " Start here when you want a clean app with the Ekairos runtime,",
1080
+ " domain endpoint, Instant configuration, and a place to add your first domain.",
1081
+ " </p>",
1082
+ " <DomainWorkbench />",
1083
+ " </section>",
1084
+ " </main>",
1085
+ " );",
1086
+ "}",
1087
+ ].join("\n"),
1088
+ "src/app/domain-workbench.tsx": [
1089
+ "export default function DomainWorkbench() {",
1090
+ " return (",
1091
+ ' <section className="workspace">',
1092
+ ' <div className="status-grid">',
1093
+ " <div><span>Runtime</span><strong>Ready</strong></div>",
1094
+ " <div><span>Endpoint</span><strong>/api</strong></div>",
1095
+ " <div><span>Domains</span><strong>0</strong></div>",
1096
+ " </div>",
1097
+ " <p>Add your first domain in <code>src/domain.ts</code>. Keep writes behind typed domain actions.</p>",
1098
+ ' <ul className="next-steps">',
1099
+ " <li>Name the domain in camelCase.</li>",
1100
+ " <li>Name entities as <code>{\"<domainName>_<entityName>\"}</code>.</li>",
1101
+ " <li>Expose the first write as a <code>defineAction</code>.</li>",
1102
+ " </ul>",
1103
+ " </section>",
1104
+ " );",
1105
+ "}",
1106
+ ].join("\n"),
1107
+ "src/domain.ts": [
1108
+ 'import { domain } from "@ekairos/domain";',
1109
+ "",
1110
+ "const baseDomain = domain(\"app\").withSchema({",
1111
+ " entities: {},",
1112
+ " links: {},",
1113
+ " rooms: {},",
1114
+ "});",
1115
+ "",
1116
+ "export const appDomain = baseDomain.withActions({});",
1117
+ "",
1118
+ "export default appDomain;",
1119
+ ].join("\n"),
1120
+ "src/runtime.ts": [
1121
+ 'import { init } from "@instantdb/admin";',
1122
+ 'import { EkairosRuntime } from "@ekairos/domain/runtime-handle";',
1123
+ 'import { configureRuntime } from "@ekairos/domain/runtime";',
1124
+ 'import appDomain from "./domain";',
1125
+ "",
1126
+ "export type AppRuntimeEnv = {",
1127
+ " actorEmail?: string | null;",
1128
+ " actorId?: string;",
1129
+ " adminToken?: string;",
1130
+ " appId?: string;",
1131
+ "};",
1132
+ "",
1133
+ "function resolveRuntimeEnv(env: AppRuntimeEnv = {}): Required<Pick<AppRuntimeEnv, \"appId\" | \"adminToken\">> & AppRuntimeEnv {",
1134
+ ' const appId = String(env.appId ?? process.env.NEXT_PUBLIC_INSTANT_APP_ID ?? "").trim();',
1135
+ ' const adminToken = String(env.adminToken ?? process.env.INSTANT_ADMIN_TOKEN ?? "").trim();',
1136
+ " if (!appId || !adminToken) {",
1137
+ ' throw new Error("Missing NEXT_PUBLIC_INSTANT_APP_ID or INSTANT_ADMIN_TOKEN. Copy .env.example to .env.local and fill both values.");',
1138
+ " }",
1139
+ " return { ...env, appId, adminToken };",
1140
+ "}",
1141
+ "",
1142
+ "export class AppRuntime extends EkairosRuntime<AppRuntimeEnv, typeof appDomain, any> {",
1143
+ " protected getDomain() { return appDomain; }",
1144
+ " protected async resolveDb(env: AppRuntimeEnv) {",
1145
+ " const resolved = resolveRuntimeEnv(env);",
1146
+ " return init({",
1147
+ " appId: resolved.appId,",
1148
+ " adminToken: resolved.adminToken,",
1149
+ " schema: appDomain.toInstantSchema(),",
1150
+ " useDateObjects: true,",
1151
+ " } as any) as any;",
1152
+ " }",
1153
+ "}",
1154
+ "",
1155
+ "export function createRuntime(env: AppRuntimeEnv = {}) {",
1156
+ " return new AppRuntime(resolveRuntimeEnv(env));",
1157
+ "}",
1158
+ "",
1159
+ "export const runtimeConfig = configureRuntime<AppRuntimeEnv>({",
1160
+ " runtime: async (env) => {",
1161
+ " const runtime = createRuntime(env);",
1162
+ " return { db: await runtime.db() };",
1163
+ " },",
1164
+ " domain: { domain: appDomain },",
1165
+ "});",
1166
+ ].join("\n"),
1167
+ "src/workflows/demo.workflow.ts": [
1168
+ "export type DemoWorkflowInput = Record<string, never>;",
1169
+ "",
1170
+ "export async function runDemoWorkflow(_input: DemoWorkflowInput) {",
1171
+ ' "use workflow";',
1172
+ " return { ok: true };",
1173
+ "}",
1174
+ ].join("\n"),
1175
+ };
1176
+ }
1177
+ if (params.template === "agent") {
1178
+ return {
1179
+ ".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
1180
+ ".env.example": [
1181
+ "NEXT_PUBLIC_INSTANT_APP_ID=",
1182
+ "INSTANT_ADMIN_TOKEN=",
1183
+ "",
1184
+ "# Optional: use this only while provisioning new apps with the CLI.",
1185
+ "INSTANT_PERSONAL_ACCESS_TOKEN=",
1186
+ ].join("\n"),
1187
+ "DOMAIN.md": [
1188
+ "# Ekairos Agent Template",
1189
+ "",
1190
+ "This app is the minimal loop for Ekairos event contexts:",
1191
+ "",
1192
+ "1. create the app",
1193
+ "2. start an agent context through `/api/agent/react`",
1194
+ "3. open the Instant app in Workbench v2",
1195
+ "4. iterate on `src/agent.ts`",
1196
+ "",
1197
+ "The scaffold uses the canonical `@ekairos/events` schema so Workbench v2 can inspect `event_contexts`.",
1198
+ "It includes an Ekairos registry alias so domain UI components can be installed with shadcn and use `@ekairos/events` directly.",
1199
+ ].join("\n"),
1200
+ "components.json": [
1201
+ "{",
1202
+ ' "$schema": "https://ui.shadcn.com/schema.json",',
1203
+ ' "style": "default",',
1204
+ ' "rsc": false,',
1205
+ ' "tsx": true,',
1206
+ ' "tailwind": {',
1207
+ ' "config": "",',
1208
+ ' "css": "src/app/globals.css",',
1209
+ ' "baseColor": "zinc",',
1210
+ ' "cssVariables": true,',
1211
+ ' "prefix": ""',
1212
+ " },",
1213
+ ' "iconLibrary": "lucide",',
1214
+ ' "aliases": {',
1215
+ ' "components": "@/components",',
1216
+ ' "utils": "@/lib/utils",',
1217
+ ' "ui": "@/components/ui",',
1218
+ ' "lib": "@/lib",',
1219
+ ' "hooks": "@/hooks"',
1220
+ " },",
1221
+ ' "registries": {',
1222
+ ' "@ekairos": "https://registry.ekairos.dev/r/{name}.json"',
1223
+ " }",
1224
+ "}",
1225
+ ].join("\n"),
1226
+ "instant.schema.ts": [
1227
+ 'import { eventsDomain } from "@ekairos/events/schema";',
1228
+ "",
1229
+ "const schema = eventsDomain.toInstantSchema();",
1230
+ "",
1231
+ "export default schema;",
1232
+ ].join("\n"),
1233
+ "next-env.d.ts": [
1234
+ '/// <reference types="next" />',
1235
+ '/// <reference types="next/image-types/global" />',
1236
+ "",
1237
+ "// This file is managed by Next.js.",
1238
+ ].join("\n"),
1239
+ "next.config.ts": [
1240
+ 'import type { NextConfig } from "next";',
1241
+ 'import { withWorkflow } from "workflow/next";',
1242
+ "",
1243
+ "const nextConfig: NextConfig = {",
1244
+ " transpilePackages: [\"@ekairos/domain\", \"@ekairos/events\"],",
1245
+ "};",
1246
+ "",
1247
+ "export default withWorkflow(nextConfig) as NextConfig;",
1248
+ ].join("\n"),
1249
+ "package.json": `${JSON.stringify(packageJson, null, 2)}\n`,
1250
+ "tsconfig.json": [
1251
+ "{",
1252
+ ' "compilerOptions": {',
1253
+ ' "target": "ES2022",',
1254
+ ' "lib": ["dom", "dom.iterable", "es2022"],',
1255
+ ' "allowJs": false,',
1256
+ ' "skipLibCheck": true,',
1257
+ ' "strict": true,',
1258
+ ' "noEmit": true,',
1259
+ ' "esModuleInterop": true,',
1260
+ ' "module": "esnext",',
1261
+ ' "moduleResolution": "bundler",',
1262
+ ' "resolveJsonModule": true,',
1263
+ ' "isolatedModules": true,',
1264
+ ' "jsx": "react-jsx",',
1265
+ ' "incremental": true,',
1266
+ ' "paths": {',
1267
+ ' "@/*": ["./src/*"]',
1268
+ " },",
1269
+ ' "plugins": [',
1270
+ ' { "name": "next" }',
1271
+ " ]",
1272
+ " },",
1273
+ ' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],',
1274
+ ' "exclude": ["node_modules"]',
1275
+ "}",
1276
+ ].join("\n"),
1277
+ "src/app/globals.css": [
1278
+ ":root {",
1279
+ " color-scheme: light;",
1280
+ " --background: #f7f8f6;",
1281
+ " --foreground: #171a18;",
1282
+ " --muted: #626a63;",
1283
+ " --border: #d9ded7;",
1284
+ " --surface: #ffffff;",
1285
+ " --accent: #26715f;",
1286
+ "}",
1287
+ "* { box-sizing: border-box; }",
1288
+ "html, body { margin: 0; min-height: 100%; }",
1289
+ "body { min-height: 100dvh; background: var(--background); color: var(--foreground); font-family: \"Segoe UI\", sans-serif; }",
1290
+ "button, textarea, input { font: inherit; }",
1291
+ "main { width: min(980px, calc(100% - 32px)); margin: 0 auto; padding: 44px 0; }",
1292
+ ".shell { display: grid; gap: 22px; }",
1293
+ ".eyebrow { color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }",
1294
+ "h1 { max-width: 760px; margin: 0; font-size: clamp(2.1rem, 5vw, 4.4rem); line-height: 1; }",
1295
+ "p { max-width: 68ch; margin: 0; color: var(--muted); line-height: 1.65; }",
1296
+ ".panel { display: grid; gap: 16px; border: 1px solid var(--border); background: var(--surface); padding: 20px; }",
1297
+ ".grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--border); }",
1298
+ ".grid div { display: grid; gap: 6px; padding: 14px; border-right: 1px solid var(--border); }",
1299
+ ".grid div:last-child { border-right: 0; }",
1300
+ ".grid span { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }",
1301
+ ".grid strong { font-size: 1rem; overflow-wrap: anywhere; }",
1302
+ "textarea { width: 100%; min-height: 120px; resize: vertical; border: 1px solid var(--border); padding: 12px; }",
1303
+ "button { width: fit-content; border: 0; background: var(--accent); color: white; padding: 10px 14px; font-weight: 700; cursor: pointer; }",
1304
+ "button:disabled { cursor: wait; opacity: 0.65; }",
1305
+ "pre { overflow: auto; margin: 0; border: 1px solid var(--border); background: #f2f4f1; padding: 14px; }",
1306
+ "code { font-family: \"Cascadia Code\", monospace; }",
1307
+ "@media (max-width: 720px) { .grid { grid-template-columns: 1fr; } .grid div { border-right: 0; border-bottom: 1px solid var(--border); } }",
1308
+ ].join("\n"),
1309
+ "src/app/layout.tsx": [
1310
+ 'import "./globals.css";',
1311
+ 'import type { ReactNode } from "react";',
1312
+ "",
1313
+ "export const metadata = {",
1314
+ ' title: "Ekairos Agent",',
1315
+ ' description: "Scaffolded Ekairos agent context app",',
1316
+ "};",
1317
+ "",
1318
+ "export default function RootLayout({ children }: { children: ReactNode }) {",
1319
+ " return (",
1320
+ ' <html lang="en">',
1321
+ " <body>{children}</body>",
1322
+ " </html>",
1323
+ " );",
1324
+ "}",
1325
+ ].join("\n"),
1326
+ "src/app/page.tsx": [
1327
+ 'import AgentWorkbench from "./agent-workbench";',
1328
+ "",
1329
+ 'export const dynamic = "force-dynamic";',
1330
+ "",
1331
+ "export default function HomePage() {",
1332
+ " return (",
1333
+ " <main>",
1334
+ ' <section className="shell">',
1335
+ ' <div className="eyebrow">Ekairos Agent Template</div>',
1336
+ " <h1>Start a context, inspect it in Workbench v2, iterate.</h1>",
1337
+ " <p>",
1338
+ " This template writes canonical @ekairos/events rows and returns the context id immediately.",
1339
+ " </p>",
1340
+ " <AgentWorkbench />",
1341
+ " </section>",
1342
+ " </main>",
1343
+ " );",
1344
+ "}",
1345
+ ].join("\n"),
1346
+ "src/app/agent-workbench.tsx": [
1347
+ '"use client";',
1348
+ "",
1349
+ "import { useMemo, useState } from \"react\";",
1350
+ "",
1351
+ "type AgentResult = {",
1352
+ " ok: boolean;",
1353
+ " appId?: string;",
1354
+ " contextId?: string;",
1355
+ " contextKey?: string | null;",
1356
+ " triggerEventId?: string;",
1357
+ " error?: string;",
1358
+ "};",
1359
+ "",
1360
+ "export default function AgentWorkbench() {",
1361
+ " const [prompt, setPrompt] = useState(\"Create a deterministic template context.\");",
1362
+ " const [result, setResult] = useState<AgentResult | null>(null);",
1363
+ " const [pending, setPending] = useState(false);",
1364
+ "",
1365
+ " const workbenchPath = useMemo(() => {",
1366
+ " if (!result?.appId || !result?.contextId) return \"\";",
1367
+ " return `/app/${result.appId}/contexts/${result.contextId}`;",
1368
+ " }, [result]);",
1369
+ "",
1370
+ " async function startContext() {",
1371
+ " setPending(true);",
1372
+ " setResult(null);",
1373
+ " try {",
1374
+ " const response = await fetch(\"/api/agent/react\", {",
1375
+ " method: \"POST\",",
1376
+ " headers: { \"content-type\": \"application/json\" },",
1377
+ " body: JSON.stringify({ prompt }),",
1378
+ " });",
1379
+ " const data = (await response.json().catch(() => ({}))) as AgentResult;",
1380
+ " setResult(response.ok ? data : { ok: false, error: data.error || response.statusText });",
1381
+ " } catch (error) {",
1382
+ " setResult({ ok: false, error: error instanceof Error ? error.message : String(error) });",
1383
+ " } finally {",
1384
+ " setPending(false);",
1385
+ " }",
1386
+ " }",
1387
+ "",
1388
+ " return (",
1389
+ ' <section className="panel">',
1390
+ ' <div className="grid">',
1391
+ " <div><span>Schema</span><strong>@ekairos/events</strong></div>",
1392
+ " <div><span>Route</span><strong>/api/agent/react</strong></div>",
1393
+ " <div><span>Mode</span><strong>scripted reactor</strong></div>",
1394
+ " </div>",
1395
+ " <textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} />",
1396
+ " <button type=\"button\" disabled={pending} onClick={startContext}>",
1397
+ " {pending ? \"Starting context...\" : \"Start context\"}",
1398
+ " </button>",
1399
+ " {workbenchPath ? <p>Workbench v2 path: <code>{workbenchPath}</code></p> : null}",
1400
+ " {result ? <pre>{JSON.stringify(result, null, 2)}</pre> : null}",
1401
+ " </section>",
1402
+ " );",
1403
+ "}",
1404
+ ].join("\n"),
1405
+ "src/app/api/agent/react/route.ts": [
1406
+ 'import { NextRequest, NextResponse } from "next/server";',
1407
+ 'import { agentContext, createTriggerEvent } from "@/agent";',
1408
+ 'import { createRuntime } from "@/runtime";',
1409
+ "",
1410
+ "export const maxDuration = 60;",
1411
+ "",
1412
+ "function readTrimmed(value: unknown) {",
1413
+ " return typeof value === \"string\" ? value.trim() : \"\";",
1414
+ "}",
1415
+ "",
1416
+ "export async function POST(req: NextRequest) {",
1417
+ " try {",
1418
+ " const body = await req.json().catch(() => ({}));",
1419
+ " const prompt = readTrimmed((body as any).prompt) || \"Create a deterministic template context.\";",
1420
+ " const contextKey = readTrimmed((body as any).contextKey) || `agent-template:${Date.now()}`;",
1421
+ " const triggerEvent = createTriggerEvent(prompt);",
1422
+ " const reaction = await agentContext.react(triggerEvent, {",
1423
+ " runtime: createRuntime({ actorId: \"template-user\" }),",
1424
+ " context: { key: contextKey },",
1425
+ " durable: false,",
1426
+ " });",
1427
+ " if (reaction.run) await reaction.run;",
1428
+ " return NextResponse.json({",
1429
+ " ok: true,",
1430
+ " appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID || null,",
1431
+ " contextId: String(reaction.context.id || \"\"),",
1432
+ " contextKey: reaction.context.key ?? contextKey,",
1433
+ " triggerEventId: triggerEvent.id,",
1434
+ " });",
1435
+ " } catch (error) {",
1436
+ " return NextResponse.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, { status: 500 });",
1437
+ " }",
1438
+ "}",
1439
+ ].join("\n"),
1440
+ "src/agent.ts": [
1441
+ "import {",
1442
+ " createContext,",
1443
+ " createScriptedReactor,",
1444
+ " defineAction,",
1445
+ " eventsDomain,",
1446
+ " type ContextItem,",
1447
+ "} from \"@ekairos/events\";",
1448
+ "import { z } from \"zod\";",
1449
+ "",
1450
+ "export type AgentEnv = {",
1451
+ " actorId?: string;",
1452
+ "};",
1453
+ "",
1454
+ "const createMessageInput = z.object({",
1455
+ " message: z.string(),",
1456
+ " marker: z.string().optional(),",
1457
+ "});",
1458
+ "",
1459
+ "const createMessageOutput = z.object({",
1460
+ " message: z.string(),",
1461
+ " marker: z.string(),",
1462
+ "});",
1463
+ "",
1464
+ "async function createMessage(input: z.infer<typeof createMessageInput>) {",
1465
+ " \"use step\";",
1466
+ " return {",
1467
+ " message: input.message,",
1468
+ " marker: input.marker ?? \"AGENT_TEMPLATE_OK\",",
1469
+ " };",
1470
+ "}",
1471
+ "",
1472
+ "function randomId() {",
1473
+ " return globalThis.crypto?.randomUUID?.() ?? `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;",
1474
+ "}",
1475
+ "",
1476
+ "export function createTriggerEvent(text: string): ContextItem {",
1477
+ " return {",
1478
+ " id: randomId(),",
1479
+ " type: \"user_message_created\",",
1480
+ " channel: \"web\",",
1481
+ " createdAt: new Date().toISOString(),",
1482
+ " content: { parts: [{ type: \"text\", text }] },",
1483
+ " } as any;",
1484
+ "}",
1485
+ "",
1486
+ "export const agentContext = createContext<AgentEnv>(\"agent.template\")",
1487
+ " .context((stored, env) => ({ ...(stored.content ?? {}), actorId: env.actorId ?? null }))",
1488
+ " .narrative(() => \"You are the minimal Ekairos agent template. Create one deterministic message.\")",
1489
+ " .actions(() => ({",
1490
+ " createMessage: defineAction<any, any, Record<string, never>, AgentEnv, typeof eventsDomain>({",
1491
+ " description: \"Create the deterministic template message.\",",
1492
+ " input: createMessageInput as any,",
1493
+ " output: createMessageOutput as any,",
1494
+ " execute: async ({ input }) => await createMessage(input),",
1495
+ " }),",
1496
+ " }))",
1497
+ " .reactor(",
1498
+ " createScriptedReactor({",
1499
+ " steps: [",
1500
+ " {",
1501
+ " assistantEvent: {",
1502
+ " content: {",
1503
+ " parts: [",
1504
+ " { type: \"text\", text: \"AGENT_TEMPLATE_OK context ready.\" },",
1505
+ " {",
1506
+ " type: \"tool-createMessage\",",
1507
+ " toolCallId: \"agent-template-create-message\",",
1508
+ " input: { message: \"AGENT_TEMPLATE_OK context ready.\", marker: \"AGENT_TEMPLATE_OK\" },",
1509
+ " },",
1510
+ " ],",
1511
+ " },",
1512
+ " },",
1513
+ " actionRequests: [",
1514
+ " {",
1515
+ " actionRef: \"agent-template-create-message\",",
1516
+ " actionName: \"createMessage\",",
1517
+ " input: { message: \"AGENT_TEMPLATE_OK context ready.\", marker: \"AGENT_TEMPLATE_OK\" },",
1518
+ " },",
1519
+ " ],",
1520
+ " messagesForModel: [],",
1521
+ " llm: { provider: \"scripted\", model: \"agent-template\" },",
1522
+ " },",
1523
+ " ],",
1524
+ " repeatLast: true,",
1525
+ " }),",
1526
+ " )",
1527
+ " .shouldContinue(() => false)",
1528
+ " .build();",
1529
+ ].join("\n"),
1530
+ "src/runtime.ts": [
1531
+ 'import { init } from "@instantdb/admin";',
1532
+ 'import { EkairosRuntime } from "@ekairos/domain/runtime-handle";',
1533
+ 'import { eventsDomain } from "@ekairos/events/schema";',
1534
+ "",
1535
+ "export type AgentRuntimeEnv = {",
1536
+ " actorId?: string | null;",
1537
+ " adminToken?: string;",
1538
+ " appId?: string;",
1539
+ "};",
1540
+ "",
1541
+ "function resolveRuntimeEnv(env: AgentRuntimeEnv = {}): Required<Pick<AgentRuntimeEnv, \"appId\" | \"adminToken\">> & AgentRuntimeEnv {",
1542
+ " const appId = String(env.appId ?? process.env.NEXT_PUBLIC_INSTANT_APP_ID ?? \"\").trim();",
1543
+ " const adminToken = String(env.adminToken ?? process.env.INSTANT_ADMIN_TOKEN ?? \"\").trim();",
1544
+ " if (!appId || !adminToken) {",
1545
+ " throw new Error(\"Missing NEXT_PUBLIC_INSTANT_APP_ID or INSTANT_ADMIN_TOKEN. Copy .env.example to .env.local and fill both values.\");",
1546
+ " }",
1547
+ " return { ...env, appId, adminToken };",
1548
+ "}",
1549
+ "",
1550
+ "export class AgentRuntime extends EkairosRuntime<AgentRuntimeEnv, typeof eventsDomain, any> {",
1551
+ " protected getDomain() { return eventsDomain; }",
1552
+ " protected async resolveDb(env: AgentRuntimeEnv) {",
1553
+ " const resolved = resolveRuntimeEnv(env);",
1554
+ " return init({",
1555
+ " appId: resolved.appId,",
1556
+ " adminToken: resolved.adminToken,",
1557
+ " schema: eventsDomain.toInstantSchema(),",
1558
+ " useDateObjects: true,",
1559
+ " } as any) as any;",
1560
+ " }",
1561
+ "}",
1562
+ "",
1563
+ "export function createRuntime(env: AgentRuntimeEnv = {}) {",
1564
+ " return new AgentRuntime(resolveRuntimeEnv(env));",
1565
+ "}",
1566
+ ].join("\n"),
1567
+ };
1568
+ }
268
1569
  return {
269
1570
  ".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
270
1571
  ".env.example": [
@@ -275,18 +1576,18 @@ function buildNextTemplateFiles(params) {
275
1576
  "INSTANT_PERSONAL_ACCESS_TOKEN=",
276
1577
  ].join("\n"),
277
1578
  "DOMAIN.md": [
278
- "# Ekairos App Domain",
1579
+ "# Ekairos Supply Chain Domain",
279
1580
  "",
280
- "This scaffold ships a small task domain and a live domain showcase UI:",
281
- "- inspect the domain through the well-known endpoint",
282
- "- fetch the manifest and data directly from the app UI",
283
- "- create tasks and seed demo data through domain actions",
284
- "- query nested data through InstaQL",
1581
+ "This scaffold ships a supply-chain control tower backed by separate domains:",
1582
+ "- `supplierNetwork` owns supplier risk and score",
1583
+ "- `procurement` owns purchase orders",
1584
+ "- `inventory` owns stock position",
1585
+ "- `transportation` owns shipments and ETA",
1586
+ "- `qualityControl` owns arrival inspection",
285
1587
  "",
286
1588
  "Actions:",
287
- "- `createTask` -> create one task",
288
- "- `addTaskComment` -> attach one comment to a task",
289
- "- `seedDemo` -> create demo data fast",
1589
+ "- `launchOrder` -> creates and links supplier, order, stock, shipment, and inspection",
1590
+ "- `expediteShipment` -> updates shipment status and ETA",
290
1591
  ].join("\n"),
291
1592
  "instant.schema.ts": [
292
1593
  'import appDomain from "./src/domain";',
@@ -334,29 +1635,32 @@ function buildNextTemplateFiles(params) {
334
1635
  ' "moduleResolution": "bundler",',
335
1636
  ' "resolveJsonModule": true,',
336
1637
  ' "isolatedModules": true,',
337
- ' "jsx": "preserve",',
1638
+ ' "jsx": "react-jsx",',
338
1639
  ' "incremental": true,',
339
- ' "baseUrl": ".",',
340
1640
  ' "paths": {',
341
1641
  ' "@/*": ["./src/*"]',
342
- " }",
1642
+ " },",
1643
+ ' "plugins": [',
1644
+ ' { "name": "next" }',
1645
+ " ]",
343
1646
  " },",
344
- ' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],',
1647
+ ' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],',
345
1648
  ' "exclude": ["node_modules"]',
346
1649
  "}",
347
1650
  ].join("\n"),
348
1651
  "src/app/globals.css": [
349
1652
  ":root {",
350
1653
  " color-scheme: light;",
351
- " --bg: #efe7da;",
352
- " --panel: #fffdf7;",
353
- " --panel-strong: #fff8ec;",
354
- " --ink: #1d1b19;",
355
- " --muted: #60584d;",
356
- " --accent: #0f766e;",
357
- " --accent-soft: #d9f3ef;",
358
- " --border: #d7cebf;",
359
- " --danger: #b42318;",
1654
+ " --bg: #f7f8f6;",
1655
+ " --surface: #ffffff;",
1656
+ " --surface-2: #f0f2ef;",
1657
+ " --ink: #171a18;",
1658
+ " --muted: #646b66;",
1659
+ " --accent: #26715f;",
1660
+ " --accent-soft: #dfece7;",
1661
+ " --border: #d9ded9;",
1662
+ " --line: #b9c1ba;",
1663
+ " --danger: #9f2f28;",
360
1664
  "}",
361
1665
  "",
362
1666
  "* {",
@@ -367,15 +1671,13 @@ function buildNextTemplateFiles(params) {
367
1671
  "body {",
368
1672
  " margin: 0;",
369
1673
  " min-height: 100%;",
370
- " background:",
371
- " radial-gradient(circle at top, #fff8ee 0%, rgba(255, 248, 238, 0.7) 28%, transparent 65%),",
372
- " linear-gradient(180deg, #f7f0e4 0%, var(--bg) 100%);",
1674
+ " background: var(--bg);",
373
1675
  " color: var(--ink);",
374
- ' font-family: "Segoe UI", sans-serif;',
1676
+ " font-family: \"Geist\", \"Aptos\", \"Segoe UI\", sans-serif;",
375
1677
  "}",
376
1678
  "",
377
1679
  "body {",
378
- " min-height: 100vh;",
1680
+ " min-height: 100dvh;",
379
1681
  "}",
380
1682
  "",
381
1683
  "button,",
@@ -384,242 +1686,360 @@ function buildNextTemplateFiles(params) {
384
1686
  "}",
385
1687
  "",
386
1688
  "main {",
387
- " max-width: 1180px;",
1689
+ " width: min(1240px, calc(100% - 40px));",
388
1690
  " margin: 0 auto;",
389
- " padding: 48px 24px 72px;",
1691
+ " padding: 44px 0 64px;",
390
1692
  "}",
391
1693
  "",
392
1694
  ".hero {",
393
1695
  " display: grid;",
394
- " gap: 18px;",
395
- " max-width: 860px;",
1696
+ " gap: 14px;",
1697
+ " max-width: 720px;",
1698
+ " margin-bottom: 28px;",
396
1699
  "}",
397
1700
  "",
398
1701
  ".eyebrow {",
399
1702
  " color: var(--accent);",
400
- " font-size: 12px;",
401
- " font-weight: 700;",
402
- " letter-spacing: 0.2em;",
1703
+ " font-size: 11px;",
1704
+ " font-weight: 800;",
1705
+ " letter-spacing: 0.16em;",
403
1706
  " text-transform: uppercase;",
404
1707
  "}",
405
1708
  "",
406
1709
  "h1,",
407
1710
  "h2 {",
408
1711
  " margin: 0;",
409
- " line-height: 0.96;",
1712
+ " letter-spacing: -0.04em;",
410
1713
  "}",
411
1714
  "",
412
1715
  "h1 {",
413
- " font-size: clamp(2.7rem, 6vw, 5.4rem);",
1716
+ " max-width: 640px;",
1717
+ " font-size: clamp(2.4rem, 5.4vw, 4.8rem);",
1718
+ " line-height: 0.96;",
414
1719
  "}",
415
1720
  "",
416
1721
  "h2 {",
417
- " font-size: 1.4rem;",
1722
+ " font-size: 1.28rem;",
1723
+ " line-height: 1.05;",
418
1724
  "}",
419
1725
  "",
420
1726
  "p {",
421
1727
  " margin: 0;",
422
1728
  " color: var(--muted);",
423
- " line-height: 1.65;",
1729
+ " line-height: 1.6;",
424
1730
  "}",
425
1731
  "",
426
1732
  ".shell {",
427
1733
  " display: grid;",
428
- " gap: 20px;",
429
- " margin-top: 32px;",
1734
+ " gap: 14px;",
430
1735
  "}",
431
1736
  "",
432
- ".stat-grid {",
1737
+ ".metrics-strip {",
433
1738
  " display: grid;",
434
- " gap: 16px;",
435
- " grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));",
1739
+ " grid-template-columns: repeat(4, minmax(0, 1fr));",
1740
+ " border: 1px solid var(--border);",
1741
+ " background: var(--surface);",
436
1742
  "}",
437
1743
  "",
438
- ".grid {",
1744
+ ".metrics-strip div {",
439
1745
  " display: grid;",
440
- " gap: 20px;",
1746
+ " gap: 6px;",
1747
+ " padding: 16px;",
1748
+ " border-right: 1px solid var(--border);",
441
1749
  "}",
442
1750
  "",
443
- ".showcase-grid {",
444
- " align-items: start;",
1751
+ ".metrics-strip div:last-child {",
1752
+ " border-right: 0;",
445
1753
  "}",
446
1754
  "",
447
- ".panel-tall,",
448
- ".panel-wide,",
449
- ".stat-card {",
450
- " min-height: 100%;",
1755
+ ".metrics-strip span,",
1756
+ ".manifest-label {",
1757
+ " color: var(--muted);",
1758
+ " font-size: 11px;",
1759
+ " font-weight: 800;",
1760
+ " letter-spacing: 0.12em;",
1761
+ " text-transform: uppercase;",
451
1762
  "}",
452
1763
  "",
453
- ".card {",
454
- " background: linear-gradient(180deg, var(--panel) 0%, var(--panel-strong) 100%);",
1764
+ ".metrics-strip strong {",
1765
+ " font-size: 1.85rem;",
1766
+ " letter-spacing: -0.04em;",
1767
+ " line-height: 1;",
1768
+ "}",
1769
+ "",
1770
+ ".workbench {",
1771
+ " display: grid;",
1772
+ " grid-template-columns: 0.8fr 1fr;",
1773
+ " gap: 14px;",
1774
+ " align-items: start;",
1775
+ "}",
1776
+ "",
1777
+ ".context-rail,",
1778
+ ".command-panel,",
1779
+ ".graph-panel {",
455
1780
  " border: 1px solid var(--border);",
456
- " border-radius: 24px;",
457
- " padding: 22px;",
458
- " box-shadow: 0 18px 45px rgba(23, 23, 23, 0.06);",
1781
+ " background: var(--surface);",
459
1782
  "}",
460
1783
  "",
461
- ".stat-card strong {",
462
- " display: block;",
463
- " margin-top: 8px;",
464
- " font-size: 2rem;",
1784
+ ".context-rail,",
1785
+ ".command-panel {",
1786
+ " padding: 18px;",
1787
+ "}",
1788
+ "",
1789
+ ".graph-panel {",
1790
+ " grid-column: 1 / -1;",
1791
+ " padding: 18px;",
465
1792
  "}",
466
1793
  "",
467
1794
  ".panel-head {",
468
1795
  " display: flex;",
469
- " align-items: flex-start;",
470
1796
  " justify-content: space-between;",
471
1797
  " gap: 16px;",
472
- " margin-bottom: 12px;",
1798
+ " align-items: start;",
1799
+ " margin-bottom: 16px;",
473
1800
  "}",
474
1801
  "",
475
- ".manifest-list,",
476
- ".action-list,",
477
- ".task-list,",
478
- ".comment-list {",
1802
+ ".context-list,",
1803
+ ".project-list,",
1804
+ ".task-lines,",
1805
+ ".lane-grid,",
1806
+ ".calls-feed,",
1807
+ ".button-row {",
479
1808
  " display: grid;",
480
- " gap: 12px;",
1809
+ " gap: 10px;",
481
1810
  "}",
482
1811
  "",
483
- ".manifest-list {",
484
- " margin: 18px 0;",
1812
+ ".context-row {",
1813
+ " display: grid;",
1814
+ " grid-template-columns: 0.7fr 1fr;",
1815
+ " gap: 14px;",
1816
+ " padding: 12px 0;",
1817
+ " border-top: 1px solid var(--border);",
1818
+ " opacity: 0;",
1819
+ " transform: translateY(8px);",
1820
+ " animation: lift-in 420ms cubic-bezier(0.16, 1, 0.3, 1) forwards;",
1821
+ " animation-delay: calc(var(--index) * 70ms);",
485
1822
  "}",
486
1823
  "",
487
- ".manifest-list > div {",
488
- " display: flex;",
489
- " justify-content: space-between;",
490
- " gap: 16px;",
491
- " border-bottom: 1px dashed var(--border);",
492
- " padding-bottom: 10px;",
1824
+ ".context-row span {",
1825
+ " font-weight: 800;",
493
1826
  "}",
494
1827
  "",
495
- ".manifest-label {",
1828
+ ".context-row strong {",
496
1829
  " color: var(--muted);",
497
- " font-weight: 600;",
1830
+ " font-size: 0.92rem;",
1831
+ " font-weight: 500;",
498
1832
  "}",
499
1833
  "",
500
1834
  ".field {",
501
1835
  " display: grid;",
502
- " gap: 8px;",
503
- " margin: 18px 0;",
1836
+ " gap: 7px;",
1837
+ " margin: 14px 0;",
504
1838
  "}",
505
1839
  "",
506
1840
  ".field span {",
507
- " font-size: 0.92rem;",
508
- " font-weight: 600;",
1841
+ " color: var(--muted);",
1842
+ " font-size: 0.85rem;",
1843
+ " font-weight: 700;",
509
1844
  "}",
510
1845
  "",
511
1846
  ".input {",
512
1847
  " width: 100%;",
513
1848
  " border: 1px solid var(--border);",
514
- " border-radius: 16px;",
515
- " padding: 14px 16px;",
516
- " background: #fff;",
1849
+ " border-radius: 6px;",
1850
+ " padding: 12px 13px;",
1851
+ " background: var(--surface-2);",
517
1852
  " color: var(--ink);",
1853
+ " outline: none;",
1854
+ "}",
1855
+ "",
1856
+ ".input:focus {",
1857
+ " border-color: var(--accent);",
1858
+ " background: var(--surface);",
518
1859
  "}",
519
1860
  "",
520
1861
  ".button-row {",
521
- " display: flex;",
522
- " flex-wrap: wrap;",
523
- " gap: 12px;",
524
- " margin-bottom: 18px;",
1862
+ " grid-template-columns: repeat(2, minmax(0, 1fr));",
1863
+ " margin-top: 16px;",
525
1864
  "}",
526
1865
  "",
527
1866
  ".button {",
528
1867
  " appearance: none;",
529
- " border: 0;",
530
- " border-radius: 999px;",
531
- " padding: 12px 18px;",
1868
+ " border: 1px solid var(--ink);",
1869
+ " border-radius: 6px;",
1870
+ " padding: 12px 14px;",
532
1871
  " background: var(--ink);",
533
- " color: #fffdf7;",
534
- " display: inline-flex;",
535
- " align-items: center;",
536
- " justify-content: center;",
537
- " gap: 10px;",
1872
+ " color: #ffffff;",
538
1873
  " cursor: pointer;",
539
- " transition: transform 120ms ease, opacity 120ms ease, background 120ms ease;",
1874
+ " font-weight: 800;",
1875
+ " transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1), opacity 160ms ease;",
540
1876
  "}",
541
1877
  "",
542
1878
  ".button:hover:not(:disabled) {",
543
1879
  " transform: translateY(-1px);",
544
1880
  "}",
545
1881
  "",
1882
+ ".button:active:not(:disabled) {",
1883
+ " transform: translateY(1px) scale(0.99);",
1884
+ "}",
1885
+ "",
546
1886
  ".button:disabled {",
547
1887
  " cursor: wait;",
548
- " opacity: 0.72;",
1888
+ " opacity: 0.62;",
549
1889
  "}",
550
1890
  "",
551
1891
  ".button.ghost {",
552
- " background: transparent;",
1892
+ " border-color: var(--border);",
1893
+ " background: var(--surface);",
553
1894
  " color: var(--ink);",
554
- " border: 1px solid var(--border);",
555
- "}",
556
- "",
557
- ".spinner {",
558
- " width: 14px;",
559
- " height: 14px;",
560
- " border-radius: 999px;",
561
- " border: 2px solid rgba(255, 253, 247, 0.35);",
562
- " border-top-color: currentColor;",
563
- " animation: spin 0.8s linear infinite;",
564
- "}",
565
- "",
566
- ".button.ghost .spinner {",
567
- " border-color: rgba(29, 27, 25, 0.18);",
568
- " border-top-color: currentColor;",
569
1895
  "}",
570
1896
  "",
571
1897
  ".status-pill {",
572
1898
  " display: inline-flex;",
573
1899
  " align-items: center;",
574
1900
  " justify-content: center;",
575
- " padding: 6px 10px;",
576
- " border-radius: 999px;",
1901
+ " min-width: 86px;",
1902
+ " padding: 7px 10px;",
1903
+ " border-radius: 6px;",
577
1904
  " background: var(--accent-soft);",
578
1905
  " color: var(--accent);",
579
- " font-size: 12px;",
580
- " font-weight: 700;",
581
- " letter-spacing: 0.04em;",
1906
+ " font-size: 11px;",
1907
+ " font-weight: 800;",
1908
+ " letter-spacing: 0.08em;",
582
1909
  " text-transform: uppercase;",
583
1910
  "}",
584
1911
  "",
585
- ".task-card {",
1912
+ ".project-row {",
586
1913
  " display: grid;",
587
- " gap: 10px;",
588
- " padding: 16px;",
589
- " border-radius: 18px;",
590
- " border: 1px solid var(--border);",
591
- " background: rgba(255, 255, 255, 0.72);",
1914
+ " grid-template-columns: 1fr auto;",
1915
+ " gap: 14px;",
1916
+ " padding: 16px 0;",
1917
+ " border-top: 1px solid var(--border);",
1918
+ "}",
1919
+ "",
1920
+ ".project-row:first-child {",
1921
+ " border-top: 0;",
592
1922
  "}",
593
1923
  "",
594
- ".task-head {",
1924
+ ".project-row strong {",
1925
+ " display: block;",
1926
+ " font-size: 1.05rem;",
1927
+ "}",
1928
+ "",
1929
+ ".project-row span {",
1930
+ " color: var(--muted);",
1931
+ "}",
1932
+ "",
1933
+ ".project-meta {",
595
1934
  " display: flex;",
596
- " align-items: center;",
597
- " justify-content: space-between;",
1935
+ " flex-wrap: wrap;",
1936
+ " gap: 8px;",
1937
+ " justify-content: flex-end;",
1938
+ "}",
1939
+ "",
1940
+ ".project-meta span {",
1941
+ " border: 1px solid var(--border);",
1942
+ " border-radius: 6px;",
1943
+ " padding: 5px 8px;",
1944
+ " color: var(--ink);",
1945
+ " font-size: 0.82rem;",
1946
+ " font-weight: 700;",
1947
+ "}",
1948
+ "",
1949
+ ".task-lines {",
1950
+ " grid-column: 1 / -1;",
1951
+ " padding-top: 2px;",
1952
+ "}",
1953
+ "",
1954
+ ".task-lines p {",
1955
+ " display: grid;",
1956
+ " grid-template-columns: minmax(180px, 0.8fr) minmax(180px, 1fr);",
598
1957
  " gap: 12px;",
1958
+ " padding-left: 14px;",
1959
+ " border-left: 2px solid var(--line);",
599
1960
  "}",
600
1961
  "",
601
- ".comment-item {",
602
- " display: flex;",
603
- " align-items: flex-start;",
604
- " gap: 10px;",
1962
+ ".task-lines b {",
1963
+ " color: var(--ink);",
1964
+ "}",
1965
+ "",
1966
+ ".task-lines em {",
1967
+ " grid-column: 1 / -1;",
605
1968
  " color: var(--muted);",
1969
+ " font-style: normal;",
606
1970
  "}",
607
1971
  "",
608
- ".comment-dot {",
609
- " width: 8px;",
610
- " height: 8px;",
611
- " margin-top: 8px;",
612
- " border-radius: 999px;",
613
- " background: var(--accent);",
614
- " flex: 0 0 auto;",
1972
+ ".raid-summary {",
1973
+ " display: grid;",
1974
+ " grid-template-columns: repeat(3, minmax(0, 1fr));",
1975
+ " border: 1px solid var(--border);",
1976
+ " margin-bottom: 14px;",
615
1977
  "}",
616
1978
  "",
617
- ".action-item {",
1979
+ ".raid-summary div {",
618
1980
  " display: grid;",
619
- " gap: 4px;",
620
- " padding: 12px 14px;",
621
- " border-radius: 16px;",
622
- " background: rgba(15, 118, 110, 0.06);",
1981
+ " gap: 6px;",
1982
+ " padding: 13px;",
1983
+ " border-right: 1px solid var(--border);",
1984
+ "}",
1985
+ "",
1986
+ ".raid-summary div:last-child {",
1987
+ " border-right: 0;",
1988
+ "}",
1989
+ "",
1990
+ ".raid-summary span {",
1991
+ " color: var(--muted);",
1992
+ " font-size: 11px;",
1993
+ " font-weight: 800;",
1994
+ " letter-spacing: 0.12em;",
1995
+ " text-transform: uppercase;",
1996
+ "}",
1997
+ "",
1998
+ ".raid-summary strong {",
1999
+ " font-size: 1rem;",
2000
+ "}",
2001
+ "",
2002
+ ".lane-grid {",
2003
+ " grid-template-columns: repeat(3, minmax(0, 1fr));",
2004
+ "}",
2005
+ "",
2006
+ ".lane {",
2007
+ " min-height: 180px;",
2008
+ " border: 1px solid var(--border);",
2009
+ " padding: 12px;",
2010
+ " background: var(--surface-2);",
2011
+ "}",
2012
+ "",
2013
+ ".objective {",
2014
+ " display: grid;",
2015
+ " gap: 5px;",
2016
+ " margin-top: 10px;",
2017
+ " padding: 10px;",
2018
+ " border: 1px solid var(--border);",
2019
+ " background: var(--surface);",
2020
+ "}",
2021
+ "",
2022
+ ".objective span,",
2023
+ ".objective em {",
2024
+ " color: var(--muted);",
2025
+ " font-size: 0.88rem;",
2026
+ " font-style: normal;",
2027
+ "}",
2028
+ "",
2029
+ ".calls-feed {",
2030
+ " margin-top: 14px;",
2031
+ " padding-top: 14px;",
2032
+ " border-top: 1px solid var(--border);",
2033
+ "}",
2034
+ "",
2035
+ ".calls-feed p {",
2036
+ " display: flex;",
2037
+ " justify-content: space-between;",
2038
+ " gap: 16px;",
2039
+ "}",
2040
+ "",
2041
+ ".calls-feed b {",
2042
+ " color: var(--ink);",
623
2043
  "}",
624
2044
  "",
625
2045
  ".muted {",
@@ -628,59 +2048,100 @@ function buildNextTemplateFiles(params) {
628
2048
  "",
629
2049
  ".empty-state,",
630
2050
  ".error-banner {",
631
- " border-radius: 18px;",
632
- " padding: 16px;",
2051
+ " border: 1px solid var(--border);",
2052
+ " border-radius: 6px;",
2053
+ " padding: 14px;",
633
2054
  "}",
634
2055
  "",
635
2056
  ".empty-state {",
636
- " background: rgba(15, 118, 110, 0.06);",
2057
+ " background: var(--surface-2);",
637
2058
  " color: var(--muted);",
638
2059
  "}",
639
2060
  "",
640
2061
  ".error-banner {",
641
- " background: rgba(180, 35, 24, 0.08);",
2062
+ " background: #fff4f2;",
642
2063
  " color: var(--danger);",
643
- " border: 1px solid rgba(180, 35, 24, 0.16);",
2064
+ " border-color: #efc4bd;",
2065
+ "}",
2066
+ "",
2067
+ ".skeleton-stack {",
2068
+ " display: grid;",
2069
+ " gap: 10px;",
2070
+ "}",
2071
+ "",
2072
+ ".skeleton-stack span {",
2073
+ " display: block;",
2074
+ " height: 54px;",
2075
+ " border-radius: 6px;",
2076
+ " background: linear-gradient(90deg, var(--surface-2), #ffffff, var(--surface-2));",
2077
+ " background-size: 220% 100%;",
2078
+ " animation: shimmer 1.2s ease-in-out infinite;",
644
2079
  "}",
645
2080
  "",
646
2081
  "pre {",
647
2082
  " overflow: auto;",
648
- " border-radius: 16px;",
649
- " padding: 14px;",
650
- " background: #171717;",
651
- " color: #f6f6f6;",
652
- " font-size: 13px;",
653
- " line-height: 1.5;",
2083
+ " margin-top: 14px;",
2084
+ " border-radius: 6px;",
2085
+ " padding: 12px;",
2086
+ " background: #1d211f;",
2087
+ " color: #eef5f1;",
2088
+ " font-size: 12px;",
2089
+ " line-height: 1.45;",
654
2090
  "}",
655
2091
  "",
656
2092
  "code {",
657
- ' font-family: "Cascadia Code", monospace;',
658
- "}",
659
- "",
660
- "@keyframes spin {",
661
- " to { transform: rotate(360deg); }",
2093
+ " font-family: \"Geist Mono\", \"Cascadia Code\", monospace;",
662
2094
  "}",
663
2095
  "",
664
- "@media (min-width: 940px) {",
665
- " .showcase-grid {",
666
- " grid-template-columns: 1.05fr 0.95fr;",
2096
+ "@keyframes lift-in {",
2097
+ " to {",
2098
+ " opacity: 1;",
2099
+ " transform: translateY(0);",
667
2100
  " }",
2101
+ "}",
668
2102
  "",
669
- " .panel-wide {",
670
- " grid-column: 1 / -1;",
2103
+ "@keyframes shimmer {",
2104
+ " to {",
2105
+ " background-position: -220% 0;",
671
2106
  " }",
672
2107
  "}",
673
2108
  "",
674
- "@media (max-width: 720px) {",
2109
+ "@media (max-width: 820px) {",
675
2110
  " main {",
676
- " padding: 32px 16px 56px;",
2111
+ " width: min(100% - 28px, 1240px);",
2112
+ " padding: 32px 0 52px;",
677
2113
  " }",
678
2114
  "",
679
- " .task-head,",
680
- " .manifest-list > div,",
681
- " .panel-head {",
2115
+ " .metrics-strip,",
2116
+ " .workbench,",
2117
+ " .button-row,",
2118
+ " .raid-summary,",
2119
+ " .lane-grid,",
2120
+ " .project-row,",
2121
+ " .task-lines p {",
682
2122
  " grid-template-columns: 1fr;",
683
- " display: grid;",
2123
+ " }",
2124
+ "",
2125
+ " .metrics-strip div {",
2126
+ " border-right: 0;",
2127
+ " border-bottom: 1px solid var(--border);",
2128
+ " }",
2129
+ "",
2130
+ " .metrics-strip div:last-child {",
2131
+ " border-bottom: 0;",
2132
+ " }",
2133
+ "",
2134
+ " .project-meta {",
2135
+ " justify-content: flex-start;",
2136
+ " }",
2137
+ "",
2138
+ " .raid-summary div {",
2139
+ " border-right: 0;",
2140
+ " border-bottom: 1px solid var(--border);",
2141
+ " }",
2142
+ "",
2143
+ " .raid-summary div:last-child {",
2144
+ " border-bottom: 0;",
684
2145
  " }",
685
2146
  "}",
686
2147
  ].join("\n"),
@@ -702,19 +2163,18 @@ function buildNextTemplateFiles(params) {
702
2163
  "}",
703
2164
  ].join("\n"),
704
2165
  "src/app/page.tsx": [
705
- 'import DomainShowcase from "./domain-showcase";',
2166
+ "import DomainShowcase from \"./domain-showcase\";",
706
2167
  "",
707
- 'export const dynamic = "force-dynamic";',
2168
+ "export const dynamic = \"force-dynamic\";",
708
2169
  "",
709
2170
  "export default function HomePage() {",
710
2171
  " return (",
711
2172
  " <main>",
712
- ' <section className="hero">',
713
- ' <div className="eyebrow">Ekairos Domain Scaffold</div>',
714
- " <h1>See your domain. Query it. Trigger an action.</h1>",
2173
+ " <section className=\"hero\">",
2174
+ " <div className=\"eyebrow\">Ekairos Domain Scaffold</div>",
2175
+ " <h1>Supply chain control tower.</h1>",
715
2176
  " <p>",
716
- " This template turns the app itself into a live domain showroom: it reads the manifest,",
717
- " queries nested data, and lets you execute actions from the UI with direct API calls.",
2177
+ " Open an order, track stock, shipment, supplier risk, and quality status in one live view.",
718
2178
  " </p>",
719
2179
  " </section>",
720
2180
  "",
@@ -724,352 +2184,472 @@ function buildNextTemplateFiles(params) {
724
2184
  "}",
725
2185
  ].join("\n"),
726
2186
  "src/app/domain-showcase.tsx": [
727
- '"use client";',
2187
+ "\"use client\";",
728
2188
  "",
729
- 'import { useEffect, useMemo, useState } from "react";',
2189
+ "import { useMemo, useState } from \"react\";",
2190
+ "import { init } from \"@instantdb/react\";",
730
2191
  "",
731
- "type ManifestAction = {",
732
- " name: string;",
733
- " key?: string | null;",
734
- " description?: string | null;",
2192
+ "type SupplierRow = {",
2193
+ " id?: string;",
2194
+ " name?: string;",
2195
+ " region?: string;",
2196
+ " risk?: string;",
2197
+ " score?: number;",
735
2198
  "};",
736
2199
  "",
737
- "type DomainManifest = {",
738
- " ok?: boolean;",
739
- " instant?: { appId?: string | null };",
740
- " auth?: { required?: boolean };",
741
- " contextString?: string | null;",
742
- " domain?: { entities?: string[]; links?: string[]; rooms?: string[] };",
743
- " actions?: ManifestAction[];",
2200
+ "type StockItemRow = {",
2201
+ " id?: string;",
2202
+ " sku?: string;",
2203
+ " warehouse?: string;",
2204
+ " available?: number;",
2205
+ " safetyStock?: number;",
744
2206
  "};",
745
2207
  "",
746
- "type TaskComment = {",
2208
+ "type InspectionRow = {",
747
2209
  " id?: string;",
748
- " body?: string;",
749
- " createdAt?: number;",
2210
+ " result?: string;",
2211
+ " severity?: string;",
2212
+ " note?: string;",
750
2213
  "};",
751
2214
  "",
752
- "type TaskRow = {",
2215
+ "type ShipmentRow = {",
753
2216
  " id?: string;",
754
- " title?: string;",
2217
+ " carrier?: string;",
2218
+ " lane?: string;",
755
2219
  " status?: string;",
756
- " createdAt?: number;",
757
- " comments?: TaskComment[] | TaskComment;",
2220
+ " etaHours?: number;",
2221
+ " inspections?: InspectionRow[] | InspectionRow;",
758
2222
  "};",
759
2223
  "",
760
- "async function requestJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {",
761
- " const response = await fetch(input, {",
762
- " ...init,",
763
- " headers: {",
764
- ' "content-type": "application/json",',
765
- " ...(init?.headers ?? {}),",
766
- " },",
767
- " cache: 'no-store',",
768
- " });",
769
- " const text = await response.text();",
770
- " if (!response.ok) {",
771
- " throw new Error(text || `request_failed:${response.status}`);",
772
- " }",
773
- " return (text ? JSON.parse(text) : null) as T;",
774
- "}",
2224
+ "type OrderRow = {",
2225
+ " id?: string;",
2226
+ " reference?: string;",
2227
+ " status?: string;",
2228
+ " spend?: number;",
2229
+ " supplier?: SupplierRow | SupplierRow[];",
2230
+ " stockItems?: StockItemRow[] | StockItemRow;",
2231
+ " shipments?: ShipmentRow[] | ShipmentRow;",
2232
+ "};",
2233
+ "",
2234
+ "const db = init({",
2235
+ " appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID || \"\",",
2236
+ "});",
775
2237
  "",
776
2238
  "function asArray<T>(value: T | T[] | null | undefined): T[] {",
777
2239
  " if (!value) return [];",
778
2240
  " return Array.isArray(value) ? value : [value];",
779
2241
  "}",
780
2242
  "",
781
- "function formatTime(value?: number) {",
782
- " if (!value) return 'now';",
783
- " try {",
784
- " return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value));",
785
- " } catch {",
786
- " return String(value);",
787
- " }",
2243
+ "function first<T>(value: T | T[] | null | undefined): T | null {",
2244
+ " return asArray(value)[0] ?? null;",
2245
+ "}",
2246
+ "",
2247
+ "function money(value?: number) {",
2248
+ " return new Intl.NumberFormat(undefined, {",
2249
+ " currency: \"USD\",",
2250
+ " maximumFractionDigits: 0,",
2251
+ " style: \"currency\",",
2252
+ " }).format(value ?? 0);",
2253
+ "}",
2254
+ "",
2255
+ "async function runAction(action: string, input: Record<string, unknown>) {",
2256
+ " const response = await fetch(\"/api/ekairos/domain\", {",
2257
+ " method: \"POST\",",
2258
+ " headers: { \"content-type\": \"application/json\" },",
2259
+ " body: JSON.stringify({ op: \"action\", action, input }),",
2260
+ " });",
2261
+ " const text = await response.text();",
2262
+ " if (!response.ok) throw new Error(text || `request_failed:${response.status}`);",
2263
+ " return text ? JSON.parse(text) : null;",
788
2264
  "}",
789
2265
  "",
790
2266
  "export default function DomainShowcase() {",
791
- " const [manifest, setManifest] = useState<DomainManifest | null>(null);",
792
- " const [tasks, setTasks] = useState<TaskRow[]>([]);",
793
- " const [draftTitle, setDraftTitle] = useState('Ship a polished domain demo');",
2267
+ " const [reference, setReference] = useState(\"PO-7842\");",
2268
+ " const [supplierName, setSupplierName] = useState(\"Marula Components\");",
2269
+ " const [sku, setSku] = useState(\"DRV-2048\");",
794
2270
  " const [loadingAction, setLoadingAction] = useState<string | null>(null);",
795
- " const [loadingData, setLoadingData] = useState(true);",
796
- " const [error, setError] = useState<string | null>(null);",
797
- " const [lastResult, setLastResult] = useState<unknown>(null);",
798
- "",
799
- " const counts = useMemo(() => ({",
800
- " entities: manifest?.domain?.entities?.length ?? 0,",
801
- " links: manifest?.domain?.links?.length ?? 0,",
802
- " actions: manifest?.actions?.length ?? 0,",
803
- " tasks: tasks.length,",
804
- " }), [manifest, tasks]);",
805
- "",
806
- " async function refresh() {",
807
- " setLoadingData(true);",
808
- " setError(null);",
809
- " try {",
810
- " const manifestData = await requestJson<DomainManifest>('/api/ekairos/domain', { method: 'GET' });",
811
- " setManifest(manifestData);",
812
- "",
813
- " const queryData = await requestJson<{ data?: { app_tasks?: TaskRow[] } }>('/api/ekairos/domain', {",
814
- " method: 'POST',",
815
- " body: JSON.stringify({",
816
- " op: 'query',",
817
- " query: {",
818
- " app_tasks: {",
819
- " $: { order: { createdAt: 'desc' }, limit: 20 },",
820
- " comments: {},",
821
- " },",
822
- " },",
823
- " }),",
824
- " });",
825
- "",
826
- " setTasks(Array.isArray(queryData?.data?.app_tasks) ? queryData.data.app_tasks : []);",
827
- " } catch (nextError) {",
828
- " setError(nextError instanceof Error ? nextError.message : String(nextError));",
829
- " } finally {",
830
- " setLoadingData(false);",
831
- " }",
832
- " }",
833
- "",
834
- " useEffect(() => {",
835
- " void refresh();",
836
- " }, []);",
2271
+ " const [actionError, setActionError] = useState<string | null>(null);",
2272
+ "",
2273
+ " const query = db.useQuery({",
2274
+ " procurement_order: {",
2275
+ " $: { order: { createdAt: \"desc\" }, limit: 8 },",
2276
+ " supplier: {},",
2277
+ " stockItems: {},",
2278
+ " shipments: {",
2279
+ " inspections: {},",
2280
+ " },",
2281
+ " },",
2282
+ " }) as {",
2283
+ " data?: { procurement_order?: OrderRow[] };",
2284
+ " error?: unknown;",
2285
+ " isLoading: boolean;",
2286
+ " };",
837
2287
  "",
838
- " async function runAction(action: string, input: Record<string, unknown>) {",
2288
+ " const orders = query.data?.procurement_order ?? [];",
2289
+ " const activeOrder = orders[0] ?? null;",
2290
+ " const supplier = first(activeOrder?.supplier);",
2291
+ " const stockItems = asArray(activeOrder?.stockItems);",
2292
+ " const shipments = asArray(activeOrder?.shipments);",
2293
+ " const shipment = shipments[0] ?? null;",
2294
+ " const inspections = shipments.flatMap((entry) => asArray(entry.inspections));",
2295
+ " const inspection = inspections[0] ?? null;",
2296
+ "",
2297
+ " const metrics = useMemo(() => ({",
2298
+ " orders: orders.length,",
2299
+ " stock: stockItems.reduce((total, item) => total + (item.available ?? 0), 0),",
2300
+ " eta: shipment?.etaHours ?? 0,",
2301
+ " risk: supplier?.risk ?? \"none\",",
2302
+ " }), [orders.length, shipment?.etaHours, stockItems, supplier?.risk]);",
2303
+ "",
2304
+ " async function submitAction(action: string, input: Record<string, unknown>) {",
839
2305
  " setLoadingAction(action);",
840
- " setError(null);",
2306
+ " setActionError(null);",
841
2307
  " try {",
842
- " const result = await requestJson('/api/ekairos/domain', {",
843
- " method: 'POST',",
844
- " body: JSON.stringify({ op: 'action', action, input }),",
845
- " });",
846
- " setLastResult(result);",
847
- " await refresh();",
848
- " } catch (nextError) {",
849
- " setError(nextError instanceof Error ? nextError.message : String(nextError));",
2308
+ " await runAction(action, input);",
2309
+ " } catch (error) {",
2310
+ " setActionError(error instanceof Error ? error.message : String(error));",
850
2311
  " } finally {",
851
2312
  " setLoadingAction(null);",
852
2313
  " }",
853
2314
  " }",
854
2315
  "",
855
2316
  " return (",
856
- ' <section className="shell">',
857
- ' <div className="stat-grid">',
858
- ' <article className="card stat-card"><span className="eyebrow">Entities</span><strong>{counts.entities}</strong></article>',
859
- ' <article className="card stat-card"><span className="eyebrow">Links</span><strong>{counts.links}</strong></article>',
860
- ' <article className="card stat-card"><span className="eyebrow">Actions</span><strong>{counts.actions}</strong></article>',
861
- ' <article className="card stat-card"><span className="eyebrow">Tasks</span><strong>{counts.tasks}</strong></article>',
2317
+ " <section className=\"shell\">",
2318
+ " <div className=\"metrics-strip\">",
2319
+ " <div><span>open orders</span><strong>{metrics.orders}</strong></div>",
2320
+ " <div><span>available stock</span><strong>{metrics.stock}</strong></div>",
2321
+ " <div><span>shipment eta</span><strong>{metrics.eta}h</strong></div>",
2322
+ " <div><span>supplier risk</span><strong>{metrics.risk}</strong></div>",
862
2323
  " </div>",
863
2324
  "",
864
- ' <div className="grid showcase-grid">',
865
- ' <article className="card panel-tall">',
866
- ' <div className="panel-head">',
867
- ' <div>',
868
- ' <span className="eyebrow">Domain Manifest</span>',
869
- ' <h2>Live contract</h2>',
870
- " </div>",
871
- " <button className=\"button ghost\" onClick={() => void refresh()} disabled={loadingData}>",
872
- ' {loadingData ? <span className="spinner" aria-hidden="true" /> : null}',
873
- " Refresh",
874
- " </button>",
875
- " </div>",
876
- ' <p className="muted">The UI calls the same Ekairos runtime route your CLI uses.</p>',
877
- ' <div className="manifest-list">',
878
- ' <div><span className="manifest-label">App ID</span><span>{manifest?.instant?.appId ?? "not configured yet"}</span></div>',
879
- ' <div><span className="manifest-label">Auth</span><span>{manifest?.auth?.required ? "required" : "open"}</span></div>',
880
- ' <div><span className="manifest-label">Entities</span><span>{(manifest?.domain?.entities ?? []).join(", ") || "none"}</span></div>',
881
- ' <div><span className="manifest-label">Links</span><span>{(manifest?.domain?.links ?? []).join(", ") || "none"}</span></div>',
882
- " </div>",
883
- ' <pre><code>{manifest?.contextString ?? "Context string will appear here after the first fetch."}</code></pre>',
884
- " </article>",
885
- "",
886
- ' <article className="card panel-tall">',
887
- ' <div className="panel-head">',
888
- ' <div>',
889
- ' <span className="eyebrow">Action Demo</span>',
890
- ' <h2>Trigger the domain</h2>',
891
- " </div>",
892
- " </div>",
893
- ' <p className="muted">Use the API directly. Click one action and watch the data refresh below.</p>',
894
- ' <label className="field">',
895
- ' <span>Task title</span>',
896
- ' <input',
897
- ' className="input"',
898
- ' value={draftTitle}',
899
- ' onChange={(event) => setDraftTitle(event.target.value)}',
900
- ' placeholder="Name your first task"',
901
- " />",
2325
+ " <div className=\"workbench\">",
2326
+ " <section className=\"command-panel\">",
2327
+ " <span className=\"eyebrow\">Release</span>",
2328
+ " <h2>Open a purchase order</h2>",
2329
+ " <label className=\"field\">",
2330
+ " <span>PO reference</span>",
2331
+ " <input className=\"input\" value={reference} onChange={(event) => setReference(event.target.value)} />",
902
2332
  " </label>",
903
- ' <div className="button-row">',
2333
+ " <label className=\"field\">",
2334
+ " <span>Supplier</span>",
2335
+ " <input className=\"input\" value={supplierName} onChange={(event) => setSupplierName(event.target.value)} />",
2336
+ " </label>",
2337
+ " <label className=\"field\">",
2338
+ " <span>SKU</span>",
2339
+ " <input className=\"input\" value={sku} onChange={(event) => setSku(event.target.value)} />",
2340
+ " </label>",
2341
+ " <div className=\"button-row\">",
904
2342
  " <button",
905
- ' className="button"',
2343
+ " className=\"button\"",
906
2344
  " disabled={loadingAction !== null}",
907
- " onClick={() => void runAction('app.task.create', { title: draftTitle, status: 'manual' })}",
2345
+ " onClick={() => void submitAction(\"supplyChain.order.launch\", { reference, sku, supplierName })}",
908
2346
  " >",
909
- ' {loadingAction === "app.task.create" ? <span className="spinner" aria-hidden="true" /> : null}',
910
- " Create Task",
2347
+ " {loadingAction === \"supplyChain.order.launch\" ? \"Opening\" : \"Open order\"}",
911
2348
  " </button>",
912
2349
  " <button",
913
- ' className="button ghost"',
914
- " disabled={loadingAction !== null}",
915
- " onClick={() => void runAction('app.demo.seed', {})}",
2350
+ " className=\"button ghost\"",
2351
+ " disabled={loadingAction !== null || !shipment?.id}",
2352
+ " onClick={() => void submitAction(\"supplyChain.shipment.expedite\", { shipmentId: shipment?.id })}",
916
2353
  " >",
917
- ' {loadingAction === "app.demo.seed" ? <span className="spinner" aria-hidden="true" /> : null}',
918
- " Seed Demo",
2354
+ " Expedite shipment",
919
2355
  " </button>",
920
2356
  " </div>",
921
- ' <div className="action-list">',
922
- ' {(manifest?.actions ?? []).map((action) => (',
923
- ' <div className="action-item" key={action.name}>',
924
- ' <strong>{action.key ?? action.name}</strong>',
925
- ' <span>{action.description ?? action.name}</span>',
926
- " </div>",
927
- " ))}",
2357
+ " </section>",
2358
+ "",
2359
+ " <section className=\"context-rail\" aria-label=\"Operational path\">",
2360
+ " <div className=\"panel-head\">",
2361
+ " <div>",
2362
+ " <span className=\"eyebrow\">Path</span>",
2363
+ " <h2>What gets linked</h2>",
2364
+ " </div>",
2365
+ " </div>",
2366
+ " <div className=\"context-list\">",
2367
+ " <div className=\"context-row\"><span>Supplier</span><strong>Risk and commercial owner</strong></div>",
2368
+ " <div className=\"context-row\"><span>Order</span><strong>Spend and release state</strong></div>",
2369
+ " <div className=\"context-row\"><span>Inventory</span><strong>SKU and stock position</strong></div>",
2370
+ " <div className=\"context-row\"><span>Transport</span><strong>Carrier lane and ETA</strong></div>",
2371
+ " <div className=\"context-row\"><span>Quality</span><strong>Arrival inspection</strong></div>",
928
2372
  " </div>",
929
- ' <pre><code>{lastResult ? JSON.stringify(lastResult, null, 2) : "Action results will appear here."}</code></pre>',
930
- " </article>",
931
- "",
932
- ' <article className="card panel-wide">',
933
- ' <div className="panel-head">',
934
- ' <div>',
935
- ' <span className="eyebrow">Query Result</span>',
936
- ' <h2>Nested task data</h2>',
2373
+ " </section>",
2374
+ "",
2375
+ " <section className=\"graph-panel\">",
2376
+ " <div className=\"panel-head\">",
2377
+ " <div>",
2378
+ " <span className=\"eyebrow\">Live order</span>",
2379
+ " <h2>{activeOrder?.reference ?? \"No order active\"}</h2>",
937
2380
  " </div>",
938
- ' <span className="status-pill">{loadingData ? "syncing" : `${tasks.length} rows`}</span>',
2381
+ " <span className=\"status-pill\">{query.isLoading ? \"loading\" : activeOrder?.status ?? \"idle\"}</span>",
939
2382
  " </div>",
940
- ' <p className="muted">This list comes from a direct `op: "query"` call to the domain API.</p>',
941
- ' {tasks.length === 0 ? <div className="empty-state">No tasks yet. Click <strong>Seed Demo</strong> to populate the canvas.</div> : null}',
942
- ' <div className="task-list">',
943
- ' {tasks.map((task, index) => (',
944
- ' <article className="task-card" key={task.id ?? `${task.title ?? "task"}-${index}`}>',
945
- ' <div className="task-head">',
946
- ' <strong>{task.title ?? "Untitled task"}</strong>',
947
- ' <span className="status-pill">{task.status ?? "draft"}</span>',
2383
+ "",
2384
+ " {query.isLoading ? (",
2385
+ " <div className=\"skeleton-stack\" aria-label=\"Loading order\">",
2386
+ " <span />",
2387
+ " <span />",
2388
+ " <span />",
2389
+ " </div>",
2390
+ " ) : query.error ? (",
2391
+ " <div className=\"error-banner\">{String(query.error)}</div>",
2392
+ " ) : !activeOrder ? (",
2393
+ " <div className=\"empty-state\">Open an order to create the first control tower view.</div>",
2394
+ " ) : (",
2395
+ " <>",
2396
+ " <div className=\"raid-summary\">",
2397
+ " <div>",
2398
+ " <span>supplier</span>",
2399
+ " <strong>{supplier?.name ?? \"unassigned\"}</strong>",
948
2400
  " </div>",
949
- ' <span className="muted">{formatTime(task.createdAt)}</span>',
950
- ' <div className="comment-list">',
951
- ' {asArray(task.comments).map((comment, commentIndex) => (',
952
- ' <div className="comment-item" key={comment.id ?? `${index}-${commentIndex}`}>',
953
- ' <span className="comment-dot" />',
954
- ' <span>{comment.body ?? "Empty comment"}</span>',
955
- " </div>",
2401
+ " <div>",
2402
+ " <span>region</span>",
2403
+ " <strong>{supplier?.region ?? \"unknown\"}</strong>",
2404
+ " </div>",
2405
+ " <div>",
2406
+ " <span>spend</span>",
2407
+ " <strong>{money(activeOrder.spend)}</strong>",
2408
+ " </div>",
2409
+ " </div>",
2410
+ "",
2411
+ " <div className=\"lane-grid\">",
2412
+ " <div className=\"lane\">",
2413
+ " <span className=\"manifest-label\">Inventory</span>",
2414
+ " {stockItems.map((item) => (",
2415
+ " <article className=\"objective\" key={item.id}>",
2416
+ " <strong>{item.sku}</strong>",
2417
+ " <span>{item.warehouse}</span>",
2418
+ " <em>{item.available} units / {item.safetyStock} safety</em>",
2419
+ " </article>",
956
2420
  " ))}",
957
2421
  " </div>",
958
- " </article>",
959
- " ))}",
960
- " </div>",
961
- " </article>",
2422
+ " <div className=\"lane\">",
2423
+ " <span className=\"manifest-label\">Transport</span>",
2424
+ " {shipments.map((entry) => (",
2425
+ " <article className=\"objective\" key={entry.id}>",
2426
+ " <strong>{entry.carrier}</strong>",
2427
+ " <span>{entry.lane}</span>",
2428
+ " <em>{entry.status} / {entry.etaHours}h ETA</em>",
2429
+ " </article>",
2430
+ " ))}",
2431
+ " </div>",
2432
+ " <div className=\"lane\">",
2433
+ " <span className=\"manifest-label\">Quality</span>",
2434
+ " {inspections.map((entry) => (",
2435
+ " <article className=\"objective\" key={entry.id}>",
2436
+ " <strong>{entry.result}</strong>",
2437
+ " <span>{entry.severity}</span>",
2438
+ " <em>{entry.note}</em>",
2439
+ " </article>",
2440
+ " ))}",
2441
+ " </div>",
2442
+ " </div>",
2443
+ " </>",
2444
+ " )}",
2445
+ " </section>",
962
2446
  " </div>",
963
2447
  "",
964
- ' {error ? <div className="error-banner">{error}</div> : null}',
2448
+ " {actionError ? <div className=\"error-banner\">{actionError}</div> : null}",
965
2449
  " </section>",
966
2450
  " );",
967
2451
  "}",
968
2452
  ].join("\n"),
969
2453
  "src/domain.ts": [
970
- 'import { defineDomainAction, domain } from "@ekairos/domain";',
971
- 'import { i } from "@instantdb/core";',
2454
+ "import { defineAction, domain } from \"@ekairos/domain\";",
2455
+ "import { i } from \"@instantdb/core\";",
2456
+ "import { z } from \"zod\";",
2457
+ "",
2458
+ "export const supplierNetworkDomain = domain(\"supplierNetwork\").withSchema({",
2459
+ " entities: {",
2460
+ " supplierNetwork_supplier: i.entity({",
2461
+ " name: i.string().indexed(),",
2462
+ " region: i.string().indexed(),",
2463
+ " risk: i.string().indexed(),",
2464
+ " score: i.number().indexed(),",
2465
+ " createdAt: i.number().indexed(),",
2466
+ " }),",
2467
+ " },",
2468
+ " links: {},",
2469
+ " rooms: {},",
2470
+ "});",
972
2471
  "",
973
- "const baseDomain = domain(\"ekairos.app\")",
974
- " .schema({",
2472
+ "export const procurementDomain = domain(\"procurement\")",
2473
+ " .includes(supplierNetworkDomain)",
2474
+ " .withSchema({",
975
2475
  " entities: {",
976
- " app_tasks: i.entity({",
977
- " title: i.string().indexed(),",
2476
+ " procurement_order: i.entity({",
2477
+ " reference: i.string().indexed(),",
978
2478
  " status: i.string().indexed(),",
2479
+ " spend: i.number().indexed(),",
979
2480
  " createdAt: i.number().indexed(),",
980
2481
  " }),",
981
- " app_task_comments: i.entity({",
982
- " body: i.string(),",
2482
+ " },",
2483
+ " links: {",
2484
+ " procurement_orderSupplier: {",
2485
+ " forward: { on: \"procurement_order\", has: \"one\", label: \"supplier\" },",
2486
+ " reverse: { on: \"supplierNetwork_supplier\", has: \"many\", label: \"orders\" },",
2487
+ " },",
2488
+ " },",
2489
+ " rooms: {},",
2490
+ " });",
2491
+ "",
2492
+ "export const inventoryDomain = domain(\"inventory\")",
2493
+ " .includes(procurementDomain)",
2494
+ " .withSchema({",
2495
+ " entities: {",
2496
+ " inventory_stockItem: i.entity({",
2497
+ " sku: i.string().indexed(),",
2498
+ " warehouse: i.string().indexed(),",
2499
+ " available: i.number().indexed(),",
2500
+ " safetyStock: i.number().indexed(),",
983
2501
  " createdAt: i.number().indexed(),",
984
2502
  " }),",
985
2503
  " },",
986
2504
  " links: {",
987
- " taskComments: {",
988
- ' forward: { on: "app_tasks", has: "many", label: "comments" },',
989
- ' reverse: { on: "app_task_comments", has: "one", label: "task" },',
2505
+ " inventory_stockItemOrder: {",
2506
+ " forward: { on: \"inventory_stockItem\", has: \"one\", label: \"order\" },",
2507
+ " reverse: { on: \"procurement_order\", has: \"many\", label: \"stockItems\" },",
990
2508
  " },",
991
2509
  " },",
992
2510
  " rooms: {},",
993
2511
  " });",
994
2512
  "",
995
- "export const createTaskAction = defineDomainAction<",
996
- " Record<string, unknown>,",
997
- " { title?: string; status?: string },",
998
- " { taskId: string },",
999
- " any",
1000
- ">({",
1001
- ' name: "app.task.create",',
1002
- " async execute({ runtime, input }): Promise<{ taskId: string }> {",
1003
- ' "use step";',
1004
- " const scoped = await runtime.use(appDomain);",
1005
- " const taskId = globalThis.crypto.randomUUID();",
1006
- " await scoped.db.transact([",
1007
- " scoped.db.tx.app_tasks[taskId].update({",
1008
- ' title: String((input as any)?.title ?? "").trim() || "Untitled task",',
1009
- ' status: String((input as any)?.status ?? "").trim() || "draft",',
1010
- " createdAt: Date.now(),",
2513
+ "export const transportationDomain = domain(\"transportation\")",
2514
+ " .includes(procurementDomain)",
2515
+ " .withSchema({",
2516
+ " entities: {",
2517
+ " transportation_shipment: i.entity({",
2518
+ " carrier: i.string().indexed(),",
2519
+ " lane: i.string().indexed(),",
2520
+ " status: i.string().indexed(),",
2521
+ " etaHours: i.number().indexed(),",
2522
+ " createdAt: i.number().indexed(),",
1011
2523
  " }),",
1012
- " ]);",
1013
- " return { taskId };",
1014
- " },",
1015
- "});",
2524
+ " },",
2525
+ " links: {",
2526
+ " transportation_shipmentOrder: {",
2527
+ " forward: { on: \"transportation_shipment\", has: \"one\", label: \"order\" },",
2528
+ " reverse: { on: \"procurement_order\", has: \"many\", label: \"shipments\" },",
2529
+ " },",
2530
+ " },",
2531
+ " rooms: {},",
2532
+ " });",
2533
+ "",
2534
+ "export const qualityControlDomain = domain(\"qualityControl\")",
2535
+ " .includes(transportationDomain)",
2536
+ " .withSchema({",
2537
+ " entities: {",
2538
+ " qualityControl_inspection: i.entity({",
2539
+ " result: i.string().indexed(),",
2540
+ " severity: i.string().indexed(),",
2541
+ " note: i.string(),",
2542
+ " createdAt: i.number().indexed(),",
2543
+ " }),",
2544
+ " },",
2545
+ " links: {",
2546
+ " qualityControl_inspectionShipment: {",
2547
+ " forward: { on: \"qualityControl_inspection\", has: \"one\", label: \"shipment\" },",
2548
+ " reverse: { on: \"transportation_shipment\", has: \"many\", label: \"inspections\" },",
2549
+ " },",
2550
+ " },",
2551
+ " rooms: {},",
2552
+ " });",
1016
2553
  "",
1017
- "export const addTaskCommentAction = defineDomainAction<",
1018
- " Record<string, unknown>,",
1019
- " { taskId?: string; body?: string },",
1020
- " { commentId: string; taskId: string },",
1021
- " any",
1022
- ">({",
1023
- ' name: "app.task.comment.add",',
1024
- " async execute({ runtime, input }): Promise<{ commentId: string; taskId: string }> {",
1025
- ' "use step";',
1026
- " const scoped = await runtime.use(appDomain);",
1027
- " const commentId = globalThis.crypto.randomUUID();",
1028
- ' const taskId = String((input as any)?.taskId ?? "").trim();',
1029
- " if (!taskId) throw new Error(\"taskId is required\");",
1030
- " await scoped.db.transact([",
1031
- " scoped.db.tx.app_task_comments[commentId].update({",
1032
- ' body: String((input as any)?.body ?? "").trim() || "Empty comment",',
1033
- " createdAt: Date.now(),",
2554
+ "const baseDomain = domain(\"supplyChain\")",
2555
+ " .includes(inventoryDomain)",
2556
+ " .includes(qualityControlDomain)",
2557
+ " .withSchema({ entities: {}, links: {}, rooms: {} });",
2558
+ "",
2559
+ "export const launchOrderAction = defineAction({",
2560
+ " name: \"supplyChain.order.launch\",",
2561
+ " input: z.object({",
2562
+ " reference: z.string().optional(),",
2563
+ " supplierName: z.string().optional(),",
2564
+ " sku: z.string().optional(),",
2565
+ " }),",
2566
+ " output: z.object({",
2567
+ " supplierId: z.string(),",
2568
+ " orderId: z.string(),",
2569
+ " stockItemId: z.string(),",
2570
+ " shipmentId: z.string(),",
2571
+ " inspectionId: z.string(),",
2572
+ " }),",
2573
+ " async execute({ runtime, input }): Promise<{",
2574
+ " supplierId: string;",
2575
+ " orderId: string;",
2576
+ " stockItemId: string;",
2577
+ " shipmentId: string;",
2578
+ " inspectionId: string;",
2579
+ " }> {",
2580
+ " const now = Date.now();",
2581
+ " const supplierId = globalThis.crypto.randomUUID();",
2582
+ " const orderId = globalThis.crypto.randomUUID();",
2583
+ " const stockItemId = globalThis.crypto.randomUUID();",
2584
+ " const shipmentId = globalThis.crypto.randomUUID();",
2585
+ " const inspectionId = globalThis.crypto.randomUUID();",
2586
+ "",
2587
+ " await runtime.db.transact([",
2588
+ " runtime.db.tx.supplierNetwork_supplier[supplierId].update({",
2589
+ " name: String(input?.supplierName ?? \"\").trim() || \"Marula Components\",",
2590
+ " region: \"Pacific North\",",
2591
+ " risk: \"watch\",",
2592
+ " score: 82,",
2593
+ " createdAt: now,",
1034
2594
  " }),",
1035
- " scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
2595
+ " runtime.db.tx.procurement_order[orderId].update({",
2596
+ " reference: String(input?.reference ?? \"\").trim() || \"PO-7842\",",
2597
+ " status: \"released\",",
2598
+ " spend: 184700,",
2599
+ " createdAt: now + 1,",
2600
+ " }),",
2601
+ " runtime.db.tx.procurement_order[orderId].link({ supplier: supplierId }),",
2602
+ " runtime.db.tx.inventory_stockItem[stockItemId].update({",
2603
+ " sku: String(input?.sku ?? \"\").trim() || \"DRV-2048\",",
2604
+ " warehouse: \"Reno DC\",",
2605
+ " available: 320,",
2606
+ " safetyStock: 140,",
2607
+ " createdAt: now + 2,",
2608
+ " }),",
2609
+ " runtime.db.tx.inventory_stockItem[stockItemId].link({ order: orderId }),",
2610
+ " runtime.db.tx.transportation_shipment[shipmentId].update({",
2611
+ " carrier: \"Northstar Freight\",",
2612
+ " lane: \"Reno -> Austin\",",
2613
+ " status: \"in-transit\",",
2614
+ " etaHours: 38,",
2615
+ " createdAt: now + 3,",
2616
+ " }),",
2617
+ " runtime.db.tx.transportation_shipment[shipmentId].link({ order: orderId }),",
2618
+ " runtime.db.tx.qualityControl_inspection[inspectionId].update({",
2619
+ " result: \"pending\",",
2620
+ " severity: \"medium\",",
2621
+ " note: \"Inspect seal integrity on arrival.\",",
2622
+ " createdAt: now + 4,",
2623
+ " }),",
2624
+ " runtime.db.tx.qualityControl_inspection[inspectionId].link({ shipment: shipmentId }),",
1036
2625
  " ]);",
1037
- " return { commentId, taskId };",
2626
+ "",
2627
+ " return { supplierId, orderId, stockItemId, shipmentId, inspectionId };",
1038
2628
  " },",
1039
2629
  "});",
1040
2630
  "",
1041
- "export const seedDemoAction = defineDomainAction<",
1042
- " Record<string, unknown>,",
1043
- " Record<string, never>,",
1044
- " { taskId: string },",
1045
- " any",
1046
- ">({",
1047
- ' name: "app.demo.seed",',
1048
- " async execute({ runtime }): Promise<{ taskId: string }> {",
1049
- ' "use step";',
1050
- " const scoped = await runtime.use(appDomain);",
1051
- " const taskId = globalThis.crypto.randomUUID();",
1052
- " const commentId = globalThis.crypto.randomUUID();",
1053
- " await scoped.db.transact([",
1054
- " scoped.db.tx.app_tasks[taskId].update({",
1055
- ' title: "Ship the first Ekairos loop",',
1056
- ' status: "ready",',
1057
- " createdAt: Date.now(),",
1058
- " }),",
1059
- " scoped.db.tx.app_task_comments[commentId].update({",
1060
- ' body: "Query me with app_tasks -> comments to validate the full CLI path.",',
1061
- " createdAt: Date.now(),",
2631
+ "export const expediteShipmentAction = defineAction({",
2632
+ " name: \"supplyChain.shipment.expedite\",",
2633
+ " input: z.object({ shipmentId: z.string().optional() }),",
2634
+ " output: z.object({ shipmentId: z.string() }),",
2635
+ " async execute({ runtime, input }): Promise<{ shipmentId: string }> {",
2636
+ " const shipmentId = String(input?.shipmentId ?? \"\").trim();",
2637
+ " if (!shipmentId) throw new Error(\"shipmentId is required\");",
2638
+ "",
2639
+ " await runtime.db.transact([",
2640
+ " runtime.db.tx.transportation_shipment[shipmentId].update({",
2641
+ " status: \"expedited\",",
2642
+ " etaHours: 16,",
1062
2643
  " }),",
1063
- " scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
1064
2644
  " ]);",
1065
- " return { taskId };",
2645
+ "",
2646
+ " return { shipmentId };",
1066
2647
  " },",
1067
2648
  "});",
1068
2649
  "",
1069
- "export const appDomain = baseDomain.actions({",
1070
- " addTaskComment: addTaskCommentAction,",
1071
- " createTask: createTaskAction,",
1072
- " seedDemo: seedDemoAction,",
2650
+ "export const appDomain = baseDomain.withActions({",
2651
+ " expediteShipment: expediteShipmentAction,",
2652
+ " launchOrder: launchOrderAction,",
1073
2653
  "});",
1074
2654
  "",
1075
2655
  "export default appDomain;",
@@ -1131,29 +2711,30 @@ function buildNextTemplateFiles(params) {
1131
2711
  "});",
1132
2712
  ].join("\n"),
1133
2713
  "src/workflows/demo.workflow.ts": [
1134
- 'import { executeRuntimeAction } from "@ekairos/domain/runtime";',
1135
- 'import { createRuntime } from "../runtime";',
2714
+ "import type { ActiveDomain } from \"@ekairos/domain\";",
2715
+ "import appDomain from \"../domain\";",
2716
+ "import { createRuntime } from \"../runtime\";",
1136
2717
  "",
1137
2718
  "export type DemoWorkflowInput = {",
1138
- " title: string;",
1139
- " comment?: string;",
2719
+ " expedite?: boolean;",
2720
+ " reference?: string;",
2721
+ " sku?: string;",
2722
+ " supplierName?: string;",
1140
2723
  "};",
1141
2724
  "",
1142
2725
  "export async function runDemoWorkflow(input: DemoWorkflowInput) {",
1143
- ' "use workflow";',
2726
+ " \"use workflow\";",
1144
2727
  " const runtime = createRuntime();",
1145
- " const created = (await executeRuntimeAction({",
1146
- " runtime,",
1147
- ' action: "app.task.create",',
1148
- " input: { title: input.title, status: \"workflow\" },",
1149
- " })) as { taskId: string };",
1150
- "",
1151
- ' const comment = String(input.comment ?? "").trim();',
1152
- " if (comment) {",
1153
- " await executeRuntimeAction({",
1154
- " runtime,",
1155
- ' action: "app.task.comment.add",',
1156
- " input: { taskId: created.taskId, body: comment },",
2728
+ " const scoped = (await runtime.use(appDomain)) as ActiveDomain<typeof appDomain>;",
2729
+ " const created = await scoped.actions.launchOrder({",
2730
+ " reference: input.reference,",
2731
+ " sku: input.sku,",
2732
+ " supplierName: input.supplierName,",
2733
+ " });",
2734
+ "",
2735
+ " if (input.expedite) {",
2736
+ " await scoped.actions.expediteShipment({",
2737
+ " shipmentId: created.shipmentId,",
1157
2738
  " });",
1158
2739
  " }",
1159
2740
  "",
@@ -1166,6 +2747,13 @@ export async function createDomainApp(params) {
1166
2747
  if (params.framework !== "next") {
1167
2748
  throw new Error("Only --next is supported right now.");
1168
2749
  }
2750
+ const template = resolveCreateAppTemplate(params);
2751
+ if (params.smoke && !params.install) {
2752
+ throw new Error("--smoke requires dependencies. Remove --no-install or run smoke after installing.");
2753
+ }
2754
+ if (params.demo && !params.smoke) {
2755
+ throw new Error("--demo runs the full app cycle and requires smoke validation.");
2756
+ }
1169
2757
  const targetDir = resolve(params.directory || ".");
1170
2758
  await emitProgress(params.onProgress, {
1171
2759
  stage: "prepare-target",
@@ -1220,6 +2808,7 @@ export async function createDomainApp(params) {
1220
2808
  });
1221
2809
  provisioned = await provisionInstantApp({
1222
2810
  directory: targetDir,
2811
+ template,
1223
2812
  instantToken: trimOrEmpty(params.instantToken),
1224
2813
  orgId: params.orgId,
1225
2814
  });
@@ -1232,6 +2821,11 @@ export async function createDomainApp(params) {
1232
2821
  }
1233
2822
  const appId = explicitAppId || provisioned?.appId || null;
1234
2823
  const adminToken = explicitAdminToken || provisioned?.adminToken || null;
2824
+ if (params.smoke && (!appId || !adminToken)) {
2825
+ throw new Error(params.demo
2826
+ ? "--demo requires Instant provisioning. Set INSTANT_PERSONAL_ACCESS_TOKEN or pass --instantToken."
2827
+ : "--smoke requires a configured Instant app. Pass --instantToken or --appId with --adminToken.");
2828
+ }
1235
2829
  await emitProgress(params.onProgress, {
1236
2830
  stage: "write-files",
1237
2831
  status: "running",
@@ -1240,6 +2834,7 @@ export async function createDomainApp(params) {
1240
2834
  });
1241
2835
  const files = buildNextTemplateFiles({
1242
2836
  targetDir,
2837
+ template,
1243
2838
  domainVersion,
1244
2839
  packageManager,
1245
2840
  workspacePath: params.workspacePath,
@@ -1251,14 +2846,15 @@ export async function createDomainApp(params) {
1251
2846
  message: "Scaffold files written",
1252
2847
  progress: 72,
1253
2848
  });
1254
- if (appId && adminToken) {
2849
+ const envFile = appId && adminToken ? join(targetDir, ".env.local") : null;
2850
+ if (appId && adminToken && envFile) {
1255
2851
  await emitProgress(params.onProgress, {
1256
2852
  stage: "write-env",
1257
2853
  status: "running",
1258
2854
  message: "Writing .env.local",
1259
2855
  progress: 78,
1260
2856
  });
1261
- await writeFile(join(targetDir, ".env.local"), [
2857
+ await writeFile(envFile, [
1262
2858
  `NEXT_PUBLIC_INSTANT_APP_ID=${appId}`,
1263
2859
  `INSTANT_ADMIN_TOKEN=${adminToken}`,
1264
2860
  "",
@@ -1285,25 +2881,60 @@ export async function createDomainApp(params) {
1285
2881
  progress: 96,
1286
2882
  });
1287
2883
  }
1288
- const nextSteps = [
1289
- `cd ${targetDir}`,
1290
- params.install
2884
+ const smoke = params.smoke
2885
+ ? await runSmoke({
2886
+ demo: template === "supply-chain",
2887
+ targetDir,
2888
+ packageManager,
2889
+ keepServer: Boolean(params.keepServer),
2890
+ onKeepServer: params.onKeepServer,
2891
+ onProgress: params.onProgress,
2892
+ })
2893
+ : null;
2894
+ const cliCommand = packageBinCommandFor(packageManager, "domain");
2895
+ const reviewUrl = smoke?.baseUrl ?? "http://localhost:3000";
2896
+ const startStep = smoke?.keepServer
2897
+ ? `Open ${reviewUrl} for review`
2898
+ : params.install
1291
2899
  ? runScriptCommandFor(packageManager, "dev")
1292
- : `${installCommandFor(packageManager)} && ${runScriptCommandFor(packageManager, "dev")}`,
1293
- "Open http://localhost:3000 and click Seed Demo in the showcase UI",
1294
- "npx @ekairos/domain inspect --baseUrl=http://localhost:3000 --admin --pretty",
1295
- "npx @ekairos/domain seedDemo --baseUrl=http://localhost:3000 --admin --pretty",
1296
- "npx @ekairos/domain query \"{ app_tasks: { comments: {} } }\" --baseUrl=http://localhost:3000 --admin --pretty",
1297
- ];
2900
+ : `${installCommandFor(packageManager)} && ${runScriptCommandFor(packageManager, "dev")}`;
2901
+ const nextSteps = template === "supply-chain"
2902
+ ? [
2903
+ `cd ${targetDir}`,
2904
+ startStep,
2905
+ `Open ${reviewUrl} and launch a purchase order from the control tower UI`,
2906
+ `${cliCommand} inspect --baseUrl=${reviewUrl} --admin --pretty`,
2907
+ `${cliCommand} "supplyChain.order.launch" "{ reference: 'PO-7842', supplierName: 'Marula Components', sku: 'DRV-2048' }" --baseUrl=${reviewUrl} --admin --pretty`,
2908
+ `${cliCommand} query "{ procurement_order: { supplier: {}, stockItems: {}, shipments: { inspections: {} } } }" --baseUrl=${reviewUrl} --admin --pretty`,
2909
+ ]
2910
+ : template === "agent"
2911
+ ? [
2912
+ `cd ${targetDir}`,
2913
+ startStep,
2914
+ `Open ${reviewUrl} and start a context through /api/agent/react`,
2915
+ `Inspect the returned context id in Workbench v2 for app ${appId || "<instant-app-id>"}`,
2916
+ `Iterate on src/agent.ts`,
2917
+ ]
2918
+ : [
2919
+ `cd ${targetDir}`,
2920
+ startStep,
2921
+ `Open ${reviewUrl} and add your first domain in src/domain.ts`,
2922
+ `${cliCommand} inspect --baseUrl=${reviewUrl} --admin --pretty`,
2923
+ ];
1298
2924
  const result = {
1299
2925
  ok: true,
1300
2926
  directory: targetDir,
1301
2927
  framework: params.framework,
2928
+ template,
1302
2929
  installed: params.install,
1303
2930
  packageManager,
1304
2931
  provisioned: Boolean(provisioned),
1305
2932
  appId,
1306
2933
  adminToken,
2934
+ adminTokenWritten: Boolean(envFile),
2935
+ envFile,
2936
+ smoke,
2937
+ demo: Boolean(params.demo),
1307
2938
  nextSteps,
1308
2939
  };
1309
2940
  await emitProgress(params.onProgress, {