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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +177 -18
  2. package/SKILL.md +56 -0
  3. package/dist/cli/bin.d.ts +1 -1
  4. package/dist/cli/bin.d.ts.map +1 -1
  5. package/dist/cli/bin.js +141 -20
  6. package/dist/cli/bin.js.map +1 -1
  7. package/dist/cli/create-app.d.ts +34 -1
  8. package/dist/cli/create-app.d.ts.map +1 -1
  9. package/dist/cli/create-app.js +2138 -507
  10. package/dist/cli/create-app.js.map +1 -1
  11. package/dist/cli/http.d.ts.map +1 -1
  12. package/dist/cli/http.js +1 -5
  13. package/dist/cli/http.js.map +1 -1
  14. package/dist/cli/server.d.ts.map +1 -1
  15. package/dist/cli/server.js +5 -2
  16. package/dist/cli/server.js.map +1 -1
  17. package/dist/cli/types.d.ts +1 -0
  18. package/dist/cli/types.d.ts.map +1 -1
  19. package/dist/cli/ui.d.ts.map +1 -1
  20. package/dist/cli/ui.js +2 -0
  21. package/dist/cli/ui.js.map +1 -1
  22. package/dist/context.test-runner.js +3 -1
  23. package/dist/context.test-runner.js.map +1 -1
  24. package/dist/domain-doc.d.ts +2 -0
  25. package/dist/domain-doc.d.ts.map +1 -1
  26. package/dist/domain-doc.js +14 -0
  27. package/dist/domain-doc.js.map +1 -1
  28. package/dist/index.d.ts +194 -53
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +313 -137
  31. package/dist/index.js.map +1 -1
  32. package/dist/next.d.ts.map +1 -1
  33. package/dist/next.js +3 -2
  34. package/dist/next.js.map +1 -1
  35. package/dist/runtime-handle.d.ts +14 -3
  36. package/dist/runtime-handle.d.ts.map +1 -1
  37. package/dist/runtime-handle.js +2 -0
  38. package/dist/runtime-handle.js.map +1 -1
  39. package/dist/runtime-step.d.ts.map +1 -1
  40. package/dist/runtime-step.js +2 -0
  41. package/dist/runtime-step.js.map +1 -1
  42. package/dist/runtime.d.ts +7 -7
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +11 -8
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +15 -11
package/README.md CHANGED
@@ -7,32 +7,38 @@ Operate it through a CLI.
7
7
 
8
8
  ## What you get
9
9
 
10
- - Composed domain graphs with `domain(...).schema(...)`
10
+ - Composed domain graphs with `domain(...).withSchema(...)`
11
11
  - Explicit runtimes with `EkairosRuntime`
12
- - Step-safe write boundaries with `defineDomainAction(...)`
12
+ - Step-safe write boundaries with `defineAction(...)`
13
13
  - Workflow-ready action execution with `executeRuntimeAction(...)`
14
- - A built-in CLI for `create-app`, `inspect`, `action`, and `query`
14
+ - A companion CLI through `@ekairos/cli` for `create-app`, `inspect`, `action`, and `query`
15
15
 
16
16
  ## Start Fast
17
17
 
18
18
  Scaffold a Next app that already exposes the Ekairos domain endpoint:
19
19
 
20
20
  ```bash
21
- npx @ekairos/domain create-app my-app --next
21
+ ekairos create-app my-app --next
22
+ ```
23
+
24
+ Run the full supply-chain demo cycle:
25
+
26
+ ```bash
27
+ ekairos create-app --demo
22
28
  ```
23
29
 
24
30
  If you already have an Instant platform token, provision the app and write `.env.local` in one pass:
25
31
 
26
32
  ```bash
27
- npx @ekairos/domain create-app my-app --next --instantToken=$INSTANT_PERSONAL_ACCESS_TOKEN
33
+ ekairos create-app my-app --next --instantToken=$INSTANT_PERSONAL_ACCESS_TOKEN
28
34
  ```
29
35
 
30
36
  Then run it and inspect it:
31
37
 
32
38
  ```bash
33
- npx @ekairos/domain inspect --baseUrl=http://localhost:3000 --admin --pretty
34
- npx @ekairos/domain seedDemo --baseUrl=http://localhost:3000 --admin --pretty
35
- npx @ekairos/domain query "{ app_tasks: { comments: {} } }" --baseUrl=http://localhost:3000 --admin --pretty
39
+ ekairos domain inspect --baseUrl=http://localhost:3000 --admin --pretty
40
+ ekairos domain "supplyChain.order.launch" "{ reference: 'PO-7842', supplierName: 'Marula Components', sku: 'DRV-2048' }" --baseUrl=http://localhost:3000 --admin --pretty
41
+ ekairos domain query "{ procurement_order: { supplier: {}, stockItems: {}, shipments: { inspections: {} } } }" --baseUrl=http://localhost:3000 --admin --pretty
36
42
  ```
37
43
 
38
44
  ## Next.js Route
@@ -65,19 +71,18 @@ const nextConfig: NextConfig = {
65
71
  export default withWorkflow(nextConfig) as NextConfig;
66
72
  ```
67
73
 
68
- The CLI uses `/api/ekairos/domain` by default and falls back to the legacy
69
- `/.well-known/ekairos/v1/domain` endpoint for older apps.
74
+ The CLI uses `/api/ekairos/domain`.
70
75
 
71
76
  ## Core Pattern
72
77
 
73
78
  ```ts
74
- import { defineDomainAction, domain } from "@ekairos/domain";
79
+ import { defineAction, domain } from "@ekairos/domain";
75
80
  import { EkairosRuntime } from "@ekairos/domain/runtime-handle";
76
81
  import { executeRuntimeAction } from "@ekairos/domain/runtime";
77
82
  import { init } from "@instantdb/admin";
78
83
  import { i } from "@instantdb/core";
79
84
 
80
- const baseDomain = domain("tasks").schema({
85
+ const baseDomain = domain("tasks").withSchema({
81
86
  entities: {
82
87
  tasks: i.entity({
83
88
  title: i.string().indexed(),
@@ -88,7 +93,7 @@ const baseDomain = domain("tasks").schema({
88
93
  rooms: {},
89
94
  });
90
95
 
91
- export const createTaskAction = defineDomainAction({
96
+ export const createTaskAction = defineAction({
92
97
  name: "tasks.create",
93
98
  async execute({ runtime, input }) {
94
99
  "use step";
@@ -98,10 +103,13 @@ export const createTaskAction = defineDomainAction({
98
103
  },
99
104
  });
100
105
 
101
- export const appDomain = baseDomain.actions({
106
+ export const appDomain = baseDomain.withActions({
102
107
  createTask: createTaskAction,
103
108
  });
104
109
 
110
+ // Raw definitions stay available for reflection and adapters:
111
+ appDomain.actions.createTask;
112
+
105
113
  export class AppRuntime extends EkairosRuntime<{
106
114
  appId?: string;
107
115
  adminToken?: string;
@@ -114,7 +122,7 @@ export class AppRuntime extends EkairosRuntime<{
114
122
  return init({
115
123
  appId: env.appId!,
116
124
  adminToken: env.adminToken!,
117
- schema: appDomain.toInstantSchema(),
125
+ schema: appDomain.instantSchema(),
118
126
  useDateObjects: true,
119
127
  } as any);
120
128
  }
@@ -147,14 +155,93 @@ export async function runWorkflow() {
147
155
  - Keep workflow orchestration above actions.
148
156
  - Use `DOMAIN.md` plus `domain.contextString()` when an AI agent needs the model explained.
149
157
 
158
+ ## Public And Full Domains
159
+
160
+ Use normal domain composition to split browser-visible schema from server/runtime
161
+ capabilities. The public domain is a smaller domain. The full domain imports and
162
+ extends it.
163
+
164
+ ```ts
165
+ // @acme/sandbox/public
166
+ export const sandboxDomain = domain("sandbox").withSchema({
167
+ entities: {
168
+ sandbox_sandboxes: i.entity({
169
+ provider: i.string().indexed(),
170
+ status: i.string().indexed(),
171
+ createdAt: i.number().indexed(),
172
+ }),
173
+ },
174
+ links: {},
175
+ rooms: {},
176
+ });
177
+ ```
178
+
179
+ ```ts
180
+ // @acme/sandbox
181
+ import { sandboxDomain as publicSandboxDomain } from "@acme/sandbox/public";
182
+
183
+ export const sandboxDomain = domain("sandbox")
184
+ .includes(publicSandboxDomain)
185
+ .withSchema({
186
+ entities: {
187
+ sandbox_processes: i.entity({
188
+ kind: i.string().indexed(),
189
+ status: i.string().indexed(),
190
+ command: i.string(),
191
+ startedAt: i.number().indexed(),
192
+ }),
193
+ },
194
+ links: {
195
+ sandboxProcessSandbox: {
196
+ forward: { on: "sandbox_processes", has: "one", label: "sandbox" },
197
+ reverse: { on: "sandbox_sandboxes", has: "many", label: "processes" },
198
+ },
199
+ },
200
+ rooms: {},
201
+ })
202
+ .withActions({
203
+ runCommand: defineAction({
204
+ name: "sandbox.runCommand",
205
+ async execute({ runtime, input }) {
206
+ "use step";
207
+ // server/provider work
208
+ },
209
+ }),
210
+ });
211
+ ```
212
+
213
+ Client schema composition imports public domains:
214
+
215
+ ```ts
216
+ import { sandboxDomain } from "@acme/sandbox/public";
217
+
218
+ export const appDomain = domain("app")
219
+ .includes(sandboxDomain)
220
+ .withSchema({ entities: {}, links: {}, rooms: {} });
221
+
222
+ export default appDomain.instantSchema();
223
+ ```
224
+
225
+ Server runtime imports full domains:
226
+
227
+ ```ts
228
+ import { sandboxDomain } from "@acme/sandbox";
229
+
230
+ const sandbox = await runtime.use(sandboxDomain);
231
+ await sandbox.actions.runCommand({ sandboxId, command: "pnpm", args: ["test"] });
232
+ ```
233
+
234
+ This pattern controls schema visibility only. It is not an authorization model:
235
+ configure Instant permissions for actual data access.
236
+
150
237
  ## CLI Input Quality Of Life
151
238
 
152
239
  The CLI accepts JSON5, `@file`, and stdin:
153
240
 
154
241
  ```bash
155
- npx @ekairos/domain query "{ tasks: { $: { limit: 5 } } }" --admin
156
- npx @ekairos/domain query @query.json5 --admin
157
- cat query.json5 | npx @ekairos/domain query - --admin
242
+ ekairos domain query "{ tasks: { $: { limit: 5 } } }" --admin
243
+ ekairos domain query @query.json5 --admin
244
+ cat query.json5 | ekairos domain query - --admin
158
245
  ```
159
246
 
160
247
  Add `--meta` when you need to know whether a query used the local client runtime path or the server route.
@@ -166,3 +253,75 @@ pnpm --filter @ekairos/domain test
166
253
  pnpm --filter @ekairos/domain test:cli
167
254
  pnpm --filter @ekairos/domain test:workflow
168
255
  ```
256
+
257
+ ## Type And DX Notes
258
+
259
+ `@ekairos/domain` intentionally encapsulates InstantDB at the domain boundary, but
260
+ the schema returned by a domain must remain usable anywhere an InstantDB schema is
261
+ expected.
262
+
263
+ Use `instantSchema()` when provisioning or passing a runtime schema to InstantDB:
264
+
265
+ ```ts
266
+ const db = init({
267
+ appId,
268
+ adminToken,
269
+ schema: appDomain.instantSchema(),
270
+ });
271
+ ```
272
+
273
+ Use `DomainInstantSchema<typeof domain>` when you need the schema type for
274
+ `db.query`, `InstaQLParams`, `InstaQLResult`, or service constructors:
275
+
276
+ ```ts
277
+ import type { DomainInstantSchema } from "@ekairos/domain";
278
+ import type { InstantAdminDatabase } from "@instantdb/admin";
279
+
280
+ type AppSchema = DomainInstantSchema<typeof appDomain>;
281
+
282
+ function createService(db: InstantAdminDatabase<AppSchema, true>) {
283
+ return db.query({
284
+ tasks: {
285
+ owner: {},
286
+ },
287
+ });
288
+ }
289
+ ```
290
+
291
+ Prefer exported domain values without widening their type:
292
+
293
+ ```ts
294
+ export const tasksDomain = domain("tasks").withSchema({
295
+ entities: { tasks: i.entity({ title: i.string() }) },
296
+ links: {},
297
+ rooms: {},
298
+ });
299
+ ```
300
+
301
+ Avoid annotating exported domains as plain `DomainSchemaResult` unless you need to
302
+ hide their concrete shape. That annotation widens the domain name and schema, so
303
+ TypeScript loses some compile-time checks for `runtime.use(...)`, `RuntimeForDomain`,
304
+ and composed queries.
305
+
306
+ ```ts
307
+ // Avoid for runtime/domain composition:
308
+ export const tasksDomain: DomainSchemaResult = domain("tasks").withSchema(...);
309
+ ```
310
+
311
+ Type tests under `src/__type_tests__` are intentionally split by use case:
312
+
313
+ - `domain.schema-*.typecheck.ts`: schema extraction, entity/link visibility, and query shape.
314
+ - `domain.includes-*.typecheck.ts`: composed entities, transitive links, and traversal direction.
315
+ - `domain.instaql-fetch.typecheck.ts`: namespaces, associations, deferred query shapes, and `queryOnce`.
316
+ - `domain.instaql-filters.typecheck.ts`: dotted relation filters and advanced where operators.
317
+ - `domain.instaql-options.typecheck.ts`: pagination, ordering, selected fields, and page info.
318
+ - `domain.instantdb-*.typecheck.ts`: InstantDB query, entity, and result helpers.
319
+ - `domain.query-negative-*.typecheck.ts`: invalid entities and relation labels stay rejected.
320
+ - `domain.dx-*.typecheck.ts`: public helper aliases, literal names, and schema helpers.
321
+ - `runtime.domain-names-*.typecheck.ts`: `RuntimeForDomain` validates name plus schema.
322
+ - `workflow-output-*.typecheck.ts`: workflow serde output contracts.
323
+
324
+ When a type regression appears, add one small file or one focused case to the
325
+ matching file. Avoid broad "kitchen sink" type tests; they make IntelliSense and
326
+ compiler failures hard to read.
327
+
package/SKILL.md ADDED
@@ -0,0 +1,56 @@
1
+ # Skill: ekairos-domain
2
+
3
+ Use this skill when creating, editing, reviewing, or operating Ekairos domain code.
4
+
5
+ ## Core Contract
6
+
7
+ - Model knowledge boundaries as domains first. A domain owns one coherent concept.
8
+ - Domain names are camelCase, for example `supplierNetwork`, `procurement`, `qualityControl`.
9
+ - Entity names must follow `<domainName>_<entityName>`, for example `supplierNetwork_supplier`.
10
+ - Put writes behind `defineDomainAction(...)`.
11
+ - Keep action bodies step-safe:
12
+ `async execute({ runtime, input }) { "use step"; const scoped = await runtime.use(appDomain); ... }`
13
+ - Prefer typed action calls in app-owned code:
14
+ `const scoped = await runtime.use(appDomain); await scoped.actions.launchOrder(input);`
15
+ - Use string action names only for dynamic runtime/HTTP/CLI execution.
16
+ - Use explicit runtime classes that extend `EkairosRuntime`.
17
+ - New Next.js apps expose `/api/ekairos/domain` through `createRuntimeRouteHandler({ createRuntime })`.
18
+ - Do not use or reintroduce `withRuntime(...)`.
19
+
20
+ ## Domain Design Workflow
21
+
22
+ 1. Identify the knowledge domains before writing schema.
23
+ 2. Name every entity with its owning domain prefix.
24
+ 3. Use `domain(...).includes(...)` only when one domain needs another domain's entities or links.
25
+ 4. Keep root app domains thin. The root should usually include domains and register actions.
26
+ 5. Write actions around business use cases, not CRUD verbs.
27
+ 6. Verify the graph with a nested query that crosses at least two domains.
28
+
29
+ ## Schema Rules
30
+
31
+ - Prefer operational names over abstract names.
32
+ - Avoid generic `app_*` entities in new examples unless the domain itself is `app`.
33
+ - Do not duplicate entity names across domains.
34
+ - Links should describe the business relationship, not implementation plumbing.
35
+ - Use optional attributes when migrating an existing app with data.
36
+
37
+ ## Runtime And Actions
38
+
39
+ - Dynamic endpoints may call `executeRuntimeAction({ action: "domain.action", input })`.
40
+ - Workflows and app code should use typed scoped actions.
41
+ - Action outputs should return durable ids needed by callers.
42
+ - Keep actions idempotent where possible when they will be used by smoke tests or agents.
43
+
44
+ ## Verification
45
+
46
+ For a generated or edited app:
47
+
48
+ 1. Run `pnpm typecheck`.
49
+ 2. Start the app locally.
50
+ 3. Verify `GET /api/ekairos/domain`.
51
+ 4. Execute at least one domain action.
52
+ 5. Query the linked graph produced by that action.
53
+
54
+ ## Related Skills
55
+
56
+ - CLI/application workflow skill: `../cli/SKILL.md`
package/dist/cli/bin.d.ts CHANGED
@@ -4,6 +4,6 @@ type CliContext = {
4
4
  stdout: Pick<typeof output, "write">;
5
5
  stderr: Pick<typeof output, "write">;
6
6
  };
7
- export declare function runCli(argv: string[], ctx?: CliContext): Promise<1 | 0>;
7
+ export declare function runCli(argv: string[], ctx?: CliContext): Promise<0 | 1>;
8
8
  export {};
9
9
  //# sourceMappingURL=bin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AAEA,OAAO,EAAkB,MAAM,IAAI,MAAM,EAAE,MAAM,cAAc,CAAA;AAgB/D,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC,CAAA;AAkiBD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,UAA+C,kBAkDrD"}
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AAGA,OAAO,EAAkB,MAAM,IAAI,MAAM,EAAE,MAAM,cAAc,CAAA;AAkB/D,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,MAAM,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,OAAO,CAAC,CAAA;CACrC,CAAA;AAgqBD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,UAA+C,kBAuDrD"}
package/dist/cli/bin.js CHANGED
@@ -1,14 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { stdin as input, stdout as output } from "node:process";
4
5
  import readline from "node:readline/promises";
5
6
  import JSON5 from "json5";
6
7
  import { clearCliSession, fetchDomainManifest, normalizeBaseUrl, postDomainAction, postDomainQuery, readCliSession, writeCliSession, ClientRuntime, } from "./index.js";
7
- import { createDomainApp } from "./create-app.js";
8
+ import { createDomainApp, normalizeCreateAppTemplate } from "./create-app.js";
8
9
  import { runCreateAppInk } from "./ui.js";
10
+ const DOMAIN_SKILL_PATH = fileURLToPath(new URL("../../SKILL.md", import.meta.url));
9
11
  function printHelp(ctx) {
10
12
  ctx.stdout.write([
11
- "ekairos-domain",
13
+ "ekairos domain",
14
+ "",
15
+ "This programmatic CLI is executed by @ekairos/cli.",
16
+ `Skill: ${DOMAIN_SKILL_PATH}`,
12
17
  "",
13
18
  "Commands:",
14
19
  " login <baseUrl> [--refreshToken=<token>] [--appId=<appId>]",
@@ -24,17 +29,51 @@ function printHelp(ctx) {
24
29
  " - Read JSON5 from stdin",
25
30
  "",
26
31
  "Shorthand:",
27
- " ekairos-domain <actionKey> <json5|@file|-> [--env=<json5>]",
32
+ " ekairos domain <actionKey> <json5|@file|-> [--env=<json5>]",
28
33
  "",
29
34
  "Output:",
30
35
  " Stable JSON by default. Add --pretty for indented JSON.",
31
- " In interactive terminals, create-app shows a live React/Ink UI unless you pass --json.",
36
+ " create-app uses an interactive UI in TTY terminals.",
37
+ " Non-interactive create-app runs only when you pass --json, --plain, or --no-ui.",
32
38
  "",
33
39
  "Scaffold flags:",
34
- " --workspace <path> Use the local workspace package instead of a published version",
35
- " --instantToken Provision an Instant app and write .env.local",
36
- " --appId/--adminToken Reuse an existing Instant app",
37
- " --json Force plain JSON output and disable the interactive UI",
40
+ " Run `ekairos create-app --help` for create-app flags.",
41
+ "",
42
+ ].join("\n"));
43
+ }
44
+ function printCreateAppHelp(ctx) {
45
+ ctx.stdout.write([
46
+ "ekairos create-app",
47
+ "",
48
+ `Skill: ${DOMAIN_SKILL_PATH}`,
49
+ "",
50
+ "Usage:",
51
+ " ekairos create-app [dir] --next [options]",
52
+ " ekairos create-app [dir] --framework=next [options]",
53
+ "",
54
+ "Interactive mode:",
55
+ " In a TTY, omit --next/--framework and the CLI asks which framework to use.",
56
+ "",
57
+ "Non-interactive mode:",
58
+ " Pass --json, --plain, or --no-ui explicitly.",
59
+ " Non-interactive runs must pass --next or --framework=next.",
60
+ "",
61
+ "Options:",
62
+ " --next, --framework=next Scaffold a Next.js app",
63
+ " --template=<name> Template: empty, supply-chain, or agent",
64
+ " --install / --no-install Install dependencies after writing files",
65
+ " --package-manager=<name> npm, pnpm, yarn, or bun",
66
+ " --instantToken=<token> Provision an Instant app and write .env.local",
67
+ " --appId=<id> Reuse an existing Instant app",
68
+ " --adminToken=<token> Reuse an existing Instant admin token",
69
+ " --orgId=<id> Provision under an Instant org",
70
+ " --workspace=<path> Use a local Ekairos workspace package",
71
+ " --force Replace a non-empty target directory",
72
+ " --demo Run the full supply-chain demo cycle",
73
+ " --smoke Typecheck, start Next, seed, and query the app",
74
+ " --keep-server With --smoke, leave the review server running",
75
+ " --json Print stable JSON and disable the interactive UI",
76
+ " --print-secrets Include secrets in JSON output; off by default",
38
77
  "",
39
78
  ].join("\n"));
40
79
  }
@@ -93,9 +132,49 @@ function shouldUseInteractiveCli(flags, ctx) {
93
132
  Boolean(output.isTTY) &&
94
133
  !hasFlag(flags, ["json", "plain", "no-ui"]));
95
134
  }
135
+ function hasHelpArg(args) {
136
+ return args.some((arg) => arg === "--help" || arg === "-h" || arg === "help");
137
+ }
138
+ function isExplicitNonInteractive(flags) {
139
+ return hasFlag(flags, ["json", "plain", "no-ui", "demo"]);
140
+ }
96
141
  function asTrimmedString(value) {
97
142
  return typeof value === "string" ? value.trim() : "";
98
143
  }
144
+ function normalizeFramework(value) {
145
+ const normalized = value.trim().toLowerCase();
146
+ if (!normalized)
147
+ return "";
148
+ if (["next", "nextjs", "next.js"].includes(normalized))
149
+ return "next";
150
+ throw new Error(`Unsupported framework: ${value}. Supported framework: next.`);
151
+ }
152
+ function resolveFrameworkFlag(flags) {
153
+ if (hasFlag(flags, ["next"]))
154
+ return "next";
155
+ const framework = asTrimmedString(flagValue(flags, ["framework"]));
156
+ return normalizeFramework(framework);
157
+ }
158
+ async function promptCreateAppFramework() {
159
+ const rl = readline.createInterface({ input, output });
160
+ try {
161
+ const answer = await rl.question("Framework [Next.js]: ");
162
+ const framework = normalizeFramework(String(answer ?? "").trim() || "next");
163
+ if (framework !== "next") {
164
+ throw new Error(`Unsupported framework: ${framework}`);
165
+ }
166
+ return "next";
167
+ }
168
+ finally {
169
+ rl.close();
170
+ }
171
+ }
172
+ function createAppOutputData(result, flags) {
173
+ if (hasFlag(flags, ["print-secrets", "printSecrets"]))
174
+ return result;
175
+ const { adminToken: _adminToken, ...safeResult } = result;
176
+ return safeResult;
177
+ }
99
178
  async function promptRefreshToken() {
100
179
  const rl = readline.createInterface({ input, output });
101
180
  try {
@@ -127,6 +206,13 @@ function toJsonText(value, flags) {
127
206
  function writeJson(stream, value, flags) {
128
207
  stream.write(toJsonText(value, flags));
129
208
  }
209
+ async function flushCliOutput(stream) {
210
+ if (stream !== output)
211
+ return;
212
+ await new Promise((resolveFlush) => {
213
+ output.write("", () => resolveFlush());
214
+ });
215
+ }
130
216
  async function readStdinText() {
131
217
  let data = "";
132
218
  for await (const chunk of input) {
@@ -292,18 +378,40 @@ async function commandInspect(args, ctx) {
292
378
  }, flags);
293
379
  }
294
380
  async function commandCreateApp(args, ctx) {
381
+ if (hasHelpArg(args)) {
382
+ printCreateAppHelp(ctx);
383
+ return;
384
+ }
295
385
  const { positionals, flags } = parseFlags(args);
296
- if (!hasFlag(flags, ["next"])) {
297
- throw new Error("create-app currently requires --next");
386
+ const interactive = shouldUseInteractiveCli(flags, ctx);
387
+ const demo = hasFlag(flags, ["demo"]);
388
+ if (!interactive && !isExplicitNonInteractive(flags)) {
389
+ throw new Error("create-app non-interactive mode is explicit. Pass --json, --plain, or --no-ui.");
390
+ }
391
+ let framework = resolveFrameworkFlag(flags);
392
+ if (!framework && demo) {
393
+ framework = "next";
394
+ }
395
+ if (!framework && interactive) {
396
+ framework = await promptCreateAppFramework();
298
397
  }
299
- const directory = String(positionals[0] ?? ".").trim() || ".";
398
+ if (!framework) {
399
+ throw new Error("framework is required. Pass --next or --framework=next.");
400
+ }
401
+ const directory = String(positionals[0] ?? (demo ? "ekairos-supply-chain-demo" : ".")).trim() ||
402
+ (demo ? "ekairos-supply-chain-demo" : ".");
403
+ const template = normalizeCreateAppTemplate(flagValue(flags, ["template"]));
300
404
  const install = flagValue(flags, ["install"]) !== false;
405
+ const keptServers = [];
301
406
  const params = {
302
407
  directory,
303
- framework: "next",
408
+ framework,
304
409
  install,
410
+ template: template ?? undefined,
411
+ demo,
305
412
  force: hasFlag(flags, ["force"]),
306
- packageManager: asTrimmedString(flagValue(flags, ["packageManager", "package-manager"])),
413
+ packageManager: asTrimmedString(flagValue(flags, ["packageManager", "package-manager"])) ||
414
+ (demo ? "pnpm" : ""),
307
415
  workspacePath: asTrimmedString(flagValue(flags, ["workspace"])),
308
416
  instantToken: asTrimmedString(flagValue(flags, ["instantToken", "instant-token"])) ||
309
417
  asTrimmedString(process.env.INSTANT_PERSONAL_ACCESS_TOKEN) ||
@@ -312,19 +420,28 @@ async function commandCreateApp(args, ctx) {
312
420
  orgId: asTrimmedString(flagValue(flags, ["orgId", "org-id"])),
313
421
  appId: asTrimmedString(flagValue(flags, ["appId", "app-id"])),
314
422
  adminToken: asTrimmedString(flagValue(flags, ["adminToken", "admin-token"])),
423
+ smoke: demo || hasFlag(flags, ["smoke"]),
424
+ keepServer: demo || hasFlag(flags, ["keep-server", "keepServer"]),
425
+ onKeepServer(server) {
426
+ keptServers.push(server.unref);
427
+ },
315
428
  };
316
- const interactive = shouldUseInteractiveCli(flags, ctx);
317
429
  const result = interactive
318
430
  ? await runCreateAppInk(params)
319
431
  : await createDomainApp(params);
320
432
  if (interactive) {
433
+ for (const unref of keptServers)
434
+ unref();
321
435
  return;
322
436
  }
323
437
  writeJson(ctx.stdout, {
324
438
  ok: true,
325
439
  command: "create-app",
326
- data: result,
440
+ data: createAppOutputData(result, flags),
327
441
  }, flags);
442
+ await flushCliOutput(ctx.stdout);
443
+ for (const unref of keptServers)
444
+ unref();
328
445
  }
329
446
  async function commandQuery(args, ctx) {
330
447
  const { positionals, flags } = parseFlags(args);
@@ -428,10 +545,17 @@ async function commandLogout(args, ctx) {
428
545
  export async function runCli(argv, ctx = { stdout: output, stderr: output }) {
429
546
  const [command, ...rest] = argv;
430
547
  try {
431
- if (!command || command === "help" || command === "--help" || command === "-h") {
548
+ if (!command || command === "--help" || command === "-h") {
432
549
  printHelp(ctx);
433
550
  return 0;
434
551
  }
552
+ if (command === "help") {
553
+ if (rest[0] === "create-app")
554
+ printCreateAppHelp(ctx);
555
+ else
556
+ printHelp(ctx);
557
+ return 0;
558
+ }
435
559
  if (command === "login") {
436
560
  await commandLogin(rest, ctx);
437
561
  return 0;
@@ -475,10 +599,7 @@ const isDirectExecution = (() => {
475
599
  const normalized = current.replace(/\\/g, "/");
476
600
  const fileName = normalized.split("/").pop() ?? "";
477
601
  return (current.endsWith("bin.js") ||
478
- current.endsWith("bin.ts") ||
479
- fileName === "ekairos-domain" ||
480
- fileName === "ekairos-domain.cmd" ||
481
- fileName === "ekairos-domain.ps1");
602
+ current.endsWith("bin.ts"));
482
603
  })();
483
604
  if (isDirectExecution) {
484
605
  runCli(process.argv.slice(2)).then((code) => {