@ekairos/domain 1.22.34-beta.development.0 → 1.22.36-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.
- package/README.md +123 -128
- package/dist/cli/bin.d.ts +9 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +488 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/client-runtime.d.ts +25 -0
- package/dist/cli/client-runtime.d.ts.map +1 -0
- package/dist/cli/client-runtime.js +60 -0
- package/dist/cli/client-runtime.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +44 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/create-app.d.ts +33 -0
- package/dist/cli/create-app.d.ts.map +1 -0
- package/dist/cli/create-app.js +1317 -0
- package/dist/cli/create-app.js.map +1 -0
- package/dist/cli/http.d.ts +28 -0
- package/dist/cli/http.d.ts.map +1 -0
- package/dist/cli/http.js +117 -0
- package/dist/cli/http.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/server.d.ts +3 -0
- package/dist/cli/server.d.ts.map +1 -0
- package/dist/cli/server.js +437 -0
- package/dist/cli/server.js.map +1 -0
- package/dist/cli/types.d.ts +60 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +3 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +136 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/index.d.ts +70 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -18
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +21 -21
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +212 -345
- package/dist/next.js.map +1 -1
- package/dist/polyfills/dom-events.d.ts +2 -0
- package/dist/polyfills/dom-events.d.ts.map +1 -0
- package/dist/polyfills/dom-events.js +92 -0
- package/dist/polyfills/dom-events.js.map +1 -0
- package/dist/runtime-handle.d.ts +34 -0
- package/dist/runtime-handle.d.ts.map +1 -0
- package/dist/runtime-handle.js +82 -0
- package/dist/runtime-handle.js.map +1 -0
- package/dist/runtime.d.ts +3 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +72 -19
- package/dist/runtime.js.map +1 -1
- package/package.json +37 -4
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { PlatformApi } from "@instantdb/platform";
|
|
5
|
+
import { i } from "@instantdb/core";
|
|
6
|
+
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";
|
|
12
|
+
function trimOrEmpty(value) {
|
|
13
|
+
return typeof value === "string" ? value.trim() : "";
|
|
14
|
+
}
|
|
15
|
+
async function emitProgress(onProgress, event) {
|
|
16
|
+
if (!onProgress)
|
|
17
|
+
return;
|
|
18
|
+
await onProgress(event);
|
|
19
|
+
}
|
|
20
|
+
function toPosix(value) {
|
|
21
|
+
return value.replace(/\\/g, "/");
|
|
22
|
+
}
|
|
23
|
+
async function detectPackageManager(explicit, workspacePath) {
|
|
24
|
+
const normalized = trimOrEmpty(explicit).toLowerCase();
|
|
25
|
+
if (normalized)
|
|
26
|
+
return normalized;
|
|
27
|
+
for (const root of [trimOrEmpty(workspacePath), process.cwd()].filter(Boolean)) {
|
|
28
|
+
if (await pathExists(join(root, "pnpm-lock.yaml")))
|
|
29
|
+
return "pnpm";
|
|
30
|
+
if (await pathExists(join(root, "yarn.lock")))
|
|
31
|
+
return "yarn";
|
|
32
|
+
if (await pathExists(join(root, "bun.lockb")))
|
|
33
|
+
return "bun";
|
|
34
|
+
}
|
|
35
|
+
const agent = trimOrEmpty(process.env.npm_config_user_agent);
|
|
36
|
+
if (agent.startsWith("pnpm/"))
|
|
37
|
+
return "pnpm";
|
|
38
|
+
if (agent.startsWith("yarn/"))
|
|
39
|
+
return "yarn";
|
|
40
|
+
if (agent.startsWith("bun/"))
|
|
41
|
+
return "bun";
|
|
42
|
+
return "npm";
|
|
43
|
+
}
|
|
44
|
+
async function pathExists(pathname) {
|
|
45
|
+
try {
|
|
46
|
+
await access(pathname);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function ensureWritableTargetDirectory(targetDir, force = false) {
|
|
54
|
+
await mkdir(targetDir, { recursive: true });
|
|
55
|
+
const entries = await readdir(targetDir);
|
|
56
|
+
if (entries.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
if (!force) {
|
|
59
|
+
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
60
|
+
}
|
|
61
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
62
|
+
await mkdir(targetDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
async function readDomainPackageVersion() {
|
|
65
|
+
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
66
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
return trimOrEmpty(parsed.version) || "latest";
|
|
69
|
+
}
|
|
70
|
+
function createScaffoldSchema() {
|
|
71
|
+
const scaffoldDomain = domain("ekairos.app").schema({
|
|
72
|
+
entities: {
|
|
73
|
+
app_tasks: i.entity({
|
|
74
|
+
title: i.string().indexed(),
|
|
75
|
+
status: i.string().indexed(),
|
|
76
|
+
createdAt: i.number().indexed(),
|
|
77
|
+
}),
|
|
78
|
+
app_task_comments: i.entity({
|
|
79
|
+
body: i.string(),
|
|
80
|
+
createdAt: i.number().indexed(),
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
links: {
|
|
84
|
+
taskComments: {
|
|
85
|
+
forward: { on: "app_tasks", has: "many", label: "comments" },
|
|
86
|
+
reverse: { on: "app_task_comments", has: "one", label: "task" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
rooms: {},
|
|
90
|
+
});
|
|
91
|
+
return scaffoldDomain.toInstantSchema();
|
|
92
|
+
}
|
|
93
|
+
function createScaffoldPerms() {
|
|
94
|
+
return {
|
|
95
|
+
attrs: {
|
|
96
|
+
allow: { create: "true" },
|
|
97
|
+
},
|
|
98
|
+
app_tasks: {
|
|
99
|
+
bind: ["isLoggedIn", "auth.id != null"],
|
|
100
|
+
allow: {
|
|
101
|
+
view: "true",
|
|
102
|
+
create: "isLoggedIn",
|
|
103
|
+
update: "isLoggedIn",
|
|
104
|
+
delete: "false",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
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
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function installCommandFor(packageManager) {
|
|
119
|
+
if (packageManager === "pnpm")
|
|
120
|
+
return "pnpm install";
|
|
121
|
+
if (packageManager === "yarn")
|
|
122
|
+
return "yarn install";
|
|
123
|
+
if (packageManager === "bun")
|
|
124
|
+
return "bun install";
|
|
125
|
+
return "npm install";
|
|
126
|
+
}
|
|
127
|
+
function runScriptCommandFor(packageManager, script) {
|
|
128
|
+
if (packageManager === "pnpm")
|
|
129
|
+
return `pnpm ${script}`;
|
|
130
|
+
if (packageManager === "yarn")
|
|
131
|
+
return `yarn ${script}`;
|
|
132
|
+
if (packageManager === "bun")
|
|
133
|
+
return `bun run ${script}`;
|
|
134
|
+
return `npm run ${script}`;
|
|
135
|
+
}
|
|
136
|
+
async function provisionInstantApp(params) {
|
|
137
|
+
const api = new PlatformApi({
|
|
138
|
+
auth: { token: params.instantToken },
|
|
139
|
+
});
|
|
140
|
+
const created = await api.createApp({
|
|
141
|
+
title: `ekairos-${trimOrEmpty(params.directory.split(/[\\/]/).pop()) || "app"}`,
|
|
142
|
+
orgId: trimOrEmpty(params.orgId) || undefined,
|
|
143
|
+
schema: createScaffoldSchema(),
|
|
144
|
+
perms: createScaffoldPerms(),
|
|
145
|
+
});
|
|
146
|
+
const appId = trimOrEmpty(created?.app?.id);
|
|
147
|
+
const adminToken = trimOrEmpty(created?.app?.adminToken);
|
|
148
|
+
if (!appId || !adminToken) {
|
|
149
|
+
throw new Error("Instant app provisioning did not return appId/adminToken");
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
appId,
|
|
153
|
+
adminToken,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function runInstall(targetDir, packageManager, onProgress) {
|
|
157
|
+
const command = packageManager === "yarn"
|
|
158
|
+
? "yarn"
|
|
159
|
+
: packageManager === "bun"
|
|
160
|
+
? "bun"
|
|
161
|
+
: packageManager === "pnpm"
|
|
162
|
+
? "pnpm"
|
|
163
|
+
: "npm";
|
|
164
|
+
const args = command === "yarn"
|
|
165
|
+
? ["install"]
|
|
166
|
+
: command === "bun"
|
|
167
|
+
? ["install"]
|
|
168
|
+
: command === "pnpm"
|
|
169
|
+
? ["install"]
|
|
170
|
+
: ["install"];
|
|
171
|
+
await new Promise((resolveInstall, rejectInstall) => {
|
|
172
|
+
const emitChunk = (() => {
|
|
173
|
+
let buffer = "";
|
|
174
|
+
return async (chunk) => {
|
|
175
|
+
buffer += chunk;
|
|
176
|
+
const lines = buffer.split(/\r?\n/);
|
|
177
|
+
buffer = lines.pop() ?? "";
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const text = line.trim();
|
|
180
|
+
if (!text)
|
|
181
|
+
continue;
|
|
182
|
+
await emitProgress(onProgress, {
|
|
183
|
+
stage: "install",
|
|
184
|
+
status: "log",
|
|
185
|
+
message: text,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
})();
|
|
190
|
+
const child = spawn(command, args, {
|
|
191
|
+
cwd: targetDir,
|
|
192
|
+
env: process.env,
|
|
193
|
+
shell: process.platform === "win32",
|
|
194
|
+
stdio: "pipe",
|
|
195
|
+
});
|
|
196
|
+
let stderr = "";
|
|
197
|
+
child.stdout.on("data", (chunk) => {
|
|
198
|
+
void emitChunk(chunk.toString());
|
|
199
|
+
});
|
|
200
|
+
child.stderr.on("data", (chunk) => {
|
|
201
|
+
const text = chunk.toString();
|
|
202
|
+
stderr += text;
|
|
203
|
+
void emitChunk(text);
|
|
204
|
+
});
|
|
205
|
+
child.on("error", rejectInstall);
|
|
206
|
+
child.on("close", (code) => {
|
|
207
|
+
if (code === 0) {
|
|
208
|
+
resolveInstall();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
rejectInstall(new Error(stderr.trim() || `${command} install failed with exit code ${code ?? "unknown"}`));
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function writeScaffoldFiles(targetDir, files) {
|
|
216
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
217
|
+
const absolutePath = join(targetDir, relativePath);
|
|
218
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
219
|
+
await writeFile(absolutePath, content, "utf8");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function resolveDomainDependencyVersion(version, targetDir, workspacePath) {
|
|
223
|
+
const workspaceRoot = trimOrEmpty(workspacePath);
|
|
224
|
+
if (!workspaceRoot) {
|
|
225
|
+
return version;
|
|
226
|
+
}
|
|
227
|
+
const packageRoot = resolve(workspaceRoot, "packages/domain");
|
|
228
|
+
const relativePath = toPosix(relative(targetDir, packageRoot));
|
|
229
|
+
if (!relativePath)
|
|
230
|
+
return "file:.";
|
|
231
|
+
const prefixed = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
232
|
+
return `file:${prefixed}`;
|
|
233
|
+
}
|
|
234
|
+
function buildNextTemplateFiles(params) {
|
|
235
|
+
const domainDependency = resolveDomainDependencyVersion(params.domainVersion, params.targetDir, params.workspacePath);
|
|
236
|
+
const packageJson = {
|
|
237
|
+
name: trimOrEmpty(params.targetDir.split(/[\\/]/).pop()) || "ekairos-app",
|
|
238
|
+
private: true,
|
|
239
|
+
version: "0.1.0",
|
|
240
|
+
type: "module",
|
|
241
|
+
scripts: {
|
|
242
|
+
build: "next build",
|
|
243
|
+
dev: "next dev",
|
|
244
|
+
start: "next start",
|
|
245
|
+
typecheck: "tsc --noEmit",
|
|
246
|
+
},
|
|
247
|
+
dependencies: {
|
|
248
|
+
"@ekairos/domain": domainDependency,
|
|
249
|
+
"@instantdb/admin": TEMPLATE_INSTANT_VERSION,
|
|
250
|
+
"@instantdb/core": TEMPLATE_INSTANT_VERSION,
|
|
251
|
+
next: TEMPLATE_NEXT_VERSION,
|
|
252
|
+
react: TEMPLATE_REACT_VERSION,
|
|
253
|
+
"react-dom": TEMPLATE_REACT_VERSION,
|
|
254
|
+
workflow: TEMPLATE_WORKFLOW_VERSION,
|
|
255
|
+
},
|
|
256
|
+
devDependencies: {
|
|
257
|
+
"@types/node": "^24.5.0",
|
|
258
|
+
"@types/react": "^19.2.2",
|
|
259
|
+
"@types/react-dom": "^19.2.2",
|
|
260
|
+
typescript: TEMPLATE_TYPESCRIPT_VERSION,
|
|
261
|
+
},
|
|
262
|
+
packageManager: params.packageManager === "pnpm"
|
|
263
|
+
? "pnpm@10.15.1"
|
|
264
|
+
: params.packageManager === "yarn"
|
|
265
|
+
? "yarn@1"
|
|
266
|
+
: undefined,
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
".gitignore": [".next", "node_modules", ".env.local", ".workflow-data"].join("\n"),
|
|
270
|
+
".env.example": [
|
|
271
|
+
"NEXT_PUBLIC_INSTANT_APP_ID=",
|
|
272
|
+
"INSTANT_ADMIN_TOKEN=",
|
|
273
|
+
"",
|
|
274
|
+
"# Optional: use this only while provisioning new apps with the CLI.",
|
|
275
|
+
"INSTANT_PERSONAL_ACCESS_TOKEN=",
|
|
276
|
+
].join("\n"),
|
|
277
|
+
"DOMAIN.md": [
|
|
278
|
+
"# Ekairos App Domain",
|
|
279
|
+
"",
|
|
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",
|
|
285
|
+
"",
|
|
286
|
+
"Actions:",
|
|
287
|
+
"- `createTask` -> create one task",
|
|
288
|
+
"- `addTaskComment` -> attach one comment to a task",
|
|
289
|
+
"- `seedDemo` -> create demo data fast",
|
|
290
|
+
].join("\n"),
|
|
291
|
+
"instant.schema.ts": [
|
|
292
|
+
'import appDomain from "./src/domain";',
|
|
293
|
+
"",
|
|
294
|
+
"const schema = appDomain.toInstantSchema();",
|
|
295
|
+
"",
|
|
296
|
+
"export default schema;",
|
|
297
|
+
].join("\n"),
|
|
298
|
+
"next-env.d.ts": [
|
|
299
|
+
'/// <reference types="next" />',
|
|
300
|
+
'/// <reference types="next/image-types/global" />',
|
|
301
|
+
"",
|
|
302
|
+
"// This file is managed by Next.js.",
|
|
303
|
+
].join("\n"),
|
|
304
|
+
"next.config.ts": [
|
|
305
|
+
'import type { NextConfig } from "next";',
|
|
306
|
+
'import { withWorkflow } from "workflow/next";',
|
|
307
|
+
"",
|
|
308
|
+
"const nextConfig: NextConfig = {",
|
|
309
|
+
" transpilePackages: [\"@ekairos/domain\"],",
|
|
310
|
+
"};",
|
|
311
|
+
"",
|
|
312
|
+
"export default withWorkflow(nextConfig) as NextConfig;",
|
|
313
|
+
].join("\n"),
|
|
314
|
+
"src/app/api/ekairos/domain/route.ts": [
|
|
315
|
+
'import { createRuntimeRouteHandler } from "@ekairos/domain/next";',
|
|
316
|
+
'import { createRuntime } from "@/runtime";',
|
|
317
|
+
"",
|
|
318
|
+
"export const { GET, POST } = createRuntimeRouteHandler({",
|
|
319
|
+
" createRuntime,",
|
|
320
|
+
"});",
|
|
321
|
+
].join("\n"),
|
|
322
|
+
"package.json": `${JSON.stringify(packageJson, null, 2)}\n`,
|
|
323
|
+
"tsconfig.json": [
|
|
324
|
+
"{",
|
|
325
|
+
' "compilerOptions": {',
|
|
326
|
+
' "target": "ES2022",',
|
|
327
|
+
' "lib": ["dom", "dom.iterable", "es2022"],',
|
|
328
|
+
' "allowJs": false,',
|
|
329
|
+
' "skipLibCheck": true,',
|
|
330
|
+
' "strict": true,',
|
|
331
|
+
' "noEmit": true,',
|
|
332
|
+
' "esModuleInterop": true,',
|
|
333
|
+
' "module": "esnext",',
|
|
334
|
+
' "moduleResolution": "bundler",',
|
|
335
|
+
' "resolveJsonModule": true,',
|
|
336
|
+
' "isolatedModules": true,',
|
|
337
|
+
' "jsx": "preserve",',
|
|
338
|
+
' "incremental": true,',
|
|
339
|
+
' "baseUrl": ".",',
|
|
340
|
+
' "paths": {',
|
|
341
|
+
' "@/*": ["./src/*"]',
|
|
342
|
+
" }",
|
|
343
|
+
" },",
|
|
344
|
+
' "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],',
|
|
345
|
+
' "exclude": ["node_modules"]',
|
|
346
|
+
"}",
|
|
347
|
+
].join("\n"),
|
|
348
|
+
"src/app/globals.css": [
|
|
349
|
+
":root {",
|
|
350
|
+
" 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;",
|
|
360
|
+
"}",
|
|
361
|
+
"",
|
|
362
|
+
"* {",
|
|
363
|
+
" box-sizing: border-box;",
|
|
364
|
+
"}",
|
|
365
|
+
"",
|
|
366
|
+
"html,",
|
|
367
|
+
"body {",
|
|
368
|
+
" margin: 0;",
|
|
369
|
+
" 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%);",
|
|
373
|
+
" color: var(--ink);",
|
|
374
|
+
' font-family: "Segoe UI", sans-serif;',
|
|
375
|
+
"}",
|
|
376
|
+
"",
|
|
377
|
+
"body {",
|
|
378
|
+
" min-height: 100vh;",
|
|
379
|
+
"}",
|
|
380
|
+
"",
|
|
381
|
+
"button,",
|
|
382
|
+
"input {",
|
|
383
|
+
" font: inherit;",
|
|
384
|
+
"}",
|
|
385
|
+
"",
|
|
386
|
+
"main {",
|
|
387
|
+
" max-width: 1180px;",
|
|
388
|
+
" margin: 0 auto;",
|
|
389
|
+
" padding: 48px 24px 72px;",
|
|
390
|
+
"}",
|
|
391
|
+
"",
|
|
392
|
+
".hero {",
|
|
393
|
+
" display: grid;",
|
|
394
|
+
" gap: 18px;",
|
|
395
|
+
" max-width: 860px;",
|
|
396
|
+
"}",
|
|
397
|
+
"",
|
|
398
|
+
".eyebrow {",
|
|
399
|
+
" color: var(--accent);",
|
|
400
|
+
" font-size: 12px;",
|
|
401
|
+
" font-weight: 700;",
|
|
402
|
+
" letter-spacing: 0.2em;",
|
|
403
|
+
" text-transform: uppercase;",
|
|
404
|
+
"}",
|
|
405
|
+
"",
|
|
406
|
+
"h1,",
|
|
407
|
+
"h2 {",
|
|
408
|
+
" margin: 0;",
|
|
409
|
+
" line-height: 0.96;",
|
|
410
|
+
"}",
|
|
411
|
+
"",
|
|
412
|
+
"h1 {",
|
|
413
|
+
" font-size: clamp(2.7rem, 6vw, 5.4rem);",
|
|
414
|
+
"}",
|
|
415
|
+
"",
|
|
416
|
+
"h2 {",
|
|
417
|
+
" font-size: 1.4rem;",
|
|
418
|
+
"}",
|
|
419
|
+
"",
|
|
420
|
+
"p {",
|
|
421
|
+
" margin: 0;",
|
|
422
|
+
" color: var(--muted);",
|
|
423
|
+
" line-height: 1.65;",
|
|
424
|
+
"}",
|
|
425
|
+
"",
|
|
426
|
+
".shell {",
|
|
427
|
+
" display: grid;",
|
|
428
|
+
" gap: 20px;",
|
|
429
|
+
" margin-top: 32px;",
|
|
430
|
+
"}",
|
|
431
|
+
"",
|
|
432
|
+
".stat-grid {",
|
|
433
|
+
" display: grid;",
|
|
434
|
+
" gap: 16px;",
|
|
435
|
+
" grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));",
|
|
436
|
+
"}",
|
|
437
|
+
"",
|
|
438
|
+
".grid {",
|
|
439
|
+
" display: grid;",
|
|
440
|
+
" gap: 20px;",
|
|
441
|
+
"}",
|
|
442
|
+
"",
|
|
443
|
+
".showcase-grid {",
|
|
444
|
+
" align-items: start;",
|
|
445
|
+
"}",
|
|
446
|
+
"",
|
|
447
|
+
".panel-tall,",
|
|
448
|
+
".panel-wide,",
|
|
449
|
+
".stat-card {",
|
|
450
|
+
" min-height: 100%;",
|
|
451
|
+
"}",
|
|
452
|
+
"",
|
|
453
|
+
".card {",
|
|
454
|
+
" background: linear-gradient(180deg, var(--panel) 0%, var(--panel-strong) 100%);",
|
|
455
|
+
" border: 1px solid var(--border);",
|
|
456
|
+
" border-radius: 24px;",
|
|
457
|
+
" padding: 22px;",
|
|
458
|
+
" box-shadow: 0 18px 45px rgba(23, 23, 23, 0.06);",
|
|
459
|
+
"}",
|
|
460
|
+
"",
|
|
461
|
+
".stat-card strong {",
|
|
462
|
+
" display: block;",
|
|
463
|
+
" margin-top: 8px;",
|
|
464
|
+
" font-size: 2rem;",
|
|
465
|
+
"}",
|
|
466
|
+
"",
|
|
467
|
+
".panel-head {",
|
|
468
|
+
" display: flex;",
|
|
469
|
+
" align-items: flex-start;",
|
|
470
|
+
" justify-content: space-between;",
|
|
471
|
+
" gap: 16px;",
|
|
472
|
+
" margin-bottom: 12px;",
|
|
473
|
+
"}",
|
|
474
|
+
"",
|
|
475
|
+
".manifest-list,",
|
|
476
|
+
".action-list,",
|
|
477
|
+
".task-list,",
|
|
478
|
+
".comment-list {",
|
|
479
|
+
" display: grid;",
|
|
480
|
+
" gap: 12px;",
|
|
481
|
+
"}",
|
|
482
|
+
"",
|
|
483
|
+
".manifest-list {",
|
|
484
|
+
" margin: 18px 0;",
|
|
485
|
+
"}",
|
|
486
|
+
"",
|
|
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;",
|
|
493
|
+
"}",
|
|
494
|
+
"",
|
|
495
|
+
".manifest-label {",
|
|
496
|
+
" color: var(--muted);",
|
|
497
|
+
" font-weight: 600;",
|
|
498
|
+
"}",
|
|
499
|
+
"",
|
|
500
|
+
".field {",
|
|
501
|
+
" display: grid;",
|
|
502
|
+
" gap: 8px;",
|
|
503
|
+
" margin: 18px 0;",
|
|
504
|
+
"}",
|
|
505
|
+
"",
|
|
506
|
+
".field span {",
|
|
507
|
+
" font-size: 0.92rem;",
|
|
508
|
+
" font-weight: 600;",
|
|
509
|
+
"}",
|
|
510
|
+
"",
|
|
511
|
+
".input {",
|
|
512
|
+
" width: 100%;",
|
|
513
|
+
" border: 1px solid var(--border);",
|
|
514
|
+
" border-radius: 16px;",
|
|
515
|
+
" padding: 14px 16px;",
|
|
516
|
+
" background: #fff;",
|
|
517
|
+
" color: var(--ink);",
|
|
518
|
+
"}",
|
|
519
|
+
"",
|
|
520
|
+
".button-row {",
|
|
521
|
+
" display: flex;",
|
|
522
|
+
" flex-wrap: wrap;",
|
|
523
|
+
" gap: 12px;",
|
|
524
|
+
" margin-bottom: 18px;",
|
|
525
|
+
"}",
|
|
526
|
+
"",
|
|
527
|
+
".button {",
|
|
528
|
+
" appearance: none;",
|
|
529
|
+
" border: 0;",
|
|
530
|
+
" border-radius: 999px;",
|
|
531
|
+
" padding: 12px 18px;",
|
|
532
|
+
" background: var(--ink);",
|
|
533
|
+
" color: #fffdf7;",
|
|
534
|
+
" display: inline-flex;",
|
|
535
|
+
" align-items: center;",
|
|
536
|
+
" justify-content: center;",
|
|
537
|
+
" gap: 10px;",
|
|
538
|
+
" cursor: pointer;",
|
|
539
|
+
" transition: transform 120ms ease, opacity 120ms ease, background 120ms ease;",
|
|
540
|
+
"}",
|
|
541
|
+
"",
|
|
542
|
+
".button:hover:not(:disabled) {",
|
|
543
|
+
" transform: translateY(-1px);",
|
|
544
|
+
"}",
|
|
545
|
+
"",
|
|
546
|
+
".button:disabled {",
|
|
547
|
+
" cursor: wait;",
|
|
548
|
+
" opacity: 0.72;",
|
|
549
|
+
"}",
|
|
550
|
+
"",
|
|
551
|
+
".button.ghost {",
|
|
552
|
+
" background: transparent;",
|
|
553
|
+
" 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
|
+
"}",
|
|
570
|
+
"",
|
|
571
|
+
".status-pill {",
|
|
572
|
+
" display: inline-flex;",
|
|
573
|
+
" align-items: center;",
|
|
574
|
+
" justify-content: center;",
|
|
575
|
+
" padding: 6px 10px;",
|
|
576
|
+
" border-radius: 999px;",
|
|
577
|
+
" background: var(--accent-soft);",
|
|
578
|
+
" color: var(--accent);",
|
|
579
|
+
" font-size: 12px;",
|
|
580
|
+
" font-weight: 700;",
|
|
581
|
+
" letter-spacing: 0.04em;",
|
|
582
|
+
" text-transform: uppercase;",
|
|
583
|
+
"}",
|
|
584
|
+
"",
|
|
585
|
+
".task-card {",
|
|
586
|
+
" 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);",
|
|
592
|
+
"}",
|
|
593
|
+
"",
|
|
594
|
+
".task-head {",
|
|
595
|
+
" display: flex;",
|
|
596
|
+
" align-items: center;",
|
|
597
|
+
" justify-content: space-between;",
|
|
598
|
+
" gap: 12px;",
|
|
599
|
+
"}",
|
|
600
|
+
"",
|
|
601
|
+
".comment-item {",
|
|
602
|
+
" display: flex;",
|
|
603
|
+
" align-items: flex-start;",
|
|
604
|
+
" gap: 10px;",
|
|
605
|
+
" color: var(--muted);",
|
|
606
|
+
"}",
|
|
607
|
+
"",
|
|
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;",
|
|
615
|
+
"}",
|
|
616
|
+
"",
|
|
617
|
+
".action-item {",
|
|
618
|
+
" display: grid;",
|
|
619
|
+
" gap: 4px;",
|
|
620
|
+
" padding: 12px 14px;",
|
|
621
|
+
" border-radius: 16px;",
|
|
622
|
+
" background: rgba(15, 118, 110, 0.06);",
|
|
623
|
+
"}",
|
|
624
|
+
"",
|
|
625
|
+
".muted {",
|
|
626
|
+
" color: var(--muted);",
|
|
627
|
+
"}",
|
|
628
|
+
"",
|
|
629
|
+
".empty-state,",
|
|
630
|
+
".error-banner {",
|
|
631
|
+
" border-radius: 18px;",
|
|
632
|
+
" padding: 16px;",
|
|
633
|
+
"}",
|
|
634
|
+
"",
|
|
635
|
+
".empty-state {",
|
|
636
|
+
" background: rgba(15, 118, 110, 0.06);",
|
|
637
|
+
" color: var(--muted);",
|
|
638
|
+
"}",
|
|
639
|
+
"",
|
|
640
|
+
".error-banner {",
|
|
641
|
+
" background: rgba(180, 35, 24, 0.08);",
|
|
642
|
+
" color: var(--danger);",
|
|
643
|
+
" border: 1px solid rgba(180, 35, 24, 0.16);",
|
|
644
|
+
"}",
|
|
645
|
+
"",
|
|
646
|
+
"pre {",
|
|
647
|
+
" overflow: auto;",
|
|
648
|
+
" border-radius: 16px;",
|
|
649
|
+
" padding: 14px;",
|
|
650
|
+
" background: #171717;",
|
|
651
|
+
" color: #f6f6f6;",
|
|
652
|
+
" font-size: 13px;",
|
|
653
|
+
" line-height: 1.5;",
|
|
654
|
+
"}",
|
|
655
|
+
"",
|
|
656
|
+
"code {",
|
|
657
|
+
' font-family: "Cascadia Code", monospace;',
|
|
658
|
+
"}",
|
|
659
|
+
"",
|
|
660
|
+
"@keyframes spin {",
|
|
661
|
+
" to { transform: rotate(360deg); }",
|
|
662
|
+
"}",
|
|
663
|
+
"",
|
|
664
|
+
"@media (min-width: 940px) {",
|
|
665
|
+
" .showcase-grid {",
|
|
666
|
+
" grid-template-columns: 1.05fr 0.95fr;",
|
|
667
|
+
" }",
|
|
668
|
+
"",
|
|
669
|
+
" .panel-wide {",
|
|
670
|
+
" grid-column: 1 / -1;",
|
|
671
|
+
" }",
|
|
672
|
+
"}",
|
|
673
|
+
"",
|
|
674
|
+
"@media (max-width: 720px) {",
|
|
675
|
+
" main {",
|
|
676
|
+
" padding: 32px 16px 56px;",
|
|
677
|
+
" }",
|
|
678
|
+
"",
|
|
679
|
+
" .task-head,",
|
|
680
|
+
" .manifest-list > div,",
|
|
681
|
+
" .panel-head {",
|
|
682
|
+
" grid-template-columns: 1fr;",
|
|
683
|
+
" display: grid;",
|
|
684
|
+
" }",
|
|
685
|
+
"}",
|
|
686
|
+
].join("\n"),
|
|
687
|
+
"src/app/layout.tsx": [
|
|
688
|
+
'import "./globals.css";',
|
|
689
|
+
'import type { ReactNode } from "react";',
|
|
690
|
+
"",
|
|
691
|
+
"export const metadata = {",
|
|
692
|
+
' title: "Ekairos App",',
|
|
693
|
+
' description: "Scaffolded Ekairos domain app",',
|
|
694
|
+
"};",
|
|
695
|
+
"",
|
|
696
|
+
"export default function RootLayout({ children }: { children: ReactNode }) {",
|
|
697
|
+
" return (",
|
|
698
|
+
' <html lang="en">',
|
|
699
|
+
" <body>{children}</body>",
|
|
700
|
+
" </html>",
|
|
701
|
+
" );",
|
|
702
|
+
"}",
|
|
703
|
+
].join("\n"),
|
|
704
|
+
"src/app/page.tsx": [
|
|
705
|
+
'import DomainShowcase from "./domain-showcase";',
|
|
706
|
+
"",
|
|
707
|
+
'export const dynamic = "force-dynamic";',
|
|
708
|
+
"",
|
|
709
|
+
"export default function HomePage() {",
|
|
710
|
+
" return (",
|
|
711
|
+
" <main>",
|
|
712
|
+
' <section className="hero">',
|
|
713
|
+
' <div className="eyebrow">Ekairos Domain Scaffold</div>',
|
|
714
|
+
" <h1>See your domain. Query it. Trigger an action.</h1>",
|
|
715
|
+
" <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.",
|
|
718
|
+
" </p>",
|
|
719
|
+
" </section>",
|
|
720
|
+
"",
|
|
721
|
+
" <DomainShowcase />",
|
|
722
|
+
" </main>",
|
|
723
|
+
" );",
|
|
724
|
+
"}",
|
|
725
|
+
].join("\n"),
|
|
726
|
+
"src/app/domain-showcase.tsx": [
|
|
727
|
+
'"use client";',
|
|
728
|
+
"",
|
|
729
|
+
'import { useEffect, useMemo, useState } from "react";',
|
|
730
|
+
"",
|
|
731
|
+
"type ManifestAction = {",
|
|
732
|
+
" name: string;",
|
|
733
|
+
" key?: string | null;",
|
|
734
|
+
" description?: string | null;",
|
|
735
|
+
"};",
|
|
736
|
+
"",
|
|
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[];",
|
|
744
|
+
"};",
|
|
745
|
+
"",
|
|
746
|
+
"type TaskComment = {",
|
|
747
|
+
" id?: string;",
|
|
748
|
+
" body?: string;",
|
|
749
|
+
" createdAt?: number;",
|
|
750
|
+
"};",
|
|
751
|
+
"",
|
|
752
|
+
"type TaskRow = {",
|
|
753
|
+
" id?: string;",
|
|
754
|
+
" title?: string;",
|
|
755
|
+
" status?: string;",
|
|
756
|
+
" createdAt?: number;",
|
|
757
|
+
" comments?: TaskComment[] | TaskComment;",
|
|
758
|
+
"};",
|
|
759
|
+
"",
|
|
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
|
+
"}",
|
|
775
|
+
"",
|
|
776
|
+
"function asArray<T>(value: T | T[] | null | undefined): T[] {",
|
|
777
|
+
" if (!value) return [];",
|
|
778
|
+
" return Array.isArray(value) ? value : [value];",
|
|
779
|
+
"}",
|
|
780
|
+
"",
|
|
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
|
+
" }",
|
|
788
|
+
"}",
|
|
789
|
+
"",
|
|
790
|
+
"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');",
|
|
794
|
+
" 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
|
+
" }, []);",
|
|
837
|
+
"",
|
|
838
|
+
" async function runAction(action: string, input: Record<string, unknown>) {",
|
|
839
|
+
" setLoadingAction(action);",
|
|
840
|
+
" setError(null);",
|
|
841
|
+
" 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));",
|
|
850
|
+
" } finally {",
|
|
851
|
+
" setLoadingAction(null);",
|
|
852
|
+
" }",
|
|
853
|
+
" }",
|
|
854
|
+
"",
|
|
855
|
+
" 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>',
|
|
862
|
+
" </div>",
|
|
863
|
+
"",
|
|
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
|
+
" />",
|
|
902
|
+
" </label>",
|
|
903
|
+
' <div className="button-row">',
|
|
904
|
+
" <button",
|
|
905
|
+
' className="button"',
|
|
906
|
+
" disabled={loadingAction !== null}",
|
|
907
|
+
" onClick={() => void runAction('app.task.create', { title: draftTitle, status: 'manual' })}",
|
|
908
|
+
" >",
|
|
909
|
+
' {loadingAction === "app.task.create" ? <span className="spinner" aria-hidden="true" /> : null}',
|
|
910
|
+
" Create Task",
|
|
911
|
+
" </button>",
|
|
912
|
+
" <button",
|
|
913
|
+
' className="button ghost"',
|
|
914
|
+
" disabled={loadingAction !== null}",
|
|
915
|
+
" onClick={() => void runAction('app.demo.seed', {})}",
|
|
916
|
+
" >",
|
|
917
|
+
' {loadingAction === "app.demo.seed" ? <span className="spinner" aria-hidden="true" /> : null}',
|
|
918
|
+
" Seed Demo",
|
|
919
|
+
" </button>",
|
|
920
|
+
" </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
|
+
" ))}",
|
|
928
|
+
" </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>',
|
|
937
|
+
" </div>",
|
|
938
|
+
' <span className="status-pill">{loadingData ? "syncing" : `${tasks.length} rows`}</span>',
|
|
939
|
+
" </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>',
|
|
948
|
+
" </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>",
|
|
956
|
+
" ))}",
|
|
957
|
+
" </div>",
|
|
958
|
+
" </article>",
|
|
959
|
+
" ))}",
|
|
960
|
+
" </div>",
|
|
961
|
+
" </article>",
|
|
962
|
+
" </div>",
|
|
963
|
+
"",
|
|
964
|
+
' {error ? <div className="error-banner">{error}</div> : null}',
|
|
965
|
+
" </section>",
|
|
966
|
+
" );",
|
|
967
|
+
"}",
|
|
968
|
+
].join("\n"),
|
|
969
|
+
"src/domain.ts": [
|
|
970
|
+
'import { defineDomainAction, domain } from "@ekairos/domain";',
|
|
971
|
+
'import { i } from "@instantdb/core";',
|
|
972
|
+
"",
|
|
973
|
+
"const baseDomain = domain(\"ekairos.app\")",
|
|
974
|
+
" .schema({",
|
|
975
|
+
" entities: {",
|
|
976
|
+
" app_tasks: i.entity({",
|
|
977
|
+
" title: i.string().indexed(),",
|
|
978
|
+
" status: i.string().indexed(),",
|
|
979
|
+
" createdAt: i.number().indexed(),",
|
|
980
|
+
" }),",
|
|
981
|
+
" app_task_comments: i.entity({",
|
|
982
|
+
" body: i.string(),",
|
|
983
|
+
" createdAt: i.number().indexed(),",
|
|
984
|
+
" }),",
|
|
985
|
+
" },",
|
|
986
|
+
" links: {",
|
|
987
|
+
" taskComments: {",
|
|
988
|
+
' forward: { on: "app_tasks", has: "many", label: "comments" },',
|
|
989
|
+
' reverse: { on: "app_task_comments", has: "one", label: "task" },',
|
|
990
|
+
" },",
|
|
991
|
+
" },",
|
|
992
|
+
" rooms: {},",
|
|
993
|
+
" });",
|
|
994
|
+
"",
|
|
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(),",
|
|
1011
|
+
" }),",
|
|
1012
|
+
" ]);",
|
|
1013
|
+
" return { taskId };",
|
|
1014
|
+
" },",
|
|
1015
|
+
"});",
|
|
1016
|
+
"",
|
|
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(),",
|
|
1034
|
+
" }),",
|
|
1035
|
+
" scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
|
|
1036
|
+
" ]);",
|
|
1037
|
+
" return { commentId, taskId };",
|
|
1038
|
+
" },",
|
|
1039
|
+
"});",
|
|
1040
|
+
"",
|
|
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(),",
|
|
1062
|
+
" }),",
|
|
1063
|
+
" scoped.db.tx.app_task_comments[commentId].link({ task: taskId }),",
|
|
1064
|
+
" ]);",
|
|
1065
|
+
" return { taskId };",
|
|
1066
|
+
" },",
|
|
1067
|
+
"});",
|
|
1068
|
+
"",
|
|
1069
|
+
"export const appDomain = baseDomain.actions({",
|
|
1070
|
+
" addTaskComment: addTaskCommentAction,",
|
|
1071
|
+
" createTask: createTaskAction,",
|
|
1072
|
+
" seedDemo: seedDemoAction,",
|
|
1073
|
+
"});",
|
|
1074
|
+
"",
|
|
1075
|
+
"export default appDomain;",
|
|
1076
|
+
].join("\n"),
|
|
1077
|
+
"src/runtime.ts": [
|
|
1078
|
+
'import { init } from "@instantdb/admin";',
|
|
1079
|
+
'import { EkairosRuntime } from "@ekairos/domain/runtime-handle";',
|
|
1080
|
+
'import { configureRuntime } from "@ekairos/domain/runtime";',
|
|
1081
|
+
'import appDomain from "./domain";',
|
|
1082
|
+
"",
|
|
1083
|
+
"export type AppRuntimeEnv = {",
|
|
1084
|
+
" actorEmail?: string | null;",
|
|
1085
|
+
" actorId?: string;",
|
|
1086
|
+
" adminToken?: string;",
|
|
1087
|
+
" appId?: string;",
|
|
1088
|
+
"};",
|
|
1089
|
+
"",
|
|
1090
|
+
"function resolveRuntimeEnv(env: AppRuntimeEnv = {}): Required<Pick<AppRuntimeEnv, \"appId\" | \"adminToken\">> & AppRuntimeEnv {",
|
|
1091
|
+
' const appId = String(env.appId ?? process.env.NEXT_PUBLIC_INSTANT_APP_ID ?? "").trim();',
|
|
1092
|
+
' const adminToken = String(env.adminToken ?? process.env.INSTANT_ADMIN_TOKEN ?? "").trim();',
|
|
1093
|
+
" if (!appId || !adminToken) {",
|
|
1094
|
+
' throw new Error("Missing NEXT_PUBLIC_INSTANT_APP_ID or INSTANT_ADMIN_TOKEN. Copy .env.example to .env.local and fill both values.");',
|
|
1095
|
+
" }",
|
|
1096
|
+
" return {",
|
|
1097
|
+
" ...env,",
|
|
1098
|
+
" appId,",
|
|
1099
|
+
" adminToken,",
|
|
1100
|
+
" };",
|
|
1101
|
+
"}",
|
|
1102
|
+
"",
|
|
1103
|
+
"export class AppRuntime extends EkairosRuntime<AppRuntimeEnv, typeof appDomain, any> {",
|
|
1104
|
+
" protected getDomain() {",
|
|
1105
|
+
" return appDomain;",
|
|
1106
|
+
" }",
|
|
1107
|
+
"",
|
|
1108
|
+
" protected async resolveDb(env: AppRuntimeEnv) {",
|
|
1109
|
+
" const resolved = resolveRuntimeEnv(env);",
|
|
1110
|
+
" return init({",
|
|
1111
|
+
" appId: resolved.appId,",
|
|
1112
|
+
" adminToken: resolved.adminToken,",
|
|
1113
|
+
" schema: appDomain.toInstantSchema(),",
|
|
1114
|
+
" useDateObjects: true,",
|
|
1115
|
+
" } as any) as any;",
|
|
1116
|
+
" }",
|
|
1117
|
+
"}",
|
|
1118
|
+
"",
|
|
1119
|
+
"export function createRuntime(env: AppRuntimeEnv = {}) {",
|
|
1120
|
+
" return new AppRuntime(resolveRuntimeEnv(env));",
|
|
1121
|
+
"}",
|
|
1122
|
+
"",
|
|
1123
|
+
"export const runtimeConfig = configureRuntime<AppRuntimeEnv>({",
|
|
1124
|
+
" runtime: async (env) => {",
|
|
1125
|
+
" const runtime = createRuntime(env);",
|
|
1126
|
+
" return { db: await runtime.db() };",
|
|
1127
|
+
" },",
|
|
1128
|
+
" domain: {",
|
|
1129
|
+
" domain: appDomain,",
|
|
1130
|
+
" },",
|
|
1131
|
+
"});",
|
|
1132
|
+
].join("\n"),
|
|
1133
|
+
"src/workflows/demo.workflow.ts": [
|
|
1134
|
+
'import { executeRuntimeAction } from "@ekairos/domain/runtime";',
|
|
1135
|
+
'import { createRuntime } from "../runtime";',
|
|
1136
|
+
"",
|
|
1137
|
+
"export type DemoWorkflowInput = {",
|
|
1138
|
+
" title: string;",
|
|
1139
|
+
" comment?: string;",
|
|
1140
|
+
"};",
|
|
1141
|
+
"",
|
|
1142
|
+
"export async function runDemoWorkflow(input: DemoWorkflowInput) {",
|
|
1143
|
+
' "use workflow";',
|
|
1144
|
+
" 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 },",
|
|
1157
|
+
" });",
|
|
1158
|
+
" }",
|
|
1159
|
+
"",
|
|
1160
|
+
" return created;",
|
|
1161
|
+
"}",
|
|
1162
|
+
].join("\n"),
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
export async function createDomainApp(params) {
|
|
1166
|
+
if (params.framework !== "next") {
|
|
1167
|
+
throw new Error("Only --next is supported right now.");
|
|
1168
|
+
}
|
|
1169
|
+
const targetDir = resolve(params.directory || ".");
|
|
1170
|
+
await emitProgress(params.onProgress, {
|
|
1171
|
+
stage: "prepare-target",
|
|
1172
|
+
status: "running",
|
|
1173
|
+
message: `Preparing ${targetDir}`,
|
|
1174
|
+
progress: 5,
|
|
1175
|
+
});
|
|
1176
|
+
await ensureWritableTargetDirectory(targetDir, params.force);
|
|
1177
|
+
await emitProgress(params.onProgress, {
|
|
1178
|
+
stage: "prepare-target",
|
|
1179
|
+
status: "completed",
|
|
1180
|
+
message: "Target ready",
|
|
1181
|
+
progress: 12,
|
|
1182
|
+
});
|
|
1183
|
+
await emitProgress(params.onProgress, {
|
|
1184
|
+
stage: "detect-package-manager",
|
|
1185
|
+
status: "running",
|
|
1186
|
+
message: "Detecting package manager",
|
|
1187
|
+
progress: 16,
|
|
1188
|
+
});
|
|
1189
|
+
const packageManager = await detectPackageManager(params.packageManager, params.workspacePath);
|
|
1190
|
+
await emitProgress(params.onProgress, {
|
|
1191
|
+
stage: "detect-package-manager",
|
|
1192
|
+
status: "completed",
|
|
1193
|
+
message: `Using ${packageManager}`,
|
|
1194
|
+
progress: 22,
|
|
1195
|
+
});
|
|
1196
|
+
await emitProgress(params.onProgress, {
|
|
1197
|
+
stage: "resolve-version",
|
|
1198
|
+
status: "running",
|
|
1199
|
+
message: "Resolving @ekairos/domain version",
|
|
1200
|
+
progress: 26,
|
|
1201
|
+
});
|
|
1202
|
+
const domainVersion = await readDomainPackageVersion();
|
|
1203
|
+
await emitProgress(params.onProgress, {
|
|
1204
|
+
stage: "resolve-version",
|
|
1205
|
+
status: "completed",
|
|
1206
|
+
message: `Version ${domainVersion}`,
|
|
1207
|
+
progress: 30,
|
|
1208
|
+
});
|
|
1209
|
+
const explicitAppId = trimOrEmpty(params.appId);
|
|
1210
|
+
const explicitAdminToken = trimOrEmpty(params.adminToken);
|
|
1211
|
+
const shouldProvision = Boolean(trimOrEmpty(params.instantToken)) &&
|
|
1212
|
+
(!explicitAppId || !explicitAdminToken);
|
|
1213
|
+
let provisioned = null;
|
|
1214
|
+
if (shouldProvision) {
|
|
1215
|
+
await emitProgress(params.onProgress, {
|
|
1216
|
+
stage: "provision-instant",
|
|
1217
|
+
status: "running",
|
|
1218
|
+
message: "Provisioning Instant app",
|
|
1219
|
+
progress: 38,
|
|
1220
|
+
});
|
|
1221
|
+
provisioned = await provisionInstantApp({
|
|
1222
|
+
directory: targetDir,
|
|
1223
|
+
instantToken: trimOrEmpty(params.instantToken),
|
|
1224
|
+
orgId: params.orgId,
|
|
1225
|
+
});
|
|
1226
|
+
await emitProgress(params.onProgress, {
|
|
1227
|
+
stage: "provision-instant",
|
|
1228
|
+
status: "completed",
|
|
1229
|
+
message: `Provisioned ${provisioned.appId}`,
|
|
1230
|
+
progress: 50,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
const appId = explicitAppId || provisioned?.appId || null;
|
|
1234
|
+
const adminToken = explicitAdminToken || provisioned?.adminToken || null;
|
|
1235
|
+
await emitProgress(params.onProgress, {
|
|
1236
|
+
stage: "write-files",
|
|
1237
|
+
status: "running",
|
|
1238
|
+
message: "Writing scaffold files",
|
|
1239
|
+
progress: 58,
|
|
1240
|
+
});
|
|
1241
|
+
const files = buildNextTemplateFiles({
|
|
1242
|
+
targetDir,
|
|
1243
|
+
domainVersion,
|
|
1244
|
+
packageManager,
|
|
1245
|
+
workspacePath: params.workspacePath,
|
|
1246
|
+
});
|
|
1247
|
+
await writeScaffoldFiles(targetDir, files);
|
|
1248
|
+
await emitProgress(params.onProgress, {
|
|
1249
|
+
stage: "write-files",
|
|
1250
|
+
status: "completed",
|
|
1251
|
+
message: "Scaffold files written",
|
|
1252
|
+
progress: 72,
|
|
1253
|
+
});
|
|
1254
|
+
if (appId && adminToken) {
|
|
1255
|
+
await emitProgress(params.onProgress, {
|
|
1256
|
+
stage: "write-env",
|
|
1257
|
+
status: "running",
|
|
1258
|
+
message: "Writing .env.local",
|
|
1259
|
+
progress: 78,
|
|
1260
|
+
});
|
|
1261
|
+
await writeFile(join(targetDir, ".env.local"), [
|
|
1262
|
+
`NEXT_PUBLIC_INSTANT_APP_ID=${appId}`,
|
|
1263
|
+
`INSTANT_ADMIN_TOKEN=${adminToken}`,
|
|
1264
|
+
"",
|
|
1265
|
+
].join("\n"), "utf8");
|
|
1266
|
+
await emitProgress(params.onProgress, {
|
|
1267
|
+
stage: "write-env",
|
|
1268
|
+
status: "completed",
|
|
1269
|
+
message: ".env.local ready",
|
|
1270
|
+
progress: 84,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
if (params.install) {
|
|
1274
|
+
await emitProgress(params.onProgress, {
|
|
1275
|
+
stage: "install",
|
|
1276
|
+
status: "running",
|
|
1277
|
+
message: `Installing dependencies with ${packageManager}`,
|
|
1278
|
+
progress: 88,
|
|
1279
|
+
});
|
|
1280
|
+
await runInstall(targetDir, packageManager, params.onProgress);
|
|
1281
|
+
await emitProgress(params.onProgress, {
|
|
1282
|
+
stage: "install",
|
|
1283
|
+
status: "completed",
|
|
1284
|
+
message: "Dependencies installed",
|
|
1285
|
+
progress: 96,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
const nextSteps = [
|
|
1289
|
+
`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",
|
|
1297
|
+
];
|
|
1298
|
+
const result = {
|
|
1299
|
+
ok: true,
|
|
1300
|
+
directory: targetDir,
|
|
1301
|
+
framework: params.framework,
|
|
1302
|
+
installed: params.install,
|
|
1303
|
+
packageManager,
|
|
1304
|
+
provisioned: Boolean(provisioned),
|
|
1305
|
+
appId,
|
|
1306
|
+
adminToken,
|
|
1307
|
+
nextSteps,
|
|
1308
|
+
};
|
|
1309
|
+
await emitProgress(params.onProgress, {
|
|
1310
|
+
stage: "complete",
|
|
1311
|
+
status: "completed",
|
|
1312
|
+
message: "App scaffolded successfully",
|
|
1313
|
+
progress: 100,
|
|
1314
|
+
});
|
|
1315
|
+
return result;
|
|
1316
|
+
}
|
|
1317
|
+
//# sourceMappingURL=create-app.js.map
|