@ekairos/domain 1.22.39-beta.development.0 → 1.22.41-beta.development.0

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.
@@ -1,4 +1,5 @@
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";
@@ -8,6 +9,7 @@ const TEMPLATE_NEXT_VERSION = "15.5.7";
8
9
  const TEMPLATE_REACT_VERSION = "19.2.1";
9
10
  const TEMPLATE_TYPESCRIPT_VERSION = "^5.9.2";
10
11
  const TEMPLATE_INSTANT_VERSION = "0.22.126";
12
+ const TEMPLATE_INSTANT_REACT_VERSION = "0.22.126";
11
13
  const TEMPLATE_WORKFLOW_VERSION = "^5.0.0-beta.1";
12
14
  function trimOrEmpty(value) {
13
15
  return typeof value === "string" ? value.trim() : "";
@@ -68,51 +70,143 @@ async function readDomainPackageVersion() {
68
70
  return trimOrEmpty(parsed.version) || "latest";
69
71
  }
70
72
  function createScaffoldSchema() {
71
- const scaffoldDomain = domain("ekairos.app").schema({
73
+ return createSupplyChainScaffoldSchema();
74
+ }
75
+ function createEmptyScaffoldSchema() {
76
+ const scaffoldDomain = domain("app").withSchema({
77
+ entities: {},
78
+ links: {},
79
+ rooms: {},
80
+ });
81
+ return scaffoldDomain.toInstantSchema();
82
+ }
83
+ function createSupplyChainScaffoldSchema() {
84
+ const supplierNetworkDomain = domain("supplierNetwork").withSchema({
85
+ entities: {
86
+ supplierNetwork_supplier: i.entity({
87
+ name: i.string().indexed(),
88
+ region: i.string().indexed(),
89
+ risk: i.string().indexed(),
90
+ score: i.number().indexed(),
91
+ createdAt: i.number().indexed(),
92
+ }),
93
+ },
94
+ links: {},
95
+ rooms: {},
96
+ });
97
+ const procurementDomain = domain("procurement")
98
+ .includes(supplierNetworkDomain)
99
+ .withSchema({
72
100
  entities: {
73
- app_tasks: i.entity({
74
- title: i.string().indexed(),
101
+ procurement_order: i.entity({
102
+ reference: i.string().indexed(),
75
103
  status: i.string().indexed(),
104
+ spend: i.number().indexed(),
76
105
  createdAt: i.number().indexed(),
77
106
  }),
78
- app_task_comments: i.entity({
79
- body: i.string(),
107
+ },
108
+ links: {
109
+ procurement_orderSupplier: {
110
+ forward: { on: "procurement_order", has: "one", label: "supplier" },
111
+ reverse: { on: "supplierNetwork_supplier", has: "many", label: "orders" },
112
+ },
113
+ },
114
+ rooms: {},
115
+ });
116
+ const inventoryDomain = domain("inventory")
117
+ .includes(procurementDomain)
118
+ .withSchema({
119
+ entities: {
120
+ inventory_stockItem: i.entity({
121
+ sku: i.string().indexed(),
122
+ warehouse: i.string().indexed(),
123
+ available: i.number().indexed(),
124
+ safetyStock: i.number().indexed(),
80
125
  createdAt: i.number().indexed(),
81
126
  }),
82
127
  },
83
128
  links: {
84
- taskComments: {
85
- forward: { on: "app_tasks", has: "many", label: "comments" },
86
- reverse: { on: "app_task_comments", has: "one", label: "task" },
129
+ inventory_stockItemOrder: {
130
+ forward: { on: "inventory_stockItem", has: "one", label: "order" },
131
+ reverse: { on: "procurement_order", has: "many", label: "stockItems" },
87
132
  },
88
133
  },
89
134
  rooms: {},
90
135
  });
136
+ const transportationDomain = domain("transportation")
137
+ .includes(procurementDomain)
138
+ .withSchema({
139
+ entities: {
140
+ transportation_shipment: i.entity({
141
+ carrier: i.string().indexed(),
142
+ lane: i.string().indexed(),
143
+ status: i.string().indexed(),
144
+ etaHours: i.number().indexed(),
145
+ createdAt: i.number().indexed(),
146
+ }),
147
+ },
148
+ links: {
149
+ transportation_shipmentOrder: {
150
+ forward: { on: "transportation_shipment", has: "one", label: "order" },
151
+ reverse: { on: "procurement_order", has: "many", label: "shipments" },
152
+ },
153
+ },
154
+ rooms: {},
155
+ });
156
+ const qualityControlDomain = domain("qualityControl")
157
+ .includes(transportationDomain)
158
+ .withSchema({
159
+ entities: {
160
+ qualityControl_inspection: i.entity({
161
+ result: i.string().indexed(),
162
+ severity: i.string().indexed(),
163
+ note: i.string(),
164
+ createdAt: i.number().indexed(),
165
+ }),
166
+ },
167
+ links: {
168
+ qualityControl_inspectionShipment: {
169
+ forward: { on: "qualityControl_inspection", has: "one", label: "shipment" },
170
+ reverse: { on: "transportation_shipment", has: "many", label: "inspections" },
171
+ },
172
+ },
173
+ rooms: {},
174
+ });
175
+ const scaffoldDomain = domain("supplyChain")
176
+ .includes(inventoryDomain)
177
+ .includes(qualityControlDomain)
178
+ .withSchema({ entities: {}, links: {}, rooms: {} });
91
179
  return scaffoldDomain.toInstantSchema();
92
180
  }
93
181
  function createScaffoldPerms() {
182
+ return createSupplyChainScaffoldPerms();
183
+ }
184
+ function createEmptyScaffoldPerms() {
94
185
  return {
95
186
  attrs: {
96
187
  allow: { create: "true" },
97
188
  },
98
- app_tasks: {
99
- bind: ["isLoggedIn", "auth.id != null"],
100
- allow: {
101
- view: "true",
102
- create: "isLoggedIn",
103
- update: "isLoggedIn",
104
- delete: "false",
105
- },
189
+ };
190
+ }
191
+ function createSupplyChainScaffoldPerms() {
192
+ const entityRules = {
193
+ bind: ["isLoggedIn", "auth.id != null"],
194
+ allow: {
195
+ view: "true",
196
+ create: "isLoggedIn",
197
+ update: "isLoggedIn",
198
+ delete: "false",
106
199
  },
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
- },
200
+ };
201
+ return {
202
+ attrs: {
203
+ allow: { create: "true" },
115
204
  },
205
+ supplierNetwork_supplier: entityRules,
206
+ procurement_order: entityRules,
207
+ inventory_stockItem: entityRules,
208
+ transportation_shipment: entityRules,
209
+ qualityControl_inspection: entityRules,
116
210
  };
117
211
  }
118
212
  function installCommandFor(packageManager) {
@@ -133,6 +227,328 @@ function runScriptCommandFor(packageManager, script) {
133
227
  return `bun run ${script}`;
134
228
  return `npm run ${script}`;
135
229
  }
230
+ function packageBinCommandFor(packageManager, command) {
231
+ void command;
232
+ return "ekairos domain";
233
+ }
234
+ function typecheckCommandFor(packageManager) {
235
+ if (packageManager === "pnpm")
236
+ return { command: "pnpm", args: ["typecheck"] };
237
+ if (packageManager === "yarn")
238
+ return { command: "yarn", args: ["typecheck"] };
239
+ if (packageManager === "bun")
240
+ return { command: "bun", args: ["run", "typecheck"] };
241
+ return { command: "npm", args: ["run", "typecheck"] };
242
+ }
243
+ function nextDevCommandFor(targetDir, port) {
244
+ return {
245
+ command: process.execPath,
246
+ args: [
247
+ join(targetDir, "node_modules", "next", "dist", "bin", "next"),
248
+ "dev",
249
+ "--hostname",
250
+ "127.0.0.1",
251
+ "--port",
252
+ String(port),
253
+ ],
254
+ };
255
+ }
256
+ async function runCommand(params) {
257
+ const timeoutMs = params.timeoutMs ?? 2 * 60 * 1000;
258
+ await new Promise((resolveRun, rejectRun) => {
259
+ const child = spawn(params.command, params.args, {
260
+ cwd: params.targetDir,
261
+ env: process.env,
262
+ shell: process.platform === "win32",
263
+ stdio: "pipe",
264
+ });
265
+ let output = "";
266
+ const timer = setTimeout(() => {
267
+ stopProcess(child.pid);
268
+ rejectRun(new Error(`${params.command} timed out after ${timeoutMs}ms`));
269
+ }, timeoutMs);
270
+ child.stdout.on("data", (chunk) => {
271
+ output += chunk.toString();
272
+ });
273
+ child.stderr.on("data", (chunk) => {
274
+ output += chunk.toString();
275
+ });
276
+ child.on("error", (error) => {
277
+ clearTimeout(timer);
278
+ rejectRun(error);
279
+ });
280
+ child.on("close", (code) => {
281
+ clearTimeout(timer);
282
+ if (code === 0) {
283
+ resolveRun();
284
+ return;
285
+ }
286
+ rejectRun(new Error(output.trim() || `${params.command} failed with exit code ${code ?? "unknown"}`));
287
+ });
288
+ });
289
+ }
290
+ async function reservePort() {
291
+ return await new Promise((resolvePort, rejectPort) => {
292
+ const server = createServer();
293
+ server.once("error", rejectPort);
294
+ server.listen(0, "127.0.0.1", () => {
295
+ const address = server.address();
296
+ if (!address || typeof address === "string") {
297
+ rejectPort(new Error("Failed to reserve smoke port"));
298
+ return;
299
+ }
300
+ const port = address.port;
301
+ server.close((error) => {
302
+ if (error)
303
+ rejectPort(error);
304
+ else
305
+ resolvePort(port);
306
+ });
307
+ });
308
+ });
309
+ }
310
+ function stopProcess(pid) {
311
+ if (!pid)
312
+ return;
313
+ if (process.platform === "win32") {
314
+ spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
315
+ stdio: "ignore",
316
+ });
317
+ return;
318
+ }
319
+ try {
320
+ process.kill(-pid, "SIGTERM");
321
+ }
322
+ catch {
323
+ try {
324
+ process.kill(pid, "SIGTERM");
325
+ }
326
+ catch {
327
+ // Process already exited.
328
+ }
329
+ }
330
+ }
331
+ function quotePowerShellString(value) {
332
+ return `'${value.replace(/'/g, "''")}'`;
333
+ }
334
+ function startDetachedWindowsProcess(params) {
335
+ const argumentList = params.args.map(quotePowerShellString).join(", ");
336
+ const script = [
337
+ `$p = Start-Process -FilePath ${quotePowerShellString(params.command)} ` +
338
+ `-ArgumentList @(${argumentList}) ` +
339
+ `-WorkingDirectory ${quotePowerShellString(params.targetDir)} ` +
340
+ "-WindowStyle Hidden -PassThru",
341
+ "Write-Output $p.Id",
342
+ ].join("; ");
343
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
344
+ encoding: "utf8",
345
+ stdio: "pipe",
346
+ });
347
+ if ((result.status ?? 1) !== 0) {
348
+ throw new Error(result.stderr?.trim() ||
349
+ result.stdout?.trim() ||
350
+ "Failed to start detached Next dev server");
351
+ }
352
+ const pid = Number(String(result.stdout ?? "").trim().split(/\s+/).pop());
353
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
354
+ }
355
+ async function fetchJsonWithTimeout(url, init, timeoutMs = 10000) {
356
+ const controller = new AbortController();
357
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
358
+ try {
359
+ const response = await fetch(url, {
360
+ ...init,
361
+ signal: controller.signal,
362
+ });
363
+ const data = await response.json().catch(() => null);
364
+ return { response, data: data };
365
+ }
366
+ finally {
367
+ clearTimeout(timer);
368
+ }
369
+ }
370
+ async function waitForDomainEndpoint(params) {
371
+ const endpoint = `${params.baseUrl}/api/ekairos/domain`;
372
+ const deadline = Date.now() + 3 * 60 * 1000;
373
+ let lastError = "";
374
+ while (Date.now() < deadline) {
375
+ if (params.processExited()) {
376
+ throw new Error(`Next dev server exited before smoke endpoint was ready.\n${params.readLogs()}`);
377
+ }
378
+ try {
379
+ const { response, data } = await fetchJsonWithTimeout(endpoint);
380
+ if (response.ok && data?.ok === true)
381
+ return data;
382
+ lastError = `status:${response.status}`;
383
+ }
384
+ catch (error) {
385
+ lastError = error instanceof Error ? error.message : String(error);
386
+ }
387
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 1000));
388
+ }
389
+ throw new Error(`Timed out waiting for ${endpoint}: ${lastError}\n${params.readLogs()}`);
390
+ }
391
+ function countCollection(value) {
392
+ if (Array.isArray(value))
393
+ return value.length;
394
+ if (value && typeof value === "object")
395
+ return Object.keys(value).length;
396
+ return 0;
397
+ }
398
+ function asArray(value) {
399
+ return Array.isArray(value) ? value : [];
400
+ }
401
+ async function postSmokeJson(baseUrl, body) {
402
+ const { response, data } = await fetchJsonWithTimeout(`${baseUrl}/api/ekairos/domain`, {
403
+ method: "POST",
404
+ headers: { "content-type": "application/json" },
405
+ body: JSON.stringify(body),
406
+ }, 30000);
407
+ if (!response.ok || data?.ok !== true) {
408
+ throw new Error(data?.error || `Smoke request failed with status ${response.status}`);
409
+ }
410
+ return data;
411
+ }
412
+ async function runSmoke(params) {
413
+ await emitProgress(params.onProgress, {
414
+ stage: "smoke",
415
+ status: "running",
416
+ message: "Running typecheck",
417
+ progress: 97,
418
+ });
419
+ const typecheck = typecheckCommandFor(params.packageManager);
420
+ await runCommand({
421
+ targetDir: params.targetDir,
422
+ command: typecheck.command,
423
+ args: typecheck.args,
424
+ });
425
+ const port = await reservePort();
426
+ const baseUrl = `http://127.0.0.1:${port}`;
427
+ const dev = nextDevCommandFor(params.targetDir, port);
428
+ let logs = "";
429
+ let smokePassed = false;
430
+ let detachedPid = null;
431
+ const child = params.keepServer && process.platform === "win32"
432
+ ? null
433
+ : spawn(dev.command, dev.args, {
434
+ cwd: params.targetDir,
435
+ env: process.env,
436
+ detached: params.keepServer,
437
+ shell: false,
438
+ stdio: params.keepServer ? "ignore" : "pipe",
439
+ });
440
+ if (params.keepServer && process.platform === "win32") {
441
+ detachedPid = startDetachedWindowsProcess({
442
+ targetDir: params.targetDir,
443
+ command: dev.command,
444
+ args: dev.args,
445
+ });
446
+ }
447
+ if (child && !params.keepServer) {
448
+ child.stdout?.on("data", (chunk) => {
449
+ logs += chunk.toString();
450
+ });
451
+ child.stderr?.on("data", (chunk) => {
452
+ logs += chunk.toString();
453
+ });
454
+ }
455
+ try {
456
+ await emitProgress(params.onProgress, {
457
+ stage: "smoke",
458
+ status: "running",
459
+ message: `Waiting for ${baseUrl}`,
460
+ progress: 98,
461
+ });
462
+ const manifest = await waitForDomainEndpoint({
463
+ baseUrl,
464
+ processExited: () => (child ? child.exitCode !== null : false),
465
+ readLogs: () => logs,
466
+ });
467
+ let launchedOrder = false;
468
+ let orders = [];
469
+ let shipments = 0;
470
+ let inspections = 0;
471
+ if (params.demo) {
472
+ const launch = await postSmokeJson(baseUrl, {
473
+ op: "action",
474
+ action: "supplyChain.order.launch",
475
+ input: {
476
+ reference: "PO-SMOKE-7842",
477
+ sku: "DRV-2048",
478
+ supplierName: "Marula Components",
479
+ },
480
+ admin: true,
481
+ });
482
+ if (String(launch.action ?? "") !== "supplyChain.order.launch") {
483
+ throw new Error("Smoke launch order action returned an unexpected action");
484
+ }
485
+ launchedOrder = true;
486
+ const query = await postSmokeJson(baseUrl, {
487
+ op: "query",
488
+ query: {
489
+ procurement_order: {
490
+ supplier: {},
491
+ stockItems: {},
492
+ shipments: {
493
+ inspections: {},
494
+ },
495
+ },
496
+ },
497
+ admin: true,
498
+ });
499
+ orders = asArray(query?.data?.procurement_order);
500
+ shipments = orders.reduce((total, order) => {
501
+ return total + asArray(order?.shipments).length;
502
+ }, 0);
503
+ inspections = orders.reduce((total, order) => {
504
+ const orderShipments = asArray(order?.shipments);
505
+ return total + orderShipments.reduce((shipmentTotal, shipment) => {
506
+ return shipmentTotal + asArray(shipment?.inspections).length;
507
+ }, 0);
508
+ }, 0);
509
+ }
510
+ smokePassed = true;
511
+ const result = {
512
+ ok: true,
513
+ baseUrl,
514
+ keepServer: params.keepServer,
515
+ pid: detachedPid ?? child?.pid ?? null,
516
+ typecheck: true,
517
+ domainEndpoint: true,
518
+ inspect: {
519
+ entities: countCollection(manifest?.domain?.entities),
520
+ actions: countCollection(manifest?.actions),
521
+ },
522
+ launchedOrder,
523
+ query: {
524
+ inspections,
525
+ orders: orders.length,
526
+ shipments,
527
+ },
528
+ };
529
+ await emitProgress(params.onProgress, {
530
+ stage: "smoke",
531
+ status: "completed",
532
+ message: params.keepServer
533
+ ? `Smoke passed and server is running at ${baseUrl}`
534
+ : "Smoke passed",
535
+ progress: 99,
536
+ });
537
+ if (params.keepServer) {
538
+ params.onKeepServer?.({
539
+ unref() {
540
+ child?.unref();
541
+ },
542
+ });
543
+ }
544
+ return result;
545
+ }
546
+ finally {
547
+ if (!params.keepServer || !smokePassed) {
548
+ stopProcess(detachedPid ?? child?.pid);
549
+ }
550
+ }
551
+ }
136
552
  async function provisionInstantApp(params) {
137
553
  const api = new PlatformApi({
138
554
  auth: { token: params.instantToken },
@@ -248,6 +664,7 @@ function buildNextTemplateFiles(params) {
248
664
  "@ekairos/domain": domainDependency,
249
665
  "@instantdb/admin": TEMPLATE_INSTANT_VERSION,
250
666
  "@instantdb/core": TEMPLATE_INSTANT_VERSION,
667
+ "@instantdb/react": TEMPLATE_INSTANT_REACT_VERSION,
251
668
  next: TEMPLATE_NEXT_VERSION,
252
669
  react: TEMPLATE_REACT_VERSION,
253
670
  "react-dom": TEMPLATE_REACT_VERSION,
@@ -265,6 +682,243 @@ function buildNextTemplateFiles(params) {
265
682
  ? "yarn@1"
266
683
  : undefined,
267
684
  };
685
+ if (!params.demo) {
686
+ return {
687
+ ".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
688
+ ".env.example": [
689
+ "NEXT_PUBLIC_INSTANT_APP_ID=",
690
+ "INSTANT_ADMIN_TOKEN=",
691
+ "",
692
+ "# Optional: use this only while provisioning new apps with the CLI.",
693
+ "INSTANT_PERSONAL_ACCESS_TOKEN=",
694
+ ].join("\n"),
695
+ "DOMAIN.md": [
696
+ "# Ekairos App Domain",
697
+ "",
698
+ "This app starts empty on purpose.",
699
+ "",
700
+ "Add your first domain in `src/domain.ts`, then expose it through `src/runtime.ts` and `/api/ekairos/domain`.",
701
+ "",
702
+ "Suggested first step:",
703
+ "- create one domain with camelCase name",
704
+ "- name entities as `<domainName>_<entityName>`",
705
+ "- add one `defineAction` for the first business write",
706
+ ].join("\n"),
707
+ "instant.schema.ts": [
708
+ 'import appDomain from "./src/domain";',
709
+ "",
710
+ "const schema = appDomain.toInstantSchema();",
711
+ "",
712
+ "export default schema;",
713
+ ].join("\n"),
714
+ "next-env.d.ts": [
715
+ '/// <reference types="next" />',
716
+ '/// <reference types="next/image-types/global" />',
717
+ "",
718
+ "// This file is managed by Next.js.",
719
+ ].join("\n"),
720
+ "next.config.ts": [
721
+ 'import type { NextConfig } from "next";',
722
+ 'import { withWorkflow } from "workflow/next";',
723
+ "",
724
+ "const nextConfig: NextConfig = {",
725
+ " transpilePackages: [\"@ekairos/domain\"],",
726
+ "};",
727
+ "",
728
+ "export default withWorkflow(nextConfig) as NextConfig;",
729
+ ].join("\n"),
730
+ "src/app/api/ekairos/domain/route.ts": [
731
+ 'import { createRuntimeRouteHandler } from "@ekairos/domain/next";',
732
+ 'import { createRuntime } from "@/runtime";',
733
+ "",
734
+ "export const { GET, POST } = createRuntimeRouteHandler({",
735
+ " createRuntime,",
736
+ "});",
737
+ ].join("\n"),
738
+ "package.json": `${JSON.stringify(packageJson, null, 2)}\n`,
739
+ "tsconfig.json": [
740
+ "{",
741
+ ' "compilerOptions": {',
742
+ ' "target": "ES2022",',
743
+ ' "lib": ["dom", "dom.iterable", "es2022"],',
744
+ ' "allowJs": false,',
745
+ ' "skipLibCheck": true,',
746
+ ' "strict": true,',
747
+ ' "noEmit": true,',
748
+ ' "esModuleInterop": true,',
749
+ ' "module": "esnext",',
750
+ ' "moduleResolution": "bundler",',
751
+ ' "resolveJsonModule": true,',
752
+ ' "isolatedModules": true,',
753
+ ' "jsx": "preserve",',
754
+ ' "incremental": true,',
755
+ ' "baseUrl": ".",',
756
+ ' "paths": {',
757
+ ' "@/*": ["./src/*"]',
758
+ " }",
759
+ " },",
760
+ ' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],',
761
+ ' "exclude": ["node_modules"]',
762
+ "}",
763
+ ].join("\n"),
764
+ "src/app/globals.css": [
765
+ ":root {",
766
+ " color-scheme: light;",
767
+ " --background: #f7f8f6;",
768
+ " --foreground: #181b18;",
769
+ " --muted: #626a63;",
770
+ " --border: #d9ded7;",
771
+ " --surface: #ffffff;",
772
+ " --accent: #2f6f5d;",
773
+ "}",
774
+ "",
775
+ "* { box-sizing: border-box; }",
776
+ "html, body { margin: 0; min-height: 100%; }",
777
+ "body { min-height: 100dvh; background: var(--background); color: var(--foreground); font-family: \"Segoe UI\", sans-serif; }",
778
+ "button, input { font: inherit; }",
779
+ "main { width: min(980px, calc(100% - 40px)); margin: 0 auto; padding: 48px 0; }",
780
+ ".shell { display: grid; gap: 24px; }",
781
+ ".eyebrow { color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }",
782
+ "h1 { max-width: 720px; margin: 0; font-size: clamp(2.4rem, 6vw, 4.8rem); letter-spacing: -0.05em; line-height: 0.96; }",
783
+ "p { max-width: 64ch; margin: 0; color: var(--muted); line-height: 1.65; }",
784
+ ".workspace { display: grid; gap: 14px; border: 1px solid var(--border); background: var(--surface); padding: 22px; }",
785
+ ".status-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border: 1px solid var(--border); }",
786
+ ".status-grid div { display: grid; gap: 6px; padding: 14px; border-right: 1px solid var(--border); }",
787
+ ".status-grid div:last-child { border-right: 0; }",
788
+ ".status-grid span { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }",
789
+ ".status-grid strong { font-size: 1.4rem; }",
790
+ ".next-steps { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none; }",
791
+ ".next-steps li { border-top: 1px solid var(--border); padding-top: 10px; color: var(--muted); }",
792
+ "code { font-family: \"Cascadia Code\", monospace; }",
793
+ "@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); } }",
794
+ ].join("\n"),
795
+ "src/app/layout.tsx": [
796
+ 'import "./globals.css";',
797
+ 'import type { ReactNode } from "react";',
798
+ "",
799
+ "export const metadata = {",
800
+ ' title: "Ekairos App",',
801
+ ' description: "Scaffolded Ekairos app",',
802
+ "};",
803
+ "",
804
+ "export default function RootLayout({ children }: { children: ReactNode }) {",
805
+ " return (",
806
+ ' <html lang="en">',
807
+ " <body>{children}</body>",
808
+ " </html>",
809
+ " );",
810
+ "}",
811
+ ].join("\n"),
812
+ "src/app/page.tsx": [
813
+ 'import DomainWorkbench from "./domain-workbench";',
814
+ "",
815
+ 'export const dynamic = "force-dynamic";',
816
+ "",
817
+ "export default function HomePage() {",
818
+ " return (",
819
+ " <main>",
820
+ ' <section className="shell">',
821
+ ' <div className="eyebrow">Ekairos App</div>',
822
+ " <h1>Empty app. Runtime ready.</h1>",
823
+ " <p>",
824
+ " Start here when you want a clean app with the Ekairos runtime,",
825
+ " domain endpoint, Instant configuration, and a place to add your first domain.",
826
+ " </p>",
827
+ " <DomainWorkbench />",
828
+ " </section>",
829
+ " </main>",
830
+ " );",
831
+ "}",
832
+ ].join("\n"),
833
+ "src/app/domain-workbench.tsx": [
834
+ "export default function DomainWorkbench() {",
835
+ " return (",
836
+ ' <section className="workspace">',
837
+ ' <div className="status-grid">',
838
+ " <div><span>Runtime</span><strong>Ready</strong></div>",
839
+ " <div><span>Endpoint</span><strong>/api</strong></div>",
840
+ " <div><span>Domains</span><strong>0</strong></div>",
841
+ " </div>",
842
+ " <p>Add your first domain in <code>src/domain.ts</code>. Keep writes behind typed domain actions.</p>",
843
+ ' <ul className="next-steps">',
844
+ " <li>Name the domain in camelCase.</li>",
845
+ " <li>Name entities as <code>{\"<domainName>_<entityName>\"}</code>.</li>",
846
+ " <li>Expose the first write as a <code>defineAction</code>.</li>",
847
+ " </ul>",
848
+ " </section>",
849
+ " );",
850
+ "}",
851
+ ].join("\n"),
852
+ "src/domain.ts": [
853
+ 'import { domain } from "@ekairos/domain";',
854
+ "",
855
+ "const baseDomain = domain(\"app\").withSchema({",
856
+ " entities: {},",
857
+ " links: {},",
858
+ " rooms: {},",
859
+ "});",
860
+ "",
861
+ "export const appDomain = baseDomain.withActions({});",
862
+ "",
863
+ "export default appDomain;",
864
+ ].join("\n"),
865
+ "src/runtime.ts": [
866
+ 'import { init } from "@instantdb/admin";',
867
+ 'import { EkairosRuntime } from "@ekairos/domain/runtime-handle";',
868
+ 'import { configureRuntime } from "@ekairos/domain/runtime";',
869
+ 'import appDomain from "./domain";',
870
+ "",
871
+ "export type AppRuntimeEnv = {",
872
+ " actorEmail?: string | null;",
873
+ " actorId?: string;",
874
+ " adminToken?: string;",
875
+ " appId?: string;",
876
+ "};",
877
+ "",
878
+ "function resolveRuntimeEnv(env: AppRuntimeEnv = {}): Required<Pick<AppRuntimeEnv, \"appId\" | \"adminToken\">> & AppRuntimeEnv {",
879
+ ' const appId = String(env.appId ?? process.env.NEXT_PUBLIC_INSTANT_APP_ID ?? "").trim();',
880
+ ' const adminToken = String(env.adminToken ?? process.env.INSTANT_ADMIN_TOKEN ?? "").trim();',
881
+ " if (!appId || !adminToken) {",
882
+ ' throw new Error("Missing NEXT_PUBLIC_INSTANT_APP_ID or INSTANT_ADMIN_TOKEN. Copy .env.example to .env.local and fill both values.");',
883
+ " }",
884
+ " return { ...env, appId, adminToken };",
885
+ "}",
886
+ "",
887
+ "export class AppRuntime extends EkairosRuntime<AppRuntimeEnv, typeof appDomain, any> {",
888
+ " protected getDomain() { return appDomain; }",
889
+ " protected async resolveDb(env: AppRuntimeEnv) {",
890
+ " const resolved = resolveRuntimeEnv(env);",
891
+ " return init({",
892
+ " appId: resolved.appId,",
893
+ " adminToken: resolved.adminToken,",
894
+ " schema: appDomain.toInstantSchema(),",
895
+ " useDateObjects: true,",
896
+ " } as any) as any;",
897
+ " }",
898
+ "}",
899
+ "",
900
+ "export function createRuntime(env: AppRuntimeEnv = {}) {",
901
+ " return new AppRuntime(resolveRuntimeEnv(env));",
902
+ "}",
903
+ "",
904
+ "export const runtimeConfig = configureRuntime<AppRuntimeEnv>({",
905
+ " runtime: async (env) => {",
906
+ " const runtime = createRuntime(env);",
907
+ " return { db: await runtime.db() };",
908
+ " },",
909
+ " domain: { domain: appDomain },",
910
+ "});",
911
+ ].join("\n"),
912
+ "src/workflows/demo.workflow.ts": [
913
+ "export type DemoWorkflowInput = Record<string, never>;",
914
+ "",
915
+ "export async function runDemoWorkflow(_input: DemoWorkflowInput) {",
916
+ ' "use workflow";',
917
+ " return { ok: true };",
918
+ "}",
919
+ ].join("\n"),
920
+ };
921
+ }
268
922
  return {
269
923
  ".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
270
924
  ".env.example": [
@@ -275,18 +929,18 @@ function buildNextTemplateFiles(params) {
275
929
  "INSTANT_PERSONAL_ACCESS_TOKEN=",
276
930
  ].join("\n"),
277
931
  "DOMAIN.md": [
278
- "# Ekairos App Domain",
932
+ "# Ekairos Supply Chain Domain",
279
933
  "",
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",
934
+ "This scaffold ships a supply-chain control tower backed by separate domains:",
935
+ "- `supplierNetwork` owns supplier risk and score",
936
+ "- `procurement` owns purchase orders",
937
+ "- `inventory` owns stock position",
938
+ "- `transportation` owns shipments and ETA",
939
+ "- `qualityControl` owns arrival inspection",
285
940
  "",
286
941
  "Actions:",
287
- "- `createTask` -> create one task",
288
- "- `addTaskComment` -> attach one comment to a task",
289
- "- `seedDemo` -> create demo data fast",
942
+ "- `launchOrder` -> creates and links supplier, order, stock, shipment, and inspection",
943
+ "- `expediteShipment` -> updates shipment status and ETA",
290
944
  ].join("\n"),
291
945
  "instant.schema.ts": [
292
946
  'import appDomain from "./src/domain";',
@@ -348,15 +1002,16 @@ function buildNextTemplateFiles(params) {
348
1002
  "src/app/globals.css": [
349
1003
  ":root {",
350
1004
  " 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;",
1005
+ " --bg: #f7f8f6;",
1006
+ " --surface: #ffffff;",
1007
+ " --surface-2: #f0f2ef;",
1008
+ " --ink: #171a18;",
1009
+ " --muted: #646b66;",
1010
+ " --accent: #26715f;",
1011
+ " --accent-soft: #dfece7;",
1012
+ " --border: #d9ded9;",
1013
+ " --line: #b9c1ba;",
1014
+ " --danger: #9f2f28;",
360
1015
  "}",
361
1016
  "",
362
1017
  "* {",
@@ -367,15 +1022,13 @@ function buildNextTemplateFiles(params) {
367
1022
  "body {",
368
1023
  " margin: 0;",
369
1024
  " 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%);",
1025
+ " background: var(--bg);",
373
1026
  " color: var(--ink);",
374
- ' font-family: "Segoe UI", sans-serif;',
1027
+ " font-family: \"Geist\", \"Aptos\", \"Segoe UI\", sans-serif;",
375
1028
  "}",
376
1029
  "",
377
1030
  "body {",
378
- " min-height: 100vh;",
1031
+ " min-height: 100dvh;",
379
1032
  "}",
380
1033
  "",
381
1034
  "button,",
@@ -384,242 +1037,360 @@ function buildNextTemplateFiles(params) {
384
1037
  "}",
385
1038
  "",
386
1039
  "main {",
387
- " max-width: 1180px;",
1040
+ " width: min(1240px, calc(100% - 40px));",
388
1041
  " margin: 0 auto;",
389
- " padding: 48px 24px 72px;",
1042
+ " padding: 44px 0 64px;",
390
1043
  "}",
391
1044
  "",
392
1045
  ".hero {",
393
1046
  " display: grid;",
394
- " gap: 18px;",
395
- " max-width: 860px;",
1047
+ " gap: 14px;",
1048
+ " max-width: 720px;",
1049
+ " margin-bottom: 28px;",
396
1050
  "}",
397
1051
  "",
398
1052
  ".eyebrow {",
399
1053
  " color: var(--accent);",
400
- " font-size: 12px;",
401
- " font-weight: 700;",
402
- " letter-spacing: 0.2em;",
1054
+ " font-size: 11px;",
1055
+ " font-weight: 800;",
1056
+ " letter-spacing: 0.16em;",
403
1057
  " text-transform: uppercase;",
404
1058
  "}",
405
1059
  "",
406
1060
  "h1,",
407
1061
  "h2 {",
408
1062
  " margin: 0;",
409
- " line-height: 0.96;",
1063
+ " letter-spacing: -0.04em;",
410
1064
  "}",
411
1065
  "",
412
1066
  "h1 {",
413
- " font-size: clamp(2.7rem, 6vw, 5.4rem);",
1067
+ " max-width: 640px;",
1068
+ " font-size: clamp(2.4rem, 5.4vw, 4.8rem);",
1069
+ " line-height: 0.96;",
414
1070
  "}",
415
1071
  "",
416
1072
  "h2 {",
417
- " font-size: 1.4rem;",
1073
+ " font-size: 1.28rem;",
1074
+ " line-height: 1.05;",
418
1075
  "}",
419
1076
  "",
420
1077
  "p {",
421
1078
  " margin: 0;",
422
1079
  " color: var(--muted);",
423
- " line-height: 1.65;",
1080
+ " line-height: 1.6;",
424
1081
  "}",
425
1082
  "",
426
1083
  ".shell {",
427
1084
  " display: grid;",
428
- " gap: 20px;",
429
- " margin-top: 32px;",
1085
+ " gap: 14px;",
430
1086
  "}",
431
1087
  "",
432
- ".stat-grid {",
1088
+ ".metrics-strip {",
433
1089
  " display: grid;",
434
- " gap: 16px;",
435
- " grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));",
1090
+ " grid-template-columns: repeat(4, minmax(0, 1fr));",
1091
+ " border: 1px solid var(--border);",
1092
+ " background: var(--surface);",
436
1093
  "}",
437
1094
  "",
438
- ".grid {",
1095
+ ".metrics-strip div {",
439
1096
  " display: grid;",
440
- " gap: 20px;",
1097
+ " gap: 6px;",
1098
+ " padding: 16px;",
1099
+ " border-right: 1px solid var(--border);",
441
1100
  "}",
442
1101
  "",
443
- ".showcase-grid {",
444
- " align-items: start;",
1102
+ ".metrics-strip div:last-child {",
1103
+ " border-right: 0;",
445
1104
  "}",
446
1105
  "",
447
- ".panel-tall,",
448
- ".panel-wide,",
449
- ".stat-card {",
450
- " min-height: 100%;",
1106
+ ".metrics-strip span,",
1107
+ ".manifest-label {",
1108
+ " color: var(--muted);",
1109
+ " font-size: 11px;",
1110
+ " font-weight: 800;",
1111
+ " letter-spacing: 0.12em;",
1112
+ " text-transform: uppercase;",
451
1113
  "}",
452
1114
  "",
453
- ".card {",
454
- " background: linear-gradient(180deg, var(--panel) 0%, var(--panel-strong) 100%);",
1115
+ ".metrics-strip strong {",
1116
+ " font-size: 1.85rem;",
1117
+ " letter-spacing: -0.04em;",
1118
+ " line-height: 1;",
1119
+ "}",
1120
+ "",
1121
+ ".workbench {",
1122
+ " display: grid;",
1123
+ " grid-template-columns: 0.8fr 1fr;",
1124
+ " gap: 14px;",
1125
+ " align-items: start;",
1126
+ "}",
1127
+ "",
1128
+ ".context-rail,",
1129
+ ".command-panel,",
1130
+ ".graph-panel {",
455
1131
  " border: 1px solid var(--border);",
456
- " border-radius: 24px;",
457
- " padding: 22px;",
458
- " box-shadow: 0 18px 45px rgba(23, 23, 23, 0.06);",
1132
+ " background: var(--surface);",
459
1133
  "}",
460
1134
  "",
461
- ".stat-card strong {",
462
- " display: block;",
463
- " margin-top: 8px;",
464
- " font-size: 2rem;",
1135
+ ".context-rail,",
1136
+ ".command-panel {",
1137
+ " padding: 18px;",
1138
+ "}",
1139
+ "",
1140
+ ".graph-panel {",
1141
+ " grid-column: 1 / -1;",
1142
+ " padding: 18px;",
465
1143
  "}",
466
1144
  "",
467
1145
  ".panel-head {",
468
1146
  " display: flex;",
469
- " align-items: flex-start;",
470
1147
  " justify-content: space-between;",
471
1148
  " gap: 16px;",
472
- " margin-bottom: 12px;",
1149
+ " align-items: start;",
1150
+ " margin-bottom: 16px;",
473
1151
  "}",
474
1152
  "",
475
- ".manifest-list,",
476
- ".action-list,",
477
- ".task-list,",
478
- ".comment-list {",
1153
+ ".context-list,",
1154
+ ".project-list,",
1155
+ ".task-lines,",
1156
+ ".lane-grid,",
1157
+ ".calls-feed,",
1158
+ ".button-row {",
479
1159
  " display: grid;",
480
- " gap: 12px;",
1160
+ " gap: 10px;",
481
1161
  "}",
482
1162
  "",
483
- ".manifest-list {",
484
- " margin: 18px 0;",
1163
+ ".context-row {",
1164
+ " display: grid;",
1165
+ " grid-template-columns: 0.7fr 1fr;",
1166
+ " gap: 14px;",
1167
+ " padding: 12px 0;",
1168
+ " border-top: 1px solid var(--border);",
1169
+ " opacity: 0;",
1170
+ " transform: translateY(8px);",
1171
+ " animation: lift-in 420ms cubic-bezier(0.16, 1, 0.3, 1) forwards;",
1172
+ " animation-delay: calc(var(--index) * 70ms);",
485
1173
  "}",
486
1174
  "",
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;",
1175
+ ".context-row span {",
1176
+ " font-weight: 800;",
493
1177
  "}",
494
1178
  "",
495
- ".manifest-label {",
1179
+ ".context-row strong {",
496
1180
  " color: var(--muted);",
497
- " font-weight: 600;",
1181
+ " font-size: 0.92rem;",
1182
+ " font-weight: 500;",
498
1183
  "}",
499
1184
  "",
500
1185
  ".field {",
501
1186
  " display: grid;",
502
- " gap: 8px;",
503
- " margin: 18px 0;",
1187
+ " gap: 7px;",
1188
+ " margin: 14px 0;",
504
1189
  "}",
505
1190
  "",
506
1191
  ".field span {",
507
- " font-size: 0.92rem;",
508
- " font-weight: 600;",
1192
+ " color: var(--muted);",
1193
+ " font-size: 0.85rem;",
1194
+ " font-weight: 700;",
509
1195
  "}",
510
1196
  "",
511
1197
  ".input {",
512
1198
  " width: 100%;",
513
1199
  " border: 1px solid var(--border);",
514
- " border-radius: 16px;",
515
- " padding: 14px 16px;",
516
- " background: #fff;",
1200
+ " border-radius: 6px;",
1201
+ " padding: 12px 13px;",
1202
+ " background: var(--surface-2);",
517
1203
  " color: var(--ink);",
1204
+ " outline: none;",
1205
+ "}",
1206
+ "",
1207
+ ".input:focus {",
1208
+ " border-color: var(--accent);",
1209
+ " background: var(--surface);",
518
1210
  "}",
519
1211
  "",
520
1212
  ".button-row {",
521
- " display: flex;",
522
- " flex-wrap: wrap;",
523
- " gap: 12px;",
524
- " margin-bottom: 18px;",
1213
+ " grid-template-columns: repeat(2, minmax(0, 1fr));",
1214
+ " margin-top: 16px;",
525
1215
  "}",
526
1216
  "",
527
1217
  ".button {",
528
1218
  " appearance: none;",
529
- " border: 0;",
530
- " border-radius: 999px;",
531
- " padding: 12px 18px;",
1219
+ " border: 1px solid var(--ink);",
1220
+ " border-radius: 6px;",
1221
+ " padding: 12px 14px;",
532
1222
  " background: var(--ink);",
533
- " color: #fffdf7;",
534
- " display: inline-flex;",
535
- " align-items: center;",
536
- " justify-content: center;",
537
- " gap: 10px;",
1223
+ " color: #ffffff;",
538
1224
  " cursor: pointer;",
539
- " transition: transform 120ms ease, opacity 120ms ease, background 120ms ease;",
1225
+ " font-weight: 800;",
1226
+ " transition: transform 160ms cubic-bezier(0.16, 1, 0.3, 1), opacity 160ms ease;",
540
1227
  "}",
541
1228
  "",
542
1229
  ".button:hover:not(:disabled) {",
543
1230
  " transform: translateY(-1px);",
544
1231
  "}",
545
1232
  "",
1233
+ ".button:active:not(:disabled) {",
1234
+ " transform: translateY(1px) scale(0.99);",
1235
+ "}",
1236
+ "",
546
1237
  ".button:disabled {",
547
1238
  " cursor: wait;",
548
- " opacity: 0.72;",
1239
+ " opacity: 0.62;",
549
1240
  "}",
550
1241
  "",
551
1242
  ".button.ghost {",
552
- " background: transparent;",
1243
+ " border-color: var(--border);",
1244
+ " background: var(--surface);",
553
1245
  " 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
1246
  "}",
570
1247
  "",
571
1248
  ".status-pill {",
572
1249
  " display: inline-flex;",
573
1250
  " align-items: center;",
574
1251
  " justify-content: center;",
575
- " padding: 6px 10px;",
576
- " border-radius: 999px;",
1252
+ " min-width: 86px;",
1253
+ " padding: 7px 10px;",
1254
+ " border-radius: 6px;",
577
1255
  " background: var(--accent-soft);",
578
1256
  " color: var(--accent);",
579
- " font-size: 12px;",
580
- " font-weight: 700;",
581
- " letter-spacing: 0.04em;",
1257
+ " font-size: 11px;",
1258
+ " font-weight: 800;",
1259
+ " letter-spacing: 0.08em;",
582
1260
  " text-transform: uppercase;",
583
1261
  "}",
584
1262
  "",
585
- ".task-card {",
1263
+ ".project-row {",
586
1264
  " 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);",
1265
+ " grid-template-columns: 1fr auto;",
1266
+ " gap: 14px;",
1267
+ " padding: 16px 0;",
1268
+ " border-top: 1px solid var(--border);",
1269
+ "}",
1270
+ "",
1271
+ ".project-row:first-child {",
1272
+ " border-top: 0;",
1273
+ "}",
1274
+ "",
1275
+ ".project-row strong {",
1276
+ " display: block;",
1277
+ " font-size: 1.05rem;",
1278
+ "}",
1279
+ "",
1280
+ ".project-row span {",
1281
+ " color: var(--muted);",
592
1282
  "}",
593
1283
  "",
594
- ".task-head {",
1284
+ ".project-meta {",
595
1285
  " display: flex;",
596
- " align-items: center;",
597
- " justify-content: space-between;",
1286
+ " flex-wrap: wrap;",
1287
+ " gap: 8px;",
1288
+ " justify-content: flex-end;",
1289
+ "}",
1290
+ "",
1291
+ ".project-meta span {",
1292
+ " border: 1px solid var(--border);",
1293
+ " border-radius: 6px;",
1294
+ " padding: 5px 8px;",
1295
+ " color: var(--ink);",
1296
+ " font-size: 0.82rem;",
1297
+ " font-weight: 700;",
1298
+ "}",
1299
+ "",
1300
+ ".task-lines {",
1301
+ " grid-column: 1 / -1;",
1302
+ " padding-top: 2px;",
1303
+ "}",
1304
+ "",
1305
+ ".task-lines p {",
1306
+ " display: grid;",
1307
+ " grid-template-columns: minmax(180px, 0.8fr) minmax(180px, 1fr);",
598
1308
  " gap: 12px;",
1309
+ " padding-left: 14px;",
1310
+ " border-left: 2px solid var(--line);",
599
1311
  "}",
600
1312
  "",
601
- ".comment-item {",
602
- " display: flex;",
603
- " align-items: flex-start;",
604
- " gap: 10px;",
1313
+ ".task-lines b {",
1314
+ " color: var(--ink);",
1315
+ "}",
1316
+ "",
1317
+ ".task-lines em {",
1318
+ " grid-column: 1 / -1;",
605
1319
  " color: var(--muted);",
1320
+ " font-style: normal;",
606
1321
  "}",
607
1322
  "",
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;",
1323
+ ".raid-summary {",
1324
+ " display: grid;",
1325
+ " grid-template-columns: repeat(3, minmax(0, 1fr));",
1326
+ " border: 1px solid var(--border);",
1327
+ " margin-bottom: 14px;",
615
1328
  "}",
616
1329
  "",
617
- ".action-item {",
1330
+ ".raid-summary div {",
618
1331
  " display: grid;",
619
- " gap: 4px;",
620
- " padding: 12px 14px;",
621
- " border-radius: 16px;",
622
- " background: rgba(15, 118, 110, 0.06);",
1332
+ " gap: 6px;",
1333
+ " padding: 13px;",
1334
+ " border-right: 1px solid var(--border);",
1335
+ "}",
1336
+ "",
1337
+ ".raid-summary div:last-child {",
1338
+ " border-right: 0;",
1339
+ "}",
1340
+ "",
1341
+ ".raid-summary span {",
1342
+ " color: var(--muted);",
1343
+ " font-size: 11px;",
1344
+ " font-weight: 800;",
1345
+ " letter-spacing: 0.12em;",
1346
+ " text-transform: uppercase;",
1347
+ "}",
1348
+ "",
1349
+ ".raid-summary strong {",
1350
+ " font-size: 1rem;",
1351
+ "}",
1352
+ "",
1353
+ ".lane-grid {",
1354
+ " grid-template-columns: repeat(3, minmax(0, 1fr));",
1355
+ "}",
1356
+ "",
1357
+ ".lane {",
1358
+ " min-height: 180px;",
1359
+ " border: 1px solid var(--border);",
1360
+ " padding: 12px;",
1361
+ " background: var(--surface-2);",
1362
+ "}",
1363
+ "",
1364
+ ".objective {",
1365
+ " display: grid;",
1366
+ " gap: 5px;",
1367
+ " margin-top: 10px;",
1368
+ " padding: 10px;",
1369
+ " border: 1px solid var(--border);",
1370
+ " background: var(--surface);",
1371
+ "}",
1372
+ "",
1373
+ ".objective span,",
1374
+ ".objective em {",
1375
+ " color: var(--muted);",
1376
+ " font-size: 0.88rem;",
1377
+ " font-style: normal;",
1378
+ "}",
1379
+ "",
1380
+ ".calls-feed {",
1381
+ " margin-top: 14px;",
1382
+ " padding-top: 14px;",
1383
+ " border-top: 1px solid var(--border);",
1384
+ "}",
1385
+ "",
1386
+ ".calls-feed p {",
1387
+ " display: flex;",
1388
+ " justify-content: space-between;",
1389
+ " gap: 16px;",
1390
+ "}",
1391
+ "",
1392
+ ".calls-feed b {",
1393
+ " color: var(--ink);",
623
1394
  "}",
624
1395
  "",
625
1396
  ".muted {",
@@ -628,59 +1399,100 @@ function buildNextTemplateFiles(params) {
628
1399
  "",
629
1400
  ".empty-state,",
630
1401
  ".error-banner {",
631
- " border-radius: 18px;",
632
- " padding: 16px;",
1402
+ " border: 1px solid var(--border);",
1403
+ " border-radius: 6px;",
1404
+ " padding: 14px;",
633
1405
  "}",
634
1406
  "",
635
1407
  ".empty-state {",
636
- " background: rgba(15, 118, 110, 0.06);",
1408
+ " background: var(--surface-2);",
637
1409
  " color: var(--muted);",
638
1410
  "}",
639
1411
  "",
640
1412
  ".error-banner {",
641
- " background: rgba(180, 35, 24, 0.08);",
1413
+ " background: #fff4f2;",
642
1414
  " color: var(--danger);",
643
- " border: 1px solid rgba(180, 35, 24, 0.16);",
1415
+ " border-color: #efc4bd;",
1416
+ "}",
1417
+ "",
1418
+ ".skeleton-stack {",
1419
+ " display: grid;",
1420
+ " gap: 10px;",
1421
+ "}",
1422
+ "",
1423
+ ".skeleton-stack span {",
1424
+ " display: block;",
1425
+ " height: 54px;",
1426
+ " border-radius: 6px;",
1427
+ " background: linear-gradient(90deg, var(--surface-2), #ffffff, var(--surface-2));",
1428
+ " background-size: 220% 100%;",
1429
+ " animation: shimmer 1.2s ease-in-out infinite;",
644
1430
  "}",
645
1431
  "",
646
1432
  "pre {",
647
1433
  " overflow: auto;",
648
- " border-radius: 16px;",
649
- " padding: 14px;",
650
- " background: #171717;",
651
- " color: #f6f6f6;",
652
- " font-size: 13px;",
653
- " line-height: 1.5;",
1434
+ " margin-top: 14px;",
1435
+ " border-radius: 6px;",
1436
+ " padding: 12px;",
1437
+ " background: #1d211f;",
1438
+ " color: #eef5f1;",
1439
+ " font-size: 12px;",
1440
+ " line-height: 1.45;",
654
1441
  "}",
655
1442
  "",
656
1443
  "code {",
657
- ' font-family: "Cascadia Code", monospace;',
658
- "}",
659
- "",
660
- "@keyframes spin {",
661
- " to { transform: rotate(360deg); }",
1444
+ " font-family: \"Geist Mono\", \"Cascadia Code\", monospace;",
662
1445
  "}",
663
1446
  "",
664
- "@media (min-width: 940px) {",
665
- " .showcase-grid {",
666
- " grid-template-columns: 1.05fr 0.95fr;",
1447
+ "@keyframes lift-in {",
1448
+ " to {",
1449
+ " opacity: 1;",
1450
+ " transform: translateY(0);",
667
1451
  " }",
1452
+ "}",
668
1453
  "",
669
- " .panel-wide {",
670
- " grid-column: 1 / -1;",
1454
+ "@keyframes shimmer {",
1455
+ " to {",
1456
+ " background-position: -220% 0;",
671
1457
  " }",
672
1458
  "}",
673
1459
  "",
674
- "@media (max-width: 720px) {",
1460
+ "@media (max-width: 820px) {",
675
1461
  " main {",
676
- " padding: 32px 16px 56px;",
1462
+ " width: min(100% - 28px, 1240px);",
1463
+ " padding: 32px 0 52px;",
677
1464
  " }",
678
1465
  "",
679
- " .task-head,",
680
- " .manifest-list > div,",
681
- " .panel-head {",
1466
+ " .metrics-strip,",
1467
+ " .workbench,",
1468
+ " .button-row,",
1469
+ " .raid-summary,",
1470
+ " .lane-grid,",
1471
+ " .project-row,",
1472
+ " .task-lines p {",
682
1473
  " grid-template-columns: 1fr;",
683
- " display: grid;",
1474
+ " }",
1475
+ "",
1476
+ " .metrics-strip div {",
1477
+ " border-right: 0;",
1478
+ " border-bottom: 1px solid var(--border);",
1479
+ " }",
1480
+ "",
1481
+ " .metrics-strip div:last-child {",
1482
+ " border-bottom: 0;",
1483
+ " }",
1484
+ "",
1485
+ " .project-meta {",
1486
+ " justify-content: flex-start;",
1487
+ " }",
1488
+ "",
1489
+ " .raid-summary div {",
1490
+ " border-right: 0;",
1491
+ " border-bottom: 1px solid var(--border);",
1492
+ " }",
1493
+ "",
1494
+ " .raid-summary div:last-child {",
1495
+ " border-bottom: 0;",
684
1496
  " }",
685
1497
  "}",
686
1498
  ].join("\n"),
@@ -702,19 +1514,18 @@ function buildNextTemplateFiles(params) {
702
1514
  "}",
703
1515
  ].join("\n"),
704
1516
  "src/app/page.tsx": [
705
- 'import DomainShowcase from "./domain-showcase";',
1517
+ "import DomainShowcase from \"./domain-showcase\";",
706
1518
  "",
707
- 'export const dynamic = "force-dynamic";',
1519
+ "export const dynamic = \"force-dynamic\";",
708
1520
  "",
709
1521
  "export default function HomePage() {",
710
1522
  " return (",
711
1523
  " <main>",
712
- ' <section className="hero">',
713
- ' <div className="eyebrow">Ekairos Domain Scaffold</div>',
714
- " <h1>See your domain. Query it. Trigger an action.</h1>",
1524
+ " <section className=\"hero\">",
1525
+ " <div className=\"eyebrow\">Ekairos Domain Scaffold</div>",
1526
+ " <h1>Supply chain control tower.</h1>",
715
1527
  " <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.",
1528
+ " Open an order, track stock, shipment, supplier risk, and quality status in one live view.",
718
1529
  " </p>",
719
1530
  " </section>",
720
1531
  "",
@@ -724,352 +1535,471 @@ function buildNextTemplateFiles(params) {
724
1535
  "}",
725
1536
  ].join("\n"),
726
1537
  "src/app/domain-showcase.tsx": [
727
- '"use client";',
1538
+ "\"use client\";",
728
1539
  "",
729
- 'import { useEffect, useMemo, useState } from "react";',
1540
+ "import { useMemo, useState } from \"react\";",
1541
+ "import { init } from \"@instantdb/react\";",
730
1542
  "",
731
- "type ManifestAction = {",
732
- " name: string;",
733
- " key?: string | null;",
734
- " description?: string | null;",
1543
+ "type SupplierRow = {",
1544
+ " id?: string;",
1545
+ " name?: string;",
1546
+ " region?: string;",
1547
+ " risk?: string;",
1548
+ " score?: number;",
735
1549
  "};",
736
1550
  "",
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[];",
1551
+ "type StockItemRow = {",
1552
+ " id?: string;",
1553
+ " sku?: string;",
1554
+ " warehouse?: string;",
1555
+ " available?: number;",
1556
+ " safetyStock?: number;",
744
1557
  "};",
745
1558
  "",
746
- "type TaskComment = {",
1559
+ "type InspectionRow = {",
747
1560
  " id?: string;",
748
- " body?: string;",
749
- " createdAt?: number;",
1561
+ " result?: string;",
1562
+ " severity?: string;",
1563
+ " note?: string;",
750
1564
  "};",
751
1565
  "",
752
- "type TaskRow = {",
1566
+ "type ShipmentRow = {",
753
1567
  " id?: string;",
754
- " title?: string;",
1568
+ " carrier?: string;",
1569
+ " lane?: string;",
755
1570
  " status?: string;",
756
- " createdAt?: number;",
757
- " comments?: TaskComment[] | TaskComment;",
1571
+ " etaHours?: number;",
1572
+ " inspections?: InspectionRow[] | InspectionRow;",
758
1573
  "};",
759
1574
  "",
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
- "}",
1575
+ "type OrderRow = {",
1576
+ " id?: string;",
1577
+ " reference?: string;",
1578
+ " status?: string;",
1579
+ " spend?: number;",
1580
+ " supplier?: SupplierRow | SupplierRow[];",
1581
+ " stockItems?: StockItemRow[] | StockItemRow;",
1582
+ " shipments?: ShipmentRow[] | ShipmentRow;",
1583
+ "};",
1584
+ "",
1585
+ "const db = init({",
1586
+ " appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID || \"\",",
1587
+ "});",
775
1588
  "",
776
1589
  "function asArray<T>(value: T | T[] | null | undefined): T[] {",
777
1590
  " if (!value) return [];",
778
1591
  " return Array.isArray(value) ? value : [value];",
779
1592
  "}",
780
1593
  "",
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
- " }",
1594
+ "function first<T>(value: T | T[] | null | undefined): T | null {",
1595
+ " return asArray(value)[0] ?? null;",
1596
+ "}",
1597
+ "",
1598
+ "function money(value?: number) {",
1599
+ " return new Intl.NumberFormat(undefined, {",
1600
+ " currency: \"USD\",",
1601
+ " maximumFractionDigits: 0,",
1602
+ " style: \"currency\",",
1603
+ " }).format(value ?? 0);",
1604
+ "}",
1605
+ "",
1606
+ "async function runAction(action: string, input: Record<string, unknown>) {",
1607
+ " const response = await fetch(\"/api/ekairos/domain\", {",
1608
+ " method: \"POST\",",
1609
+ " headers: { \"content-type\": \"application/json\" },",
1610
+ " body: JSON.stringify({ op: \"action\", action, input }),",
1611
+ " });",
1612
+ " const text = await response.text();",
1613
+ " if (!response.ok) throw new Error(text || `request_failed:${response.status}`);",
1614
+ " return text ? JSON.parse(text) : null;",
788
1615
  "}",
789
1616
  "",
790
1617
  "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');",
1618
+ " const [reference, setReference] = useState(\"PO-7842\");",
1619
+ " const [supplierName, setSupplierName] = useState(\"Marula Components\");",
1620
+ " const [sku, setSku] = useState(\"DRV-2048\");",
794
1621
  " 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
- " }, []);",
1622
+ " const [actionError, setActionError] = useState<string | null>(null);",
1623
+ "",
1624
+ " const query = db.useQuery({",
1625
+ " procurement_order: {",
1626
+ " $: { order: { createdAt: \"desc\" }, limit: 8 },",
1627
+ " supplier: {},",
1628
+ " stockItems: {},",
1629
+ " shipments: {",
1630
+ " inspections: {},",
1631
+ " },",
1632
+ " },",
1633
+ " }) as {",
1634
+ " data?: { procurement_order?: OrderRow[] };",
1635
+ " error?: unknown;",
1636
+ " isLoading: boolean;",
1637
+ " };",
837
1638
  "",
838
- " async function runAction(action: string, input: Record<string, unknown>) {",
1639
+ " const orders = query.data?.procurement_order ?? [];",
1640
+ " const activeOrder = orders[0] ?? null;",
1641
+ " const supplier = first(activeOrder?.supplier);",
1642
+ " const stockItems = asArray(activeOrder?.stockItems);",
1643
+ " const shipments = asArray(activeOrder?.shipments);",
1644
+ " const shipment = shipments[0] ?? null;",
1645
+ " const inspections = shipments.flatMap((entry) => asArray(entry.inspections));",
1646
+ " const inspection = inspections[0] ?? null;",
1647
+ "",
1648
+ " const metrics = useMemo(() => ({",
1649
+ " orders: orders.length,",
1650
+ " stock: stockItems.reduce((total, item) => total + (item.available ?? 0), 0),",
1651
+ " eta: shipment?.etaHours ?? 0,",
1652
+ " risk: supplier?.risk ?? \"none\",",
1653
+ " }), [orders.length, shipment?.etaHours, stockItems, supplier?.risk]);",
1654
+ "",
1655
+ " async function submitAction(action: string, input: Record<string, unknown>) {",
839
1656
  " setLoadingAction(action);",
840
- " setError(null);",
1657
+ " setActionError(null);",
841
1658
  " 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));",
1659
+ " await runAction(action, input);",
1660
+ " } catch (error) {",
1661
+ " setActionError(error instanceof Error ? error.message : String(error));",
850
1662
  " } finally {",
851
1663
  " setLoadingAction(null);",
852
1664
  " }",
853
1665
  " }",
854
1666
  "",
855
1667
  " 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>',
1668
+ " <section className=\"shell\">",
1669
+ " <div className=\"metrics-strip\">",
1670
+ " <div><span>open orders</span><strong>{metrics.orders}</strong></div>",
1671
+ " <div><span>available stock</span><strong>{metrics.stock}</strong></div>",
1672
+ " <div><span>shipment eta</span><strong>{metrics.eta}h</strong></div>",
1673
+ " <div><span>supplier risk</span><strong>{metrics.risk}</strong></div>",
862
1674
  " </div>",
863
1675
  "",
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
- " />",
1676
+ " <div className=\"workbench\">",
1677
+ " <section className=\"command-panel\">",
1678
+ " <span className=\"eyebrow\">Release</span>",
1679
+ " <h2>Open a purchase order</h2>",
1680
+ " <label className=\"field\">",
1681
+ " <span>PO reference</span>",
1682
+ " <input className=\"input\" value={reference} onChange={(event) => setReference(event.target.value)} />",
902
1683
  " </label>",
903
- ' <div className="button-row">',
1684
+ " <label className=\"field\">",
1685
+ " <span>Supplier</span>",
1686
+ " <input className=\"input\" value={supplierName} onChange={(event) => setSupplierName(event.target.value)} />",
1687
+ " </label>",
1688
+ " <label className=\"field\">",
1689
+ " <span>SKU</span>",
1690
+ " <input className=\"input\" value={sku} onChange={(event) => setSku(event.target.value)} />",
1691
+ " </label>",
1692
+ " <div className=\"button-row\">",
904
1693
  " <button",
905
- ' className="button"',
1694
+ " className=\"button\"",
906
1695
  " disabled={loadingAction !== null}",
907
- " onClick={() => void runAction('app.task.create', { title: draftTitle, status: 'manual' })}",
1696
+ " onClick={() => void submitAction(\"supplyChain.order.launch\", { reference, sku, supplierName })}",
908
1697
  " >",
909
- ' {loadingAction === "app.task.create" ? <span className="spinner" aria-hidden="true" /> : null}',
910
- " Create Task",
1698
+ " {loadingAction === \"supplyChain.order.launch\" ? \"Opening\" : \"Open order\"}",
911
1699
  " </button>",
912
1700
  " <button",
913
- ' className="button ghost"',
914
- " disabled={loadingAction !== null}",
915
- " onClick={() => void runAction('app.demo.seed', {})}",
1701
+ " className=\"button ghost\"",
1702
+ " disabled={loadingAction !== null || !shipment?.id}",
1703
+ " onClick={() => void submitAction(\"supplyChain.shipment.expedite\", { shipmentId: shipment?.id })}",
916
1704
  " >",
917
- ' {loadingAction === "app.demo.seed" ? <span className="spinner" aria-hidden="true" /> : null}',
918
- " Seed Demo",
1705
+ " Expedite shipment",
919
1706
  " </button>",
920
1707
  " </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
- " ))}",
1708
+ " </section>",
1709
+ "",
1710
+ " <section className=\"context-rail\" aria-label=\"Operational path\">",
1711
+ " <div className=\"panel-head\">",
1712
+ " <div>",
1713
+ " <span className=\"eyebrow\">Path</span>",
1714
+ " <h2>What gets linked</h2>",
1715
+ " </div>",
928
1716
  " </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>',
1717
+ " <div className=\"context-list\">",
1718
+ " <div className=\"context-row\"><span>Supplier</span><strong>Risk and commercial owner</strong></div>",
1719
+ " <div className=\"context-row\"><span>Order</span><strong>Spend and release state</strong></div>",
1720
+ " <div className=\"context-row\"><span>Inventory</span><strong>SKU and stock position</strong></div>",
1721
+ " <div className=\"context-row\"><span>Transport</span><strong>Carrier lane and ETA</strong></div>",
1722
+ " <div className=\"context-row\"><span>Quality</span><strong>Arrival inspection</strong></div>",
1723
+ " </div>",
1724
+ " </section>",
1725
+ "",
1726
+ " <section className=\"graph-panel\">",
1727
+ " <div className=\"panel-head\">",
1728
+ " <div>",
1729
+ " <span className=\"eyebrow\">Live order</span>",
1730
+ " <h2>{activeOrder?.reference ?? \"No order active\"}</h2>",
937
1731
  " </div>",
938
- ' <span className="status-pill">{loadingData ? "syncing" : `${tasks.length} rows`}</span>',
1732
+ " <span className=\"status-pill\">{query.isLoading ? \"loading\" : activeOrder?.status ?? \"idle\"}</span>",
939
1733
  " </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>',
1734
+ "",
1735
+ " {query.isLoading ? (",
1736
+ " <div className=\"skeleton-stack\" aria-label=\"Loading order\">",
1737
+ " <span />",
1738
+ " <span />",
1739
+ " <span />",
1740
+ " </div>",
1741
+ " ) : query.error ? (",
1742
+ " <div className=\"error-banner\">{String(query.error)}</div>",
1743
+ " ) : !activeOrder ? (",
1744
+ " <div className=\"empty-state\">Open an order to create the first control tower view.</div>",
1745
+ " ) : (",
1746
+ " <>",
1747
+ " <div className=\"raid-summary\">",
1748
+ " <div>",
1749
+ " <span>supplier</span>",
1750
+ " <strong>{supplier?.name ?? \"unassigned\"}</strong>",
1751
+ " </div>",
1752
+ " <div>",
1753
+ " <span>region</span>",
1754
+ " <strong>{supplier?.region ?? \"unknown\"}</strong>",
1755
+ " </div>",
1756
+ " <div>",
1757
+ " <span>spend</span>",
1758
+ " <strong>{money(activeOrder.spend)}</strong>",
948
1759
  " </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>",
1760
+ " </div>",
1761
+ "",
1762
+ " <div className=\"lane-grid\">",
1763
+ " <div className=\"lane\">",
1764
+ " <span className=\"manifest-label\">Inventory</span>",
1765
+ " {stockItems.map((item) => (",
1766
+ " <article className=\"objective\" key={item.id}>",
1767
+ " <strong>{item.sku}</strong>",
1768
+ " <span>{item.warehouse}</span>",
1769
+ " <em>{item.available} units / {item.safetyStock} safety</em>",
1770
+ " </article>",
956
1771
  " ))}",
957
1772
  " </div>",
958
- " </article>",
959
- " ))}",
960
- " </div>",
961
- " </article>",
1773
+ " <div className=\"lane\">",
1774
+ " <span className=\"manifest-label\">Transport</span>",
1775
+ " {shipments.map((entry) => (",
1776
+ " <article className=\"objective\" key={entry.id}>",
1777
+ " <strong>{entry.carrier}</strong>",
1778
+ " <span>{entry.lane}</span>",
1779
+ " <em>{entry.status} / {entry.etaHours}h ETA</em>",
1780
+ " </article>",
1781
+ " ))}",
1782
+ " </div>",
1783
+ " <div className=\"lane\">",
1784
+ " <span className=\"manifest-label\">Quality</span>",
1785
+ " {inspections.map((entry) => (",
1786
+ " <article className=\"objective\" key={entry.id}>",
1787
+ " <strong>{entry.result}</strong>",
1788
+ " <span>{entry.severity}</span>",
1789
+ " <em>{entry.note}</em>",
1790
+ " </article>",
1791
+ " ))}",
1792
+ " </div>",
1793
+ " </div>",
1794
+ " </>",
1795
+ " )}",
1796
+ " </section>",
962
1797
  " </div>",
963
1798
  "",
964
- ' {error ? <div className="error-banner">{error}</div> : null}',
1799
+ " {actionError ? <div className=\"error-banner\">{actionError}</div> : null}",
965
1800
  " </section>",
966
1801
  " );",
967
1802
  "}",
968
1803
  ].join("\n"),
969
1804
  "src/domain.ts": [
970
- 'import { defineDomainAction, domain } from "@ekairos/domain";',
971
- 'import { i } from "@instantdb/core";',
1805
+ "import { defineAction, domain } from \"@ekairos/domain\";",
1806
+ "import { i } from \"@instantdb/core\";",
1807
+ "",
1808
+ "export const supplierNetworkDomain = domain(\"supplierNetwork\").withSchema({",
1809
+ " entities: {",
1810
+ " supplierNetwork_supplier: i.entity({",
1811
+ " name: i.string().indexed(),",
1812
+ " region: i.string().indexed(),",
1813
+ " risk: i.string().indexed(),",
1814
+ " score: i.number().indexed(),",
1815
+ " createdAt: i.number().indexed(),",
1816
+ " }),",
1817
+ " },",
1818
+ " links: {},",
1819
+ " rooms: {},",
1820
+ "});",
972
1821
  "",
973
- "const baseDomain = domain(\"ekairos.app\")",
974
- " .schema({",
1822
+ "export const procurementDomain = domain(\"procurement\")",
1823
+ " .includes(supplierNetworkDomain)",
1824
+ " .withSchema({",
975
1825
  " entities: {",
976
- " app_tasks: i.entity({",
977
- " title: i.string().indexed(),",
1826
+ " procurement_order: i.entity({",
1827
+ " reference: i.string().indexed(),",
978
1828
  " status: i.string().indexed(),",
1829
+ " spend: i.number().indexed(),",
979
1830
  " createdAt: i.number().indexed(),",
980
1831
  " }),",
981
- " app_task_comments: i.entity({",
982
- " body: i.string(),",
1832
+ " },",
1833
+ " links: {",
1834
+ " procurement_orderSupplier: {",
1835
+ " forward: { on: \"procurement_order\", has: \"one\", label: \"supplier\" },",
1836
+ " reverse: { on: \"supplierNetwork_supplier\", has: \"many\", label: \"orders\" },",
1837
+ " },",
1838
+ " },",
1839
+ " rooms: {},",
1840
+ " });",
1841
+ "",
1842
+ "export const inventoryDomain = domain(\"inventory\")",
1843
+ " .includes(procurementDomain)",
1844
+ " .withSchema({",
1845
+ " entities: {",
1846
+ " inventory_stockItem: i.entity({",
1847
+ " sku: i.string().indexed(),",
1848
+ " warehouse: i.string().indexed(),",
1849
+ " available: i.number().indexed(),",
1850
+ " safetyStock: i.number().indexed(),",
983
1851
  " createdAt: i.number().indexed(),",
984
1852
  " }),",
985
1853
  " },",
986
1854
  " links: {",
987
- " taskComments: {",
988
- ' forward: { on: "app_tasks", has: "many", label: "comments" },',
989
- ' reverse: { on: "app_task_comments", has: "one", label: "task" },',
1855
+ " inventory_stockItemOrder: {",
1856
+ " forward: { on: \"inventory_stockItem\", has: \"one\", label: \"order\" },",
1857
+ " reverse: { on: \"procurement_order\", has: \"many\", label: \"stockItems\" },",
990
1858
  " },",
991
1859
  " },",
992
1860
  " rooms: {},",
993
1861
  " });",
994
1862
  "",
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(),",
1863
+ "export const transportationDomain = domain(\"transportation\")",
1864
+ " .includes(procurementDomain)",
1865
+ " .withSchema({",
1866
+ " entities: {",
1867
+ " transportation_shipment: i.entity({",
1868
+ " carrier: i.string().indexed(),",
1869
+ " lane: i.string().indexed(),",
1870
+ " status: i.string().indexed(),",
1871
+ " etaHours: i.number().indexed(),",
1872
+ " createdAt: i.number().indexed(),",
1011
1873
  " }),",
1012
- " ]);",
1013
- " return { taskId };",
1014
- " },",
1015
- "});",
1874
+ " },",
1875
+ " links: {",
1876
+ " transportation_shipmentOrder: {",
1877
+ " forward: { on: \"transportation_shipment\", has: \"one\", label: \"order\" },",
1878
+ " reverse: { on: \"procurement_order\", has: \"many\", label: \"shipments\" },",
1879
+ " },",
1880
+ " },",
1881
+ " rooms: {},",
1882
+ " });",
1883
+ "",
1884
+ "export const qualityControlDomain = domain(\"qualityControl\")",
1885
+ " .includes(transportationDomain)",
1886
+ " .withSchema({",
1887
+ " entities: {",
1888
+ " qualityControl_inspection: i.entity({",
1889
+ " result: i.string().indexed(),",
1890
+ " severity: i.string().indexed(),",
1891
+ " note: i.string(),",
1892
+ " createdAt: i.number().indexed(),",
1893
+ " }),",
1894
+ " },",
1895
+ " links: {",
1896
+ " qualityControl_inspectionShipment: {",
1897
+ " forward: { on: \"qualityControl_inspection\", has: \"one\", label: \"shipment\" },",
1898
+ " reverse: { on: \"transportation_shipment\", has: \"many\", label: \"inspections\" },",
1899
+ " },",
1900
+ " },",
1901
+ " rooms: {},",
1902
+ " });",
1903
+ "",
1904
+ "const baseDomain = domain(\"supplyChain\")",
1905
+ " .includes(inventoryDomain)",
1906
+ " .includes(qualityControlDomain)",
1907
+ " .withSchema({ entities: {}, links: {}, rooms: {} });",
1016
1908
  "",
1017
- "export const addTaskCommentAction = defineDomainAction<",
1909
+ "export const launchOrderAction = defineAction<",
1018
1910
  " Record<string, unknown>,",
1019
- " { taskId?: string; body?: string },",
1020
- " { commentId: string; taskId: string },",
1911
+ " { reference?: string; supplierName?: string; sku?: string },",
1912
+ " { supplierId: string; orderId: string; stockItemId: string; shipmentId: string; inspectionId: string },",
1021
1913
  " any",
1022
1914
  ">({",
1023
- ' name: "app.task.comment.add",',
1024
- " async execute({ runtime, input }): Promise<{ commentId: string; taskId: string }> {",
1025
- ' "use step";',
1915
+ " name: \"supplyChain.order.launch\",",
1916
+ " async execute({ runtime, input }): Promise<{",
1917
+ " supplierId: string;",
1918
+ " orderId: string;",
1919
+ " stockItemId: string;",
1920
+ " shipmentId: string;",
1921
+ " inspectionId: string;",
1922
+ " }> {",
1923
+ " \"use step\";",
1026
1924
  " 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\");",
1925
+ " const now = Date.now();",
1926
+ " const supplierId = globalThis.crypto.randomUUID();",
1927
+ " const orderId = globalThis.crypto.randomUUID();",
1928
+ " const stockItemId = globalThis.crypto.randomUUID();",
1929
+ " const shipmentId = globalThis.crypto.randomUUID();",
1930
+ " const inspectionId = globalThis.crypto.randomUUID();",
1931
+ "",
1030
1932
  " 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(),",
1933
+ " scoped.db.tx.supplierNetwork_supplier[supplierId].update({",
1934
+ " name: String(input?.supplierName ?? \"\").trim() || \"Marula Components\",",
1935
+ " region: \"Pacific North\",",
1936
+ " risk: \"watch\",",
1937
+ " score: 82,",
1938
+ " createdAt: now,",
1939
+ " }),",
1940
+ " scoped.db.tx.procurement_order[orderId].update({",
1941
+ " reference: String(input?.reference ?? \"\").trim() || \"PO-7842\",",
1942
+ " status: \"released\",",
1943
+ " spend: 184700,",
1944
+ " createdAt: now + 1,",
1945
+ " }),",
1946
+ " scoped.db.tx.procurement_order[orderId].link({ supplier: supplierId }),",
1947
+ " scoped.db.tx.inventory_stockItem[stockItemId].update({",
1948
+ " sku: String(input?.sku ?? \"\").trim() || \"DRV-2048\",",
1949
+ " warehouse: \"Reno DC\",",
1950
+ " available: 320,",
1951
+ " safetyStock: 140,",
1952
+ " createdAt: now + 2,",
1034
1953
  " }),",
1035
- " scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
1954
+ " scoped.db.tx.inventory_stockItem[stockItemId].link({ order: orderId }),",
1955
+ " scoped.db.tx.transportation_shipment[shipmentId].update({",
1956
+ " carrier: \"Northstar Freight\",",
1957
+ " lane: \"Reno -> Austin\",",
1958
+ " status: \"in-transit\",",
1959
+ " etaHours: 38,",
1960
+ " createdAt: now + 3,",
1961
+ " }),",
1962
+ " scoped.db.tx.transportation_shipment[shipmentId].link({ order: orderId }),",
1963
+ " scoped.db.tx.qualityControl_inspection[inspectionId].update({",
1964
+ " result: \"pending\",",
1965
+ " severity: \"medium\",",
1966
+ " note: \"Inspect seal integrity on arrival.\",",
1967
+ " createdAt: now + 4,",
1968
+ " }),",
1969
+ " scoped.db.tx.qualityControl_inspection[inspectionId].link({ shipment: shipmentId }),",
1036
1970
  " ]);",
1037
- " return { commentId, taskId };",
1971
+ "",
1972
+ " return { supplierId, orderId, stockItemId, shipmentId, inspectionId };",
1038
1973
  " },",
1039
1974
  "});",
1040
1975
  "",
1041
- "export const seedDemoAction = defineDomainAction<",
1976
+ "export const expediteShipmentAction = defineAction<",
1042
1977
  " Record<string, unknown>,",
1043
- " Record<string, never>,",
1044
- " { taskId: string },",
1978
+ " { shipmentId?: string },",
1979
+ " { shipmentId: string },",
1045
1980
  " any",
1046
1981
  ">({",
1047
- ' name: "app.demo.seed",',
1048
- " async execute({ runtime }): Promise<{ taskId: string }> {",
1049
- ' "use step";',
1982
+ " name: \"supplyChain.shipment.expedite\",",
1983
+ " async execute({ runtime, input }): Promise<{ shipmentId: string }> {",
1984
+ " \"use step\";",
1050
1985
  " const scoped = await runtime.use(appDomain);",
1051
- " const taskId = globalThis.crypto.randomUUID();",
1052
- " const commentId = globalThis.crypto.randomUUID();",
1986
+ " const shipmentId = String(input?.shipmentId ?? \"\").trim();",
1987
+ " if (!shipmentId) throw new Error(\"shipmentId is required\");",
1988
+ "",
1053
1989
  " 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(),",
1990
+ " scoped.db.tx.transportation_shipment[shipmentId].update({",
1991
+ " status: \"expedited\",",
1992
+ " etaHours: 16,",
1062
1993
  " }),",
1063
- " scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
1064
1994
  " ]);",
1065
- " return { taskId };",
1995
+ "",
1996
+ " return { shipmentId };",
1066
1997
  " },",
1067
1998
  "});",
1068
1999
  "",
1069
- "export const appDomain = baseDomain.actions({",
1070
- " addTaskComment: addTaskCommentAction,",
1071
- " createTask: createTaskAction,",
1072
- " seedDemo: seedDemoAction,",
2000
+ "export const appDomain = baseDomain.withActions({",
2001
+ " expediteShipment: expediteShipmentAction,",
2002
+ " launchOrder: launchOrderAction,",
1073
2003
  "});",
1074
2004
  "",
1075
2005
  "export default appDomain;",
@@ -1131,29 +2061,29 @@ function buildNextTemplateFiles(params) {
1131
2061
  "});",
1132
2062
  ].join("\n"),
1133
2063
  "src/workflows/demo.workflow.ts": [
1134
- 'import { executeRuntimeAction } from "@ekairos/domain/runtime";',
1135
- 'import { createRuntime } from "../runtime";',
2064
+ "import appDomain from \"../domain\";",
2065
+ "import { createRuntime } from \"../runtime\";",
1136
2066
  "",
1137
2067
  "export type DemoWorkflowInput = {",
1138
- " title: string;",
1139
- " comment?: string;",
2068
+ " expedite?: boolean;",
2069
+ " reference?: string;",
2070
+ " sku?: string;",
2071
+ " supplierName?: string;",
1140
2072
  "};",
1141
2073
  "",
1142
2074
  "export async function runDemoWorkflow(input: DemoWorkflowInput) {",
1143
- ' "use workflow";',
2075
+ " \"use workflow\";",
1144
2076
  " 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 },",
2077
+ " const scoped = await runtime.use(appDomain);",
2078
+ " const created = await scoped.actions.launchOrder({",
2079
+ " reference: input.reference,",
2080
+ " sku: input.sku,",
2081
+ " supplierName: input.supplierName,",
2082
+ " });",
2083
+ "",
2084
+ " if (input.expedite) {",
2085
+ " await scoped.actions.expediteShipment({",
2086
+ " shipmentId: created.shipmentId,",
1157
2087
  " });",
1158
2088
  " }",
1159
2089
  "",
@@ -1166,6 +2096,12 @@ export async function createDomainApp(params) {
1166
2096
  if (params.framework !== "next") {
1167
2097
  throw new Error("Only --next is supported right now.");
1168
2098
  }
2099
+ if (params.smoke && !params.install) {
2100
+ throw new Error("--smoke requires dependencies. Remove --no-install or run smoke after installing.");
2101
+ }
2102
+ if (params.demo && !params.smoke) {
2103
+ throw new Error("--demo runs the full app cycle and requires smoke validation.");
2104
+ }
1169
2105
  const targetDir = resolve(params.directory || ".");
1170
2106
  await emitProgress(params.onProgress, {
1171
2107
  stage: "prepare-target",
@@ -1232,6 +2168,11 @@ export async function createDomainApp(params) {
1232
2168
  }
1233
2169
  const appId = explicitAppId || provisioned?.appId || null;
1234
2170
  const adminToken = explicitAdminToken || provisioned?.adminToken || null;
2171
+ if (params.smoke && (!appId || !adminToken)) {
2172
+ throw new Error(params.demo
2173
+ ? "--demo requires Instant provisioning. Set INSTANT_PERSONAL_ACCESS_TOKEN or pass --instantToken."
2174
+ : "--smoke requires a configured Instant app. Pass --instantToken or --appId with --adminToken.");
2175
+ }
1235
2176
  await emitProgress(params.onProgress, {
1236
2177
  stage: "write-files",
1237
2178
  status: "running",
@@ -1251,14 +2192,15 @@ export async function createDomainApp(params) {
1251
2192
  message: "Scaffold files written",
1252
2193
  progress: 72,
1253
2194
  });
1254
- if (appId && adminToken) {
2195
+ const envFile = appId && adminToken ? join(targetDir, ".env.local") : null;
2196
+ if (appId && adminToken && envFile) {
1255
2197
  await emitProgress(params.onProgress, {
1256
2198
  stage: "write-env",
1257
2199
  status: "running",
1258
2200
  message: "Writing .env.local",
1259
2201
  progress: 78,
1260
2202
  });
1261
- await writeFile(join(targetDir, ".env.local"), [
2203
+ await writeFile(envFile, [
1262
2204
  `NEXT_PUBLIC_INSTANT_APP_ID=${appId}`,
1263
2205
  `INSTANT_ADMIN_TOKEN=${adminToken}`,
1264
2206
  "",
@@ -1285,15 +2227,29 @@ export async function createDomainApp(params) {
1285
2227
  progress: 96,
1286
2228
  });
1287
2229
  }
2230
+ const smoke = params.smoke
2231
+ ? await runSmoke({
2232
+ demo: Boolean(params.demo),
2233
+ targetDir,
2234
+ packageManager,
2235
+ keepServer: Boolean(params.keepServer),
2236
+ onKeepServer: params.onKeepServer,
2237
+ onProgress: params.onProgress,
2238
+ })
2239
+ : null;
2240
+ const cliCommand = packageBinCommandFor(packageManager, "domain");
2241
+ const reviewUrl = smoke?.baseUrl ?? "http://localhost:3000";
1288
2242
  const nextSteps = [
1289
2243
  `cd ${targetDir}`,
1290
- params.install
1291
- ? 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",
2244
+ smoke?.keepServer
2245
+ ? `Open ${reviewUrl} for review`
2246
+ : params.install
2247
+ ? runScriptCommandFor(packageManager, "dev")
2248
+ : `${installCommandFor(packageManager)} && ${runScriptCommandFor(packageManager, "dev")}`,
2249
+ `Open ${reviewUrl} and launch a purchase order from the control tower UI`,
2250
+ `${cliCommand} inspect --baseUrl=${reviewUrl} --admin --pretty`,
2251
+ `${cliCommand} "supplyChain.order.launch" "{ reference: 'PO-7842', supplierName: 'Marula Components', sku: 'DRV-2048' }" --baseUrl=${reviewUrl} --admin --pretty`,
2252
+ `${cliCommand} query "{ procurement_order: { supplier: {}, stockItems: {}, shipments: { inspections: {} } } }" --baseUrl=${reviewUrl} --admin --pretty`,
1297
2253
  ];
1298
2254
  const result = {
1299
2255
  ok: true,
@@ -1304,6 +2260,10 @@ export async function createDomainApp(params) {
1304
2260
  provisioned: Boolean(provisioned),
1305
2261
  appId,
1306
2262
  adminToken,
2263
+ adminTokenWritten: Boolean(envFile),
2264
+ envFile,
2265
+ smoke,
2266
+ demo: Boolean(params.demo),
1307
2267
  nextSteps,
1308
2268
  };
1309
2269
  await emitProgress(params.onProgress, {